Program Compilation

Compile, Assembly and Link

一个C/C++程序从源文件.c/.cpp到可执行文件.exe一般要经过以下四个步骤:

1

Fig. 1. 从源文件到可执行文件
  1. 预处理阶段:主要完成源文件的宏替换;
  2. 编译(Compile)阶段:将高级语言翻译为汇编语言、源程序翻译为汇编程序;
  3. 汇编(Assembly)阶段:将汇编语言翻译为机器能识别的二进制机器语言,生成的.o文件称可重定位目标文件,用于后续的链接操作;
  4. 链接(Link)阶段:将程序用到的库程序、自定义的依赖程序等与程序链接到一起,形成最终的可执行文件以及逻辑地址。

事实上,现在大多数编译器(Compiler)会同时完成编译和汇编的任务。

GCC

GCC,全称GNU C Compiler或CNU Compiler Collection,前者是其最初的称呼,是GNU Project的发起者为完善类Unix操作系统(即Linux)而开发的C/C++编译器,后来随着GCC的发展,其支持的语言也逐渐增多,如Java、Go等,由此才有了后面的称呼。通常,Linux发行版的操作系统都会自带GCC,如果没有,则需要手动安装。我们可以使用gcc --versiong++ --version来查看本机的GCC版本。

1
2
3
4
5
6
7
8
9
10
11
12
[meme@localhost Playground]$ gcc --version
gcc (GCC) 11.2.1 20220127 (Red Hat 11.2.1-9)
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

[meme@localhost Playground]$ g++ --version
g++ (GCC) 11.2.1 20220127 (Red Hat 11.2.1-9)
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

gcc是C编译程序,而g++是C++编译程序。本节将以gcc为例记录一些GCC编译器的用法。

在使用gcc前,我们先创建一个简单的C程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[meme@localhost Playground]$ cat > main.c
#include <stdio.h>

int main() {
printf("Goodbye, world!\n");
return 0;
}
[meme@localhost Playground]$ ls
main.c
[meme@localhost Playground]$ cat main.c
#include <stdio.h>

int main() {
printf("Goodbye, world!\n");
return 0;
}

a.out

若我们不为gcc提供任何选项而直接使用gcc编译文件,gcc会生成a.out作为该程序的可执行文件。需要注意的是,在Linux操作系统中,默认路径并不包含当前工作目录,因此需要使用./a.out来运行a.out

1
2
3
4
5
[meme@localhost Playground]$ gcc main.c
[meme@localhost Playground]$ ls
a.out main.c
[meme@localhost Playground]$ ./a.out
Goodbye, world!

a.out无法运行,则需要检查一下当前用户是否有运行a.out的权限。若无,则需用chmod a+x a.out来赋予当前用户权限。

-c, -o, -g

若我们想要指定可执行文件的名字,我们就需要指定选项来逐步编译。

  • -c选项示意gcc完成除Link以外的全部步骤,生成可重定位的.o文件:
    1
    2
    3
    [meme@localhost Playground]$ gcc -c main.c
    [meme@localhost Playground]$ ls
    a.out main.c main.o
  • -o选项示意gcc完成可重定位文件及其库文件的Link。其对象可以是.o文件,也可以是.c文件。若为.ogcc只完成Link;若为.cgcc将完成从源文件到可执行文件的所有步骤。需要注意的是,可执行文件的名字应严格置于-o之后:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    [meme@localhost Playground]$ gcc -o main main.o  # 等价于gcc main.o -o main
    [meme@localhost Playground]$ ls
    a.out main main.c main.o
    [meme@localhost Playground]$ ./main
    Goodbye, world!
    [meme@localhost Playground]$ rm main main.o
    [meme@localhost Playground]$ ls
    a.out main.c
    [meme@localhost Playground]$ gcc -o main main.c
    [meme@localhost Playground]$ ls
    a.out main main.c
    [meme@localhost Playground]$ ./main
    Goodbye, world!
  • -g选项使得程序以Debug模式编译,以该方式编译的程序可以使用GDB来进行Debug:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    [meme@localhost Playground]$ rm a.out main
    [meme@localhost Playground]$ gcc -g main.c
    [meme@localhost Playground]$ ls
    a.out main.c
    [meme@localhost Playground]$ gdb a.out
    GNU gdb (GDB) Red Hat Enterprise Linux 10.2-6.el7
    ...
    (gdb) q
    [meme@localhost Playground]$ ls
    a.out main.c
    [meme@localhost Playground]$ gcc -g -o main main.c
    [meme@localhost Playground]$ ls
    a.out main main.c
    [meme@localhost Playground]$ gdb main
    GNU gdb (GDB) Red Hat Enterprise Linux 10.2-6.el7
    ...
    (gdb) q

以上这3个就是GCC的3个基本选项,还有其他的选项如-l用于加入不在标准库中的第三方库等。

Make

一个项目往往会有多个相互包含的文件,如,我们移除之前生成的a.out以及main文件,并重新创建两个新文件add.cadd.h,同时修改main.c的内容让main.c引用add.c中的函数add

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[meme@localhost Playground]$ ls
main.c
[meme@localhost Playground]$ cat > add.c
int add(int a, int b) {
return a + b;
}
[meme@localhost Playground]$ cat > add.h
int add(int a, int b);
[meme@localhost Playground]$ ls
add.c add.h main.c
[meme@localhost Playground]$ vim main.c
[meme@localhost Playground]$ cat main.c
#include <stdio.h>
#include "add.h"

int main() {
printf("Goodbye, world!\n");
printf("%d\n", add(5, 3));
return 0;
}

由于两个文件的关系很简单,所以我们仍可以简单地生成a.out

1
2
3
4
[meme@localhost Playground]$ gcc main.c add.c
[meme@localhost Playground]$ ./a.out
Goodbye, world!
8

或者生成自命名的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[meme@localhost Playground]$ gcc -o main_add main.c add.c
[meme@localhost Playground]$ ls
add.c add.h a.out main_add main.c
[meme@localhost Playground]$ ./main_add
Goodbye, world!
8
[meme@localhost Playground]$ rm main_add
[meme@localhost Playground]$ ls
add.c add.h a.out main.c
[meme@localhost Playground]$ gcc -c main.c add.c
[meme@localhost Playground]$ ls
add.c add.h add.o a.out main.c main.o
[meme@localhost Playground]$ gcc -o main_add main.o add.o
[meme@localhost Playground]$ ls
add.c add.h add.o a.out main_add main.c main.o
[meme@localhost Playground]$ ./main_add
Goodbye, world!
8

上述两个程序很简单,因此手动地生成可执行文件仍是可行的。但是对于复杂的项目,其包含的程序可能有十几二十,甚至上百个,此时再手动地编译、链接就不太现实了。

make工具可以帮助我们省去每次重新编译时敲打文件名的麻烦。make基于用户预先编写的Makefile文件,实现自动编译、链接。使用make --version可以查看本机的make版本,在此我们先创建我们的Makefile文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
[meme@localhost Playground]$ make --version
GNU Make 4.3
Built for x86_64-redhat-linux-gnu
Copyright (C) 1988-2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
[meme@localhost Playground]$ rm a.out main_add main.o add.o
[meme@localhost Playground]$ ls
add.c add.h main.c
[meme@localhost Playground]$ touch Makefile
[meme@localhost Playground]$ ls
add.c add.h main.c Makefile

Makefilemake指定使用的文件名,它只是一个普通的文本文件,其内部的内容用于指导make完成编译操作,一个Makefile文件的基本内容有:

1
2
3
4
5
6
7
8
9
10
11
12
13
all: main

main: main.o add.o
gcc -o main main.o add.o

main.o: main.c
gcc -c main.c

add.o: add.c
gcc -c add.c

clean:
rm main main.o add.o

其中,all后面的是最终要生成的可执行文件的名称,其后续的main&main.o&add.o、冒号后的部分及下方的指令分别代表要生成的文件生成这些文件要依赖的其他文件相应的GCC指令。最后的clean使得我们能执行make clean来清除部分或所有生成的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[meme@localhost Playground]$ vim Makefile
[meme@localhost Playground]$ cat Makefile
all: main

main: main.o add.o
gcc -o main main.o add.o

main.o: main.c
gcc -c main.c

add.o: add.c
gcc -c add.c

clean:
rm main main.o add.o
[meme@localhost Playground]$ make
gcc -c main.c
gcc -c add.c
gcc -o main main.o add.o
[meme@localhost Playground]$ ls
add.c add.h add.o main main.c main.o Makefile
[meme@localhost Playground]$ ./main
Goodbye, world!
8
[meme@localhost Playground]$ make clean
rm main main.o add.o
[meme@localhost Playground]$ ls
add.c add.h main.c Makefile

make的另一个优点在于:在一次编译过后再次编译时,它只会编译被修改过的文件。比如,若我们将main.c中的add(5, 3)修改为add(5, 4)再重新编译,我们将得到如下结果(第一个make编译的是未修改前的程序):

1
2
3
4
5
6
7
8
9
10
11
[meme@localhost Playground]$ make
gcc -c main.c
gcc -c add.c
gcc -o main main.o add.o
[meme@localhost Playground]$ vim main.c
[meme@localhost Playground]$ make
gcc -c main.c
gcc -o main main.o add.o
[meme@localhost Playground]$ ./main
Goodbye, world!
9

可见,add.o并没有被重新生成。以上是makeMakefile的一些基本操作。想要了解更多有关GCC和Make的知识可以看南洋理工大学的一份指南:Compiling, Linking and Building C/C++ Applications

CMake

即便有了make,我们仍会遇到一些仅仅是编写Makefile就很麻烦的项目。cmake就是为了解决这项问题而出现的。类似于makecmake也有其特有的文件CMakeLists.txt。但是不同于make的是,cmake的特有文件是用于生成Makefile的。cmakemakegcc的关系如下所示:

1
2
              cmake           make       gcc
CMakeLists.txt -----> Makefile ----> Cmds ---> Binary

同样地,我们可以使用cmake --version查看本系统的cmake版本(若没有则需要安装)。

1
2
3
4
[meme@localhost Playground]$ cmake --version
cmake version 3.15.3

CMake suite maintained and supported by Kitware (kitware.com/cmake).

安装好cmake后,我们就可以在当前目录下创建CMakeLists.txt文件:

1
2
3
[meme@localhost Playground]$ touch CMakeLists.txt
[meme@localhost Playground]$ ls
add.c add.h add.o CMakeLists.txt main main.c main.o Makefile

CMakeLists.txt的编写比Makefile要更加复杂,事实上,其编写的方式本身就可以被视为一种新的语言。此处只记录一些基本的语法,更多的要去看官方文档CMake Tutorial

一个最基本的CMakeLists.txt会包含3个基本命令:

  • cmake_minimum_required():参数为该CMakeLists.txt文件所要求的最低cmake版本,是为了程序的可移植性考虑;
  • project():参数为最后生成的可执行文件名;
  • add_executable():参数为可执行文件名及其需要的源文件。

make中使用的main.cadd.c为例,其CMakeLists.txt应为:

1
2
3
4
5
6
7
cmake_minimum_required(VERSION 3.10)

# set the project name
project(main)

# add the executable
add_executable(main main.c add.c)

由于cmake利用CMakeLists.txt最终生成的是Makefile文件以及一些附属文件,我们通常会新建一个文件夹来执行cmake,一般我们会将该文件夹命名为build(也可自由命名):

1
2
3
4
[meme@localhost Playground]$ mkdir build
[meme@localhost Playground]$ ls
add.c add.h add.o build CMakeLists.txt main main.c main.o Makefile
[meme@localhost Playground]$ cd build

然后,在build文件夹中执行我们的cmake指令。由于CMakeLists.txt存在于父目录中,我们应使用cmake ..而不是单单的cmake

1
2
3
4
5
6
[meme@localhost build]$ cmake ..
-- The C compiler identification is GNU 11.2.1
...
-- Build files have been written to: /home/meme/Playground/build
[meme@localhost build]$ ls
CMakeCache.txt CMakeFiles cmake_install.cmake Makefile

得到Makefile后再执行make即可生成相应的可执行文件:

1
2
3
4
5
6
7
8
9
[meme@localhost build]$ make
make[1]: Entering directory '/home/meme/Playground/build'
...
make[1]: Leaving directory '/home/meme/Playground/build'
[meme@localhost build]$ ls
CMakeCache.txt CMakeFiles cmake_install.cmake main Makefile
[meme@localhost build]$ ./main
Goodbye, world!
9

cmake能跨目录执行,但是make只能在有Makefile的目录执行。

参考