以下内容源于C语言中文网的学习与整理,非原创,如有侵权请告知删除。

参考内容

(1)GCC 预处理器选项_dllbl的博客-CSDN博客

(2)Preprocessor Options (Using the GNU Compiler Collection (GCC))

(3)C/C++程序编译过程为什么要分为四个步骤? – 知乎(推荐)

一、编译的流程

编译C/C++ 程序,是指将C/C++源代码转变为可执行程序。

这需要经历4个过程:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)、链接(Linking)。

步骤命令等价命令输出文件
预处理cppgcc -E.i, .ii
编译cc1, cc1plusgcc -S.s
汇编asgcc -c.o, .obj
链接ldgcc可执行文件

二、一步完成编译

1、一步完成编译的方法

GCC 编译器并未提供给用户可用鼠标点击的界面窗口,要想调用 GCC 编译器编译 C 或者 C++ 程序,只能通过执行相应的 gcc 或者 g++ 指令。使用 GCC 编译器编译 C 或者 C++ 程序,也必须要经历上述的4 个过程。但考虑在实际使用中,用户可能并不关心程序的执行结果,只想快速得到最终的可执行程序,因此 gcc 和 g++ 都对此需求做了支持。

如下所示,运行C文件demo.c与C++文件demo.cpp的一步完成编译的指令,GCC 编译器就会在当前目录下生成对应的可执行文件,该文件的名称为 a.out,或者使用-o 选项指定要生成的文件名。

xjh@ubuntu:~/iot/tmp$ gcc demo.cxjh@ubuntu:~/iot/tmp$ g++ demo.cpp#或者 gcc -xc++ -lstdc++ -shared-libgcc demo.cpp
xjh@ubuntu:~/iot/tmp$ gcc demo.c -o demo.exexjh@ubuntu:~/iot/tmp$ g++ demo.cpp -o democpp.exe # 或者 gcc -xc++ -lstdc++ -shared-libgcc demo.cpp -o democpp.exe

虽然仅有一条指令,但依然按照预处理、编译、汇编、链接的过程将 C 、C++ 程序转变为可执行程序的。

2、如何保留中间文件

那些本应在预处理阶段、编译阶段、汇编阶段生成的中间文件,上述执行方式默认是不会生成的,只会生成最终的 a.out 可执行文件,除非为 gcc 或者 g++ 额外添加 -save-temps 选项

xjh@ubuntu:~/iot/tmp$ lsdemo.cxjh@ubuntu:~/iot/tmp$ gcc demo.cxjh@ubuntu:~/iot/tmp$ lsa.outdemo.c xjh@ubuntu:~/iot/tmp$ gcc demo.c -save-tempsxjh@ubuntu:~/iot/tmp$ lsa.outdemo.cdemo.idemo.odemo.sxjh@ubuntu:~/iot/tmp$ 

三、分步编译程序

表1列出了实际使用 gcc 或者 g++ 指令编译 C/C++ 程序时,常用的一些指令选项。

表 1 GCC常用的编译选项
gcc/g++指令选项功 能
-E预处理指定的源文件,不进行编译。
-S编译指定的源文件,不进行汇编。
-c编译、汇编指定的源文件,不进行链接。
-o指定生成文件的文件名。

-llibrary 或

-I library

其中 library 表示要搜索的库文件的名称。该选项用于手动指定链接环节中程序可以调用的库文件。建议 -l 和库文件名之间不使用空格,比如 -lstdc++。

-L/pathname该选项用于为GCC添加另一个搜索链接库的目录,其中pathname是链接库的路径。
-ansi对于 C 语言程序来说,其等价于 -std=c90;对于 C++ 程序来说,其等价于 -std=c++98。
-std=手动指令编程语言所遵循的标准,例如 c89、c90、c++98、c++11 等。

下面将以某个demo.cpp 源文件来说明如何分步编译一个C++程序。


1、预处理

(1)通过给 g++ 指令添加 -E 选项,可实现令 GCC 编译器只对目标源程序进行预处理操作。

(2)预处理,其实就是对各种预处理命令进行处理,包括头文件的包含、宏定义的扩展、条件编译的选择、去掉空行与注释等等。

  • 将所有的#define删除,并展开所有的宏定义。
  • 处理所有条件编译命令,比如#if、#ifdef、#elif、#else、#endif等。
  • 处理#include命令,将被包含文件的内容插入到该命令所在的位置,这与复制粘贴的效果一样。注意,这个过程是递归进行的,也就是说被包含的文件可能还会包含其他的文件。
  • 删除所有的注释///* ... */
  • 添加行号和文件名标识,便于在调试和出错时给出具体的代码位置。
  • 保留所有的#pragma命令,因为编译器需要使用它们。

Linux系统中,通常用 “.i” 或者 “.ii” 作为 C++ 程序预处理后所得文件的后缀名。当你无法判断宏定义是否正确,或者文件包含是否有效时,可以查看.i文件来确定问题。

(3)默认情况下 g++-E 指令只会将预处理操作的结果输出到屏幕上,并不会自动保存到某个文件。因此该指令往往会和 -o 选项连用,将结果导入到指令的文件中,如下所示。

xjh@ubuntu:~/iot/tmp$ lsdemo.cppxjh@ubuntu:~/iot/tmp$ g++ -E demo.cpp -o demo.ixjh@ubuntu:~/iot/tmp$ lsdemo.cppdemo.ixjh@ubuntu:~/iot/tmp$ 

(4)使用“g++-E ”表示只进行预处理操作,在此基础上我们还可以加上其他参数选项。相关介绍见参考内容(1)(2)。

选项功能
-C(大写字母)阻止GCC删除源文件和头文件中的注释
-include file如同在源代码中添加 #include “file” 一样。
…………
-dM告诉预处理器输出有效的宏定义列表(预处理结束时仍然有效的宏定义)

2、编译

(1)通过给 g++ 指令添加 -S 选项,即可令 GCC 编译器仅对指定预处理文件做编译操作。

(2)编译,其实就是将预处理得到的程序代码,经过一系列的词法分析、语法分析、语义分析以及优化,生成为当前机器支持的汇编代码文件(Linux 发行版通常以 “.s” 作为其后缀名)。

(3)即便没有-o 选项,编译结果也会输出到和预处理文件同名(后缀名改为 .s)的新建文件中。

xjh@ubuntu:~/iot/tmp$ g++ -S demo.i xjh@ubuntu:~/iot/tmp$ lsdemo.cppdemo.idemo.sxjh@ubuntu:~/iot/tmp$ 

(4)使用“g++-S”时,-S 选项只是表明让 GCC 编译器将指定文件处理至编译阶段结束,操作的文件不一定是经过预处理后得到的 .i 文件,也可以是源代码文件。如果操作对象为 .i 文件,则 GCC 编译器只需编译此文件;如果操作对象为 .c 或者 .cpp 源代码文件,则 GCC 编译器会对其进行预处理和编译这 2 步操作。

(5)在“g++-S”基础上,如果想提高文件内汇编代码的可读性,可以借助-fverbose-asm 选项,GCC 编译器会自行为汇编代码添加必要的注释,

xjh@ubuntu:~/iot/tmp$ g++ -S -fverbose-asm demo.ixjh@ubuntu:~/iot/tmp$ lsdemo.cppdemo.idemo.sxjh@ubuntu:~/iot/tmp$ 

3、汇编

(1)通过给 g++ 指令添加 -c 选项,即可令 GCC 编译器仅对指定的汇编代码文件做汇编操作。

(2)汇编阶段就是将之前生成的汇编代码文件(demo.s)做进一步转换,生成对应的机器指令。大部分汇编语句对应一条机器指令,有的汇编语句对应多条机器指令。相对于编译操作,汇编过程会简单很多,它并没有复杂的语法,也没有语义,也不需要做指令优化,只需要根据汇编语句和机器指令的对照表一一翻译即可。

(3)默认情况下汇编操作会自动生成一个和汇编代码文件名称相同、后缀名为 .o 的二进制文件(又称为目标文件)。

xjh@ubuntu:~/iot/tmp$ g++ -c demo.sxjh@ubuntu:~/iot/tmp$ lsdemo.cppdemo.idemo.odemo.sxjh@ubuntu:~/iot/tmp$

(4)和 g++-S 类似,g++-c 选项并非只能用于加工 .s 文件。事实上,-c 选项只是令 GCC 编译器将指定文件加工至汇编阶段,但不执行链接操作。这也就意味着:

  • 如果指定文件为源程序文件(例如 demo.cpp),则 g++-c 指令会对 demo.cpp 文件执行预处理、编译以及汇编这 3 步操作;
  • 如果指定文件为刚刚经过预处理后的文件(例如 demo.i),则 g++-c 指令对 demo.i 文件执行编译和汇编这 2 步操作;
  • 如果指定文件为刚刚经过编译后的文件(例如 demo.s),则 g++-c 指令只对 demo.s 文件执行汇编这 1 步操作。
  • 如果指定文件已经经过汇编,或者 GCC 编译器无法识别,则 g++-c 指令不做任何操作。

4、链接

(1)目标文件已经是二进制文件,与可执行文件的组织形式类似,只是有些函数和全局变量的地址还未找到,因此还无法执行。链接器的作用就是找到这些目标地址,将所有的目标文件组织成一个可以执行的二进制文件。它必须把符号(变量名、函数名等一些列标识符)用对应的数据的内存地址(变量地址、函数地址等)替代,以完成程序中多个模块的外部引用。

(2)另外,链接器也必须将程序中所用到的所有C标准库函数加入其中。对于链接器而言,链接库不过是一个具有许多目标文件的集合,它们在一个文件中以方便处理。

标准库的大部分函数通常放在文件 libc.a 中(文件名后缀.a代表“achieve”,译为“获取”),或者放在用于共享的动态链接文件 libc.so 中(文件名后缀.so代表“share object”,译为“共享对象”)。这些链接库一般位于 /lib/ 或 /usr/lib/,或者位于 GCC 默认搜索的其他目录。 当使用 GCC 编译和链接程序时,GCC 默认会链接 libc.a 或者 libc.so,但是对于其他的库(例如非标准库、第三方库等),就需要手动添加。

(3)如何实现链接操作?

只要将汇编阶段得到的 demo.o 作为参数传递给g++,g++ 会根据所给文件的后缀名 .o,自行判断出此类文件为目标文件,仅需要进行链接操作,g++就会在其基础上完成链接操作(GCC默认会链接 libc.a 或者 libc.so,但是对于其他的库,例如非标准库、第三方库等,就需要手动添加)。

如果不使用 -o 选项将执行结果输出到指定文件,则默认创建一个名为 a.out 的可执行文件,并将执行结果输出到该文件中。

xjh@ubuntu:~/iot/tmp$ g++ demo.oxjh@ubuntu:~/iot/tmp$ lsa.outdemo.cppdemo.idemo.odemo.sxjh@ubuntu:~/iot/tmp$ g++ demo.o -o demo.exexjh@ubuntu:~/iot/tmp$ lsa.outdemo.cppdemo.exedemo.idemo.odemo.sxjh@ubuntu:~/iot/tmp$