链接器可操作的最小元素
链接器可操作的最小元素是一个简单的目标文件,通常我们写的 .cpp 源文件编译后就生成了对应的目标文件,我们写的实现文件比如 list.cpp 编译后就生成了对应的目标文件 list.o (Windows下为list.obj),这个 list.o 就是链接器可以操作的最小元素。意味着我们可以多核进行编译。
目标文件
源文件被编译后生成的目标文件中本质上只有两部分:数据段和代码段
代码部分:计算机可以执行的机器指令,也就是源文件中定义的所有函数。比如上图中定义的函数fn_b以及fn_c。
数据部分:源文件中定义的全局变量。如果是已经初始化后的全局变量,该全局变量的值也存在于数据部分。
编译和链接
编译器在编译过程中遇到外部定义的全局变量或函数时,只要编译器能找到相应的变量声明就会在心里默念“all is well, all is well(一切顺利)“,从这里可以看出编译器的要求还是很低的,至于所使用变量的定义编译器是不会费力去四处搜索,而是愉快的继续接下来的编译。
编译器在遇到外部定义的全局变量或者函数时只要能在当前文件找到其声明,编译器就认为编译正确。而寻找使用变量定义的这项任务就被留给了链接器。链接器的其中一项任务就是要确定所使用的变量要有其唯一的定义。虽然编译器给链接器留了一项任务,但为了让链接器工作的轻松一点编译器还是多做了一点工作的,这部分工作就是符号表(Symbol table)。
符号表(Symbol table)
每一个生成的 elf 文件都有符号表,但是可执行文件的符号表是没有用的,符号表只用于链接,函数地址的寻找不是通过符号表,而是通过链接器替换。(重定位)
符号表中保存的信息有两部分:该目标文件中引用的全局变量以及函数,该目标文件中定义的全局变量以及函数。符号表只是想表达两件事:我能提供给其它文件使用的符号,我需要其它文件提供给我使用的符号。
在当前文件中变量仅仅是声明,编译器并没有找到其定义,那么就是引用未定义。剩余的变量编译器都可以在当前文件中找到其定义。static修饰过的函数的函数以及变量都是当前文件私有的,对外部不可见。
一个目标文件可以理解为如图所示的三段,而符号表中的内容就是上面的表格。
符号决议的过程
链接器会依次扫描每一个给定的目标文件,链接器还维护了两个集合,一个是已定义符号集合D,另一个是未定义符合集合U。
对于当前目标文件,查找其符号表,并将已定义的符号并添加到已定义符号集合 D 中。
对于当前目标文件,查找其符号表,将每一个当前目标文件引用的符号与已定义符号集合 D 进行对比,如果该符号不在集合D中则将其添加到未定义符合集合U中。
当所有文件都扫描完成后,如果未定义符号集合U不为空,则说明当前输入的目标文件集合中有未定义错误,链接器报错,整个编译过程终止。
上面的过程看似复杂,其实用一句话概括每个目标文件所引用变量都能在其它目标文件中找到唯一的定义,整个链接过程就是正确的。