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
的目录执行。