我们在编写代码时,往往编译和运行一气呵成,但我们很少知道编译器的处理操作。例如像"hello world"程序在linux下,用GCC来编译时,只需要最简单的命令(假设源代码文件名为hello.c)
gcc hllo.c
./a.out
hello world
这看似简单的代码,过程可以分成四个步骤:预处理、编译、汇编、链接。
预编译:
1.将所有"#define"删除,并且展开所有宏定义。
2.处理所有条件预编译指令,比如“#if”、“#elif”、“#else”、“#endif”。
3.处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能包含其他文件。
4.删除所有的注释“//”和“/* */”。
5添加行号和文件名标识,比如#“hello.c”2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
6.保留所有的#pragma编译器指令,因为编译器须要是用它们。
经过预编译后的.i文件不包括任何宏定义,因为所有的宏已经被展开,并且包含的文件已经被插入到.i文件中。所以无法判断定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。
编译:
就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件。
汇编:
汇编器将汇编代码变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来说比较简单,没有复杂语法,也没有语义,也不需要做指令优化,只是根据汇编指令的对照表一一翻译。使用gcc命令从c源代码文件开始,经过预编译、编译和汇编直接输出目标文件:
gcc -c hello.c -o hello.o
链接:
我们需要将一大堆文件链接起来才可以得到"a.out",即最终的可执行文件。链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。链接过程主要包括地址和空间分配、符号决议、重定位等步骤。
最基本的静态链接过程是每个模块源代码文件经过编译器编译成目标文件(.o或.obj),目标文件和库一起链接形成最终可执行文件。最常见的就是运行时库,是支持程序运行的基本函数的集合。库是一组目标文件的包,就是一些常用代码编译成目标文件后打包存放。
静态链接的基本过程和作用:使用连接器时,可以直接引用其他模块的函数和全局变量而无需知道它们的地址,因为链接器在链接的时候,会根据所引用的符号,自动对应.c模块查找其地址,然后将main.c模块中所有引用到函数的指令重新修正,让它们的目标地址为真正的函数地址。
在链接过程中,对其他定义在目标文件中的函数调用的指令需要被重新调整,对使用其他定义在其他目标文件的变量来说,也存在相同的问题。