GCC 编译 C++ 程序分步骤流程(预处理 gcc -E、编译 gcc -S、汇编 gcc -c 和链接 gcc 以及 gcc -o 选项)
C 或者 C++ 程序从源代码生成可执行程序的过程,需经历 4 个过程,分别是预处理、编译、汇编和链接。
同样,使用 GCC 编译器编译 C 或者 C++ 程序,也必须要经历这 4 个过程。但考虑在实际使用中,用户可能并不关心程序的执行结果,只想快速得到最终的可执行程序,因此 gcc 和 g++ 都对此需求做了支持。
#include <iostream>
int main()
{
std::cout << "hello,world" << std::endl;
return 0;
}
编写如下指令并执行
wohu@ubuntu:~/cpp/src$ g++ test.cpp
wohu@ubuntu:~/cpp/src$ ls
a.out test.cpp
同样,GCC 编译器会在当前目录下生成一个名为 a.out 的可执行文件(如果之前有同名文件,旧文件会被覆盖)。通过如下指令即可运行该文件:
wohu@ubuntu:~/cpp/src$ ./a.out
hello,world
wohu@ubuntu:~/cpp/src$
注意,gcc 或者 g++ 指令还支持用户手动指定最终生成的可执行文件的文件名,
wohu@ubuntu:~/cpp/src$ g++ test.cpp -o main
wohu@ubuntu:~/cpp/src$ ls
a.out main test.cpp
wohu@ubuntu:~/cpp/src$ ./main
hello,world
wohu@ubuntu:~/cpp/src$
其中 -o 选项用于指定要生成的文件名,例如 -o main 即表示将生成的可执行文件名设为 main。
1. gcc -o选项
gcc -o 选项用来指定输出文件,如果不使用 -o 选项,那么将采用默认的输出文件。例如默认情况下,生成的可执行文件的名字默认为 a.out。
如下是 gcc -o 指令的使用语法格式:
gcc [-E|-S|-c] [infile] [-o outfile]
其中,用方括号 [] 括起来的部分可以忽略。
- [infile] 表示输入文件(也即要处理的文件),它可以是源文件、汇编文件或者目标文件;
- [outfile] 表示输出文件(也即处理的结果),可以是预处理文件、目标文件、可执行文件等;
值得一提的是,通常情况下 [infile] 处放置一个文件,但根据实际需要也可以放置多个文件,表示有多个输入文件。
注意,虽然我们仅编写了一条 gcc 或者 g++ 指令,但其底层依据是按照预处理、编译、汇编、链接的过程将 C 、C++ 程序转变为可执行程序的。而本应在预处理阶段、编译阶段、汇编阶段生成的中间文件,此执行方式默认是不会生成的,只会生成最终的 a.out 可执行文件(除非为 gcc 或者 g++ 额外添加 -save-temps 选项)。
查看该过程中产生的中间文件。如此,上面介绍的执行方式将不再使用,而要采用分步编译的方式。
所谓“分步编译”,即由用户手动调用 GCC 编译器完成对 C、C++源代码的预处理、编译、汇编以及链接过程,每个阶段都会生成对源代码加工后的文件。
GCC 常用的编译选项

接下来将对如何实现分步编译做详细的讲解,即如何将一个源代码程序经历预处理、编译、汇编以及链接这 4 个过程,最终生成对应的可执行程序。
2. 预处理
无论是 C 还是 C++ 程序,其从源代码转变为可执行代码的过程,具体可分为 4 个过程,分别为预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。
默认情况下,gcc 指令会一气呵成,直接将源代码历经这 4 个过程转变为可执行代码,且不会保留各个阶段产生的中间文件。
而如果想查看这 4 个阶段各自产生的中间文件,最简单直接的方式就是对源代码进行“分步编译”,即控制 GCC 编译器逐步对源代码进行预处理、编译、汇编以及链接操作。其中,通过为 gcc 指令添加 -E 选项,即可控制 GCC 编译器仅对源代码做预处理操作。
所谓预处理操作,主要是处理那些源文件和头文件中以 # 开头的命令(比如 #include、#define、#ifdef 等),并删除程序中所有的注释 // 和 /* … */
默认情况下 gcc -E 指令只会将预处理操作的结果输出到屏幕上,并不会自动保存到某个文件。因此该指令往往会和 -o 选项连用,将结果导入到指令的文件中。比如:
wohu@ubuntu:~/cpp/src$ g++ -E test.cpp -o test.ii
wohu@ubuntu:~/cpp/src$ ls
test.cpp test.ii
wohu@ubuntu:~/cpp/src$
Linux 系统中,通常用 .i 或者 .ii 作为 C/C++ 程序预处理后所得文件的后缀名。由此,就完成了 test.cpp 文件的预处理操作,并将其结果导入到了 test.ii 文件中。
读者可以通过执行 cat test.ii 指令查看该文件中的内容,但通常没有足够 C++ 语言功底的读者是看不懂的。为此,我们可以为 g++ 指令再添加一个 -C 选项,阻止 GCC 删除源文件和头文件中的注释:
wohu@ubuntu:~/cpp/src$ g++ -E -C test.cpp -o test.ii
注意,这里是大写的 -C,不是小写的 -c。小写的 -c 另作他用,
gcc -E 支持的常用选项

其中,对于指定 #include 搜索路径的几个选项,作用的先后顺序如下:
- 对于用
#include ""引号形式引入的头文件,首先搜索当前程序文件所在的目录,其次再前往-iquote选项指定的目录中查找; - 前往
-I选项指定的目录中搜索; - 前往
-isystem选项指定的目录中搜索; - 前往默认的系统路径下搜索;
- 前往
-idirafter选项指定的目录中搜索;
3. 编译
所谓编译,简单理解就是将预处理得到的程序代码,经过一系列的词法分析、语法分析、语义分析以及优化,加工为当前机器支持的汇编代码。
通过给 g++ 指令添加 -S(注意是大写)选项,即可令 GCC 编译器仅将指定文件加工至编译阶段,并生成对应的汇编代码文件。例如:
wohu@ubuntu:~/cpp/src$ g++ -S test.ii
wohu@ubuntu:~/cpp/src$ ls
test.cpp test.ii test.s
wohu@ubuntu:~/cpp/src$
可以看到,经过执行 g++ -S 指令,其生成了一个名为 test.s 的文件,这就是经过编译的汇编代码文件。也就是说默认情况下,编译操作会自行新建一个文件名和指定文件相同、后缀名为 .s 的文件,并将编译的结果保存在该文件中。
我们还可以为 gcc -S 指令添加 -o 选项,令 GCC 编译器将编译结果保存在我们指定的文件中。例如:
wohu@ubuntu:~/cpp/src$ g++ -S test.ii -o demo.s
wohu@ubuntu:~/cpp/src$ ls
demo.s test.cpp test.ii test.s
wohu@ubuntu:~/cpp/src$
需要注意的是,gcc -S 指令操作的文件并非必须是经过预处理后得到的 .i 或 .ii 文件,-S 选项的功能是令 GCC 编译器将指定文件处理至编译阶段结束。这也就意味着,gcc -S 指令可以操作预处理后的 .i 或 .ii 文件,也可以操作源代码文件:
- 如果操作对象为
.i或.ii文件,则GCC编译器只需编译此文件; - 如果操作对象为
.c或者.cpp源代码文件,则GCC编译器会对其进行预处理和编译这 2 步操作。
因此,如果我们想直接得到 test.cpp 文件对应的汇编文件,就可以借助 gcc -S 指令:
wohu@ubuntu:~/cpp/src$ g++ -S test.cpp -o test.s
wohu@ubuntu:~/cpp/src$ ls
test.cpp test.s
wohu@ubuntu:~/cpp/src$
由此,我们就可以直接获得 test.cpp 对应的 test.s 汇编文件。
如果想提高文件内汇编代码的可读性,可以借助 -fverbose-asm 选项,GCC 编译器会自行为汇编代码添加必要的注释,例如:
wohu@ubuntu:~/cpp/src$ g++ -S test.cpp -o test.s -fverbose-asm
wohu@ubuntu:~/cpp/src$ ls
test.cpp test.s
wohu@ubuntu:~/cpp/src$
4. 汇编
如何对已得到的 test.s 执行汇编操作,并得到相应的目标文件,需要进行汇编操作。所谓目标文件,其本质为二进制文件,但由于尚未经过链接操作,所以无法直接运行。
简单地理解,汇编其实就是将汇编代码转换成可以执行的机器指令。大部分汇编语句对应一条机器指令,有的汇编语句对应多条机器指令。相对于编译操作,汇编过程会简单很多,它并没有复杂的语法,也没有语义,也不需要做指令优化,只需要根据汇编语句和机器指令的对照表一一翻译即可。
通过为 gcc 指令添加 -c 选项(注意是小写字母 c),即可让 GCC 编译器将指定文件加工至汇编阶段,并生成相应的目标文件。例如:
wohu@ubuntu:~/cpp/src$ ls
test.cpp test.s
wohu@ubuntu:~/cpp/src$ g++ -c test.s
wohu@ubuntu:~/cpp/src$ ls
test.cpp test.o test.s
wohu@ubuntu:~/cpp/src$
可以看到,该指令生成了和 test.s 同名但后缀名为 .o 的文件,这就是经过汇编操作得到的目标文件。
还可以为 gcc -c 指令在添加一个 -o 选项,用于将汇编操作的结果输入到指定文件中,例如:
wohu@ubuntu:~/cpp/src$ g++ -c test.s -o test_obj.o
wohu@ubuntu:~/cpp/src$ ls
test.cpp test.o test_obj.o test.s
wohu@ubuntu:~/cpp/src$
需要强调的一点是,和 g++ -S 类似,g++ -c 选项并非只能用于加工 .s 文件。事实上,-c 选项只是令 GCC 编译器将指定文件加工至汇编阶段,但不执行链接操作。这也就意味着:
- 如果指定文件为源程序文件(例如 test.cpp),则
gcc -c指令会对 test.cpp 文件执行预处理、编译以及汇编这 3 步操作; - 如果指定文件为刚刚经过预处理后的文件(例如 test.i),则
gcc -c指令对 test.i 文件执行编译和汇编这 2 步操作; - 如果指定文件为刚刚经过编译后的文件(例如 test.s),则
gcc -c指令只对test.s文件执行汇编这 1 步操作。
注意,如果指定文件已经经过汇编,或者 GCC 编译器无法识别,则 gcc -c 指令不做任何操作。
5. 链接
总的来说链接阶段要完成的工作,就是将同一项目中各源文件生成的目标文件以及程序中用到的库文件整合为一个可执行文件。
目标文件已经是二进制文件,与可执行文件的组织形式类似,只是有些函数和全局变量的地址还未找到,因此还无法执行。链接的作用就是找到这些目标地址,将所有的目标文件组织成一个可以执行的二进制文件。
完成链接操作,并不需要给 g++ 添加任何选项,只要将汇编阶段得到的 test.o 作为参数传递给它,g++就会在其基础上完成链接操作。例如:
wohu@ubuntu:~/cpp/src$ ls
test.cpp test.o test_obj.o test.s
wohu@ubuntu:~/cpp/src$ g++ test.o
wohu@ubuntu:~/cpp/src$ ls
a.out test.cpp test.o test_obj.o test.s
wohu@ubuntu:~/cpp/src$ g++ test_obj.o -o test_obj
wohu@ubuntu:~/cpp/src$ ls
a.out test.cpp test.o test_obj test_obj.o test.s
wohu@ubuntu:~/cpp/src$ ./a.out
hello,world
wohu@ubuntu:~/cpp/src$ ./test_obj
hello,world
wohu@ubuntu:~/cpp/src$
gcc 会根据所给文件的后缀名 .o,自行判断出此类文件为目标文件,仅需要进行链接操作。
通过分别执行这 a.out 和 test_obj 可执行文件,其执行结果完全相同,说明 test.o 和 test_obj.o 是完全相同的。
6. 一步生成所有中间结果
如果读者不想执行这么多条指令,但想获得预处理、编译、汇编以及链接这 4 个过程产生的中间文件,可以执行如下指令:
wohu@ubuntu:~/cpp/src$ ls
test.cpp
wohu@ubuntu:~/cpp/src$ g++ test.cpp -save-temps
wohu@ubuntu:~/cpp/src$ ls
a.out test.cpp test.ii test.o test.s
wohu@ubuntu:~/cpp/src$
可以看到,通过给 g++ 添加 -save-temps 选项,可以使 GCC 编译器保留编译源文件过程中产生的所有中间文件。
更多推荐


所有评论(0)