概述
我们写出的C语言代码(.c文件),若要在机器上运行,需要经过一个编译过程,主要分为如下四个阶段(参考1,表1):
- 预处理阶段,即完成宏定义和include 文件展开等工作;生成.i文件。GCC命令为:·
gcc -E
- 根据编译参数进行不同程序的优化,编译成汇编代码;生成.s文件。GCC命令为:·
gcc -S
- 用汇编器把上一阶段生成的汇编代码进一步生成目标代码;生成.o文件。GCC命令为:·
gcc -C
- 用链接器把上一阶段生成的目标代码、其他一些相关的系统提供的目标代码(如crtx.o)和系统或用户提供的库链接起来,生成最终的执行代码。生成可执行文件。GCC命令为:·
gcc
具体如下所示(参考2)
预处理(Preprocessing)
预处理主要进行以下几个方面的处理:
- 宏定义指令
如#define Pi 3.1415
,预处理阶段会将程序中所有的Pi
用3.1415代替。与之对应的#undef
则会取消对某个宏的定义,使之后面出现时不再被替换。 - 条件编译指令
如#ifdef
、#ifndef
、#else
、#elif
、#endif
等伪指令的引入可以使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理,即预处理阶段将根据有关的文件将不必要的代码过滤掉。 - 头文件包含指令
如#include
,头文件中一般通过#define
定义了一些宏(如字符常量),同时也包含了各种外部符号的声明。采用头文件可以使一些定义在多个不同的C源程序中使用,而不必在文件中重新定义。预处理阶段会将头文件中的定义加入到引用它的代码中。 - 特殊符号
如在源程序中出现的FUNCTION
会被解释为当前被编译的C源程序中的函数名称。预处理阶段会对源程序中出现的这些特殊符号用合适的值进行替换。
总结:可以看出,预处理阶段主要是完成对源程序的替换工作。经过替换后,会生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件.i,该输出文件中只有常量如数字、字符等,或者是变量的定义,以及C语言的关键字如if
、else
等。
编译(Compilation)
编译阶段所有做的工作就是通过词法分析和语法分析,在确认所有指令都符合语法规则之后,将其翻译成等价的中间代码或者是汇编代码。
编译阶段会对代码进行优化处理,不仅涉及到编译技术本身,还涉及到机器的硬件环境。优化分为两部分:
- 不依赖于具体计算机的优化。主要是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制、已知量的合并等)、无用赋值的删除等
- 同机器硬件结构相关的优化。主要考虑如何充分利用机器的硬件寄存器存放的有关变量的值以减少内存的访问次数;根据机器硬件执行指令的特点对指令进行调整使目标代码比较短,执行效率更高等。
汇编(Assemble)
汇编是把汇编语言代码翻译成目标机器指令的过程。目标文件中存放的是与源程序等效的目标机器语言代码。目标文件由段组成,通常一个目标文件中至少有两个段:
- 代码段:主要包含程序的指令。该段一般是可读和可执行的,一般不可写。
- 数据段:主要存放程序中用到的各种全局变量和静态的数据。一般数据段是可读、可写、可执行的。
链接(Linking)
链接阶段的主要工作是将有关的目标文件相链接,即将一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有这些目标文件成为一个能够被操作系统执行的统一整体。举例如下:
某个源文件的函数引用了另一个源文件中定义的变量和函数,因此需要链接阶段将这些变量和函数连接在一起。
根据开发人员指定的同库函数的链接方式的不同,链接处理分为两种:
- 静态链接:
函数代码将从其所在的静态链接库中被拷贝到最终的可执行程序中。 - 动态链接:
此时,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所做的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的信息,在可执行程序被执行时,动态链接库的全部内容将被影射到运行时相应进程的虚拟地址空间。动态链接程序将根据可执行程序中记录的信息找到对应的函数代码。
备注:使用动态链接能使最终的可执行文件较小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。
附录
表1 GCC识别的主要文件扩展名
文件扩展名 | 文件类型 |
---|---|
.c | C语言代码 |
.C、.cc | C++语言代码 |
.i | 预处理后的C语言代码 |
.s、.S | 汇编语言代码 |
.o | 目标代码 |
.a | 静态链接库(程序编译时使用) |
.so | 动态链接库(程序运行时使用) |