编译与链接
之前天天编软件,总是一键“build”,然后等啊等啊,修改报错的地方。相信也有一些非计算机专业的软件工程师和我一样,从来不知道到底我们编译出来的hex文件到底是怎么生成的。直到最近看了一本《程序员的自我修养》我才找到了答案。
对于我们平常的嵌入式C语言开发中,我们很少关注与C语言的编译与链接过程。因为现在的集成开发环境(IDE)已经将编译、链接过程一步到位,也就是IDE里的“build”键。一键build虽然大大方便了软件工程师,但是在软件开发过程中莫名其妙的error,使得工程师很难看清楚编译过程的本质。如果能够深入了解这些机制,那么解决这些问题就能够更加得心应手。事实上,C语言的编译都可以分解为4个步骤,分别为预编译(Preprocess),编译(Compilation),汇编(Assembly)和链接(Linking)。
1预编译
预编译过程总主要处理那些源代码中以“#”开始的预编译指令,比如#include,#define,以及条件编译##if,#ifdef,#elif,#else,#endfi。包括:
(1)所有的#define删除,并展开所有的宏定义。
(2)将#include包含的文件插入到该预编译指令的位置。这个过程是递归的,也就是说被包含的文件还可能包含着其他文件。
(3)删除所有注释
(4)添加行号和文件名标识,以便于编译器产生调试用的行号信息及用于编译时产生的编译错误或警告时能够显示行号。
(5)保留所有的#pragma编译器指令,因为编译器需要使用它们。
经过预编译后的.i文件不包含任何宏定义,因为宏定义已经被展开,并且包含的文件也已经被插入到.i文件中。所以当我们无法判断宏定义或头文件是否正确时,可以查看预编译后的文件来确定问题。
2编译
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后产生相应的汇编代码文件,这个过程往往是我们所说的最核心也最复杂的部分。
编译时,编译器需要的是语法的正确,函数与变量的声明的正确,只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件(.o文件或是.obj文件)。
3汇编
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。
4链接
在现代软件开发过程中,软件的规模往往都很大,动辄数百万行的代码,如果都放在一个.C文件里肯定无法想象。所以现代的大型软件往往都拥有成千上万个模块,这些模块之间相互依赖又相互独立。这种按照层次化及模块化存储和组织源代码有很多好处,比如代码更加容易阅读、理解、重用。每个模块可以单独开发、编译、测试,更改部分代码不需要编译整个程序等。
在一个程序被分割成为多个模块后,这些模块如何组合形成一个单独的程序是必须要解决的!模块之间如何组合的问题可以归结于模块之间的函数调用与变量访问。函数访问必须知道目标函数的地址,访问变量也必须知道目标变量的地址,所以 这两种方式可以归咎于一种方式,那就是模块间的符号应用。模块间依靠符号来通信类似于拼图,定义符号的模块多出一块区域,引用该符号的模块刚好少那一块区域。这个欧快的拼接过程就是链接(Linking)。
由于在各个模块编译的过程中,编译器并不知道它们的地址,所以它暂时将这些地址搁置。等待最后链接的时候由链接器去将这些指令的目标地址修正。所以定义在其他模块的全局变量和函数在最终运行时的绝对地址都要在最终链接的时候才能确定。
有时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib 文件,在UNIX下,是Archive File,也就是.a文件。总而言之,链接就是那些目标文件之间相互链接自己所需要的函数和全局变量,而函数可能来源于其他目标文件或库文件。
《程序员的自我修养》俞甲子,石凡,潘爱民
编译与链接的区别,MONKEY_D_MENG