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

- 预处理阶段:主要完成源文件的宏替换;
- 编译(Compile)阶段:将高级语言翻译为汇编语言、源程序翻译为汇编程序;
- 汇编(Assembly)阶段:将汇编语言翻译为机器能识别的二进制机器语言,生成的
.o文件称可重定位目标文件,用于后续的链接操作; - 链接(Link)阶段:将程序用到的库程序、自定义的依赖程序等与程序链接到一起,形成最终的可执行文件以及逻辑地址。
事实上,现在大多数编译器(Compiler)会同时完成编译和汇编的任务。
GCC
GCC,全称GNU C Compiler或CNU Compiler Collection,前者是其最初的称呼,是GNU Project的发起者为完善类Unix操作系统(即Linux)而开发的C/C++编译器,后来随着GCC的发展,其支持的语言也逐渐增多,如Java、Go等,由此才有了后面的称呼。通常,Linux发行版的操作系统都会自带GCC,如果没有,则需要手动安装。我们可以使用gcc --version或g++ --version来查看本机的GCC版本。
1 | [meme@localhost Playground]$ gcc --version |
gcc是C编译程序,而g++是C++编译程序。本节将以gcc为例记录一些GCC编译器的用法。
在使用gcc前,我们先创建一个简单的C程序:
1 | [meme@localhost Playground]$ cat > main.c |
a.out
若我们不为gcc提供任何选项而直接使用gcc编译文件,gcc会生成a.out作为该程序的可执行文件。需要注意的是,在Linux操作系统中,默认路径并不包含当前工作目录,因此需要使用./a.out来运行a.out。
1 | [meme@localhost Playground]$ gcc main.c |
若
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文件。若为.o则gcc只完成Link;若为.c则gcc将完成从源文件到可执行文件的所有步骤。需要注意的是,可执行文件的名字应严格置于-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.c和add.h,同时修改main.c的内容让main.c引用add.c中的函数add:
1 | [meme@localhost Playground]$ ls |
由于两个文件的关系很简单,所以我们仍可以简单地生成a.out:
1 | [meme@localhost Playground]$ gcc main.c add.c |
或者生成自命名的文件:
1 | [meme@localhost Playground]$ gcc -o main_add main.c add.c |
上述两个程序很简单,因此手动地生成可执行文件仍是可行的。但是对于复杂的项目,其包含的程序可能有十几二十,甚至上百个,此时再手动地编译、链接就不太现实了。
make工具可以帮助我们省去每次重新编译时敲打文件名的麻烦。make基于用户预先编写的Makefile文件,实现自动编译、链接。使用make --version可以查看本机的make版本,在此我们先创建我们的Makefile文件:
1 | [meme@localhost Playground]$ make --version |
Makefile是make指定使用的文件名,它只是一个普通的文本文件,其内部的内容用于指导make完成编译操作,一个Makefile文件的基本内容有:
1 | all: main |
其中,all后面的是最终要生成的可执行文件的名称,其后续的main&main.o&add.o、冒号后的部分及下方的指令分别代表要生成的文件、生成这些文件要依赖的其他文件和相应的GCC指令。最后的clean使得我们能执行make clean来清除部分或所有生成的文件。
1 | [meme@localhost Playground]$ vim Makefile |
make的另一个优点在于:在一次编译过后再次编译时,它只会编译被修改过的文件。比如,若我们将main.c中的add(5, 3)修改为add(5, 4)再重新编译,我们将得到如下结果(第一个make编译的是未修改前的程序):
1 | [meme@localhost Playground]$ make |
可见,add.o并没有被重新生成。以上是make及Makefile的一些基本操作。想要了解更多有关GCC和Make的知识可以看南洋理工大学的一份指南:Compiling, Linking and Building C/C++ Applications。
CMake
即便有了make,我们仍会遇到一些仅仅是编写Makefile就很麻烦的项目。cmake就是为了解决这项问题而出现的。类似于make,cmake也有其特有的文件CMakeLists.txt。但是不同于make的是,cmake的特有文件是用于生成Makefile的。cmake、make和gcc的关系如下所示:
1 | cmake make gcc |
同样地,我们可以使用cmake --version查看本系统的cmake版本(若没有则需要安装)。
1 | [meme@localhost Playground]$ cmake --version |
安装好cmake后,我们就可以在当前目录下创建CMakeLists.txt文件:
1 | [meme@localhost Playground]$ touch CMakeLists.txt |
CMakeLists.txt的编写比Makefile要更加复杂,事实上,其编写的方式本身就可以被视为一种新的语言。此处只记录一些基本的语法,更多的要去看官方文档CMake Tutorial。
一个最基本的CMakeLists.txt会包含3个基本命令:
cmake_minimum_required():参数为该CMakeLists.txt文件所要求的最低cmake版本,是为了程序的可移植性考虑;project():参数为最后生成的可执行文件名;add_executable():参数为可执行文件名及其需要的源文件。
以make中使用的main.c和add.c为例,其CMakeLists.txt应为:
1 | cmake_minimum_required(VERSION 3.10) |
由于cmake利用CMakeLists.txt最终生成的是Makefile文件以及一些附属文件,我们通常会新建一个文件夹来执行cmake,一般我们会将该文件夹命名为build(也可自由命名):
1 | [meme@localhost Playground]$ mkdir build |
然后,在build文件夹中执行我们的cmake指令。由于CMakeLists.txt存在于父目录中,我们应使用cmake ..而不是单单的cmake
1 | [meme@localhost build]$ cmake .. |
得到Makefile后再执行make即可生成相应的可执行文件:
1 | [meme@localhost build]$ make |
cmake能跨目录执行,但是make只能在有Makefile的目录执行。