链接,就是将不同部分的代码和数据收集和组合成为一个单一文件的过程,这个文件可被加载到存储器中执行。链接可以执行于编译时(compile time) ,也就是源代码被翻译成机器代码时(eg.普通的链接器链接,以及静态链接库,由静态链接器链接);也可以执行于加载时(例如动态链接库的加载时链接);也可以执行于运行时(run time)(动态链接库在应用程序中链接)。
1. 编译器驱动程序----首先,看一下程序在完成后系统的一些动作(以main.cpp为例,一下均以main.xx为例)
main.cpp 经过 C预处理器 ASCII码中间文件 main.i
main.i 经过 C编译器 ASCII汇编语言文件 main.s
main.s 经过 汇编器 可重定位目标文件 main.o
main.o 经过 链接器 可执行的目标文件 假设为p
系统shell 调用加载器(loader), 拷贝可执行文件p 中的代码和数据到存储器,然后控制转移到找个程序的开头运行找个程序。
2. 链接器的动作
Unix 链接器 以一组可重定位的目标文件和命令行参数作为输入,生成一个可以完全链接的可以加载和运行的可执行目标文件。
它所完成的动作有:
a. 符号解析(symbol resolution): 将每个符号引用和一个符号定义联系起来。符号:程序中的函数名、变量名等。
b. 重定位(relocation): 编译器和汇编器生成的是从地址零开始的代码和数据节。链接器把每个符号定义与存储器位置联系起来,并修改符号引用,使之指向这个存储器位置,从而重定位这些符号。---即,为符号分配存储位置,并用找个地址代替程序中对符号的引用。
3. 符号解析
对于每个输入的文件,汇编器会产生一个可重定位的目标文件main.o,Unix系统中为ELF格式。这个文件中由不同的节构成,常用的有如下几个:
.text 代码段,存放编译后的代码
.data 数据段,存放程序文件中初始化的全局变量
.bss 数据段,没有空间,名义上存放未初始化的全局变量,为提高效率而设
.systab 符号表,存放在程序中定义或引用的全局变量或函数符号,并有一个数据结构维护,用于符号解析
.rel.text 重定位代码段(即函数) - 存放 需要重定位的符号的重定位表目,一个数据结构维护,存放重定位信息。
.rel.data 重定位数据段(即全局变量) 内容同上
(详见--深入理解计算机系统一书 page 465)
符号解析,即根据.systab中的符号表,对每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义进行联系。如果有的符号引用找不到对应的定义,则会产生错误,undefined reference!!
若同时出现多个符号定义,则函数和已初始化的全局变量为强符号,未初始化的全局变量为弱符号,链接器按如下方式进行符号解析
1. 不允许有多个强符号
2. 若一个强符号,若干个弱符号,则选择强符号
3. 若 多个弱符号,则从其中选择一个。
需要特别注意,多个定义中所引起的,存储器越界错误,见 p471
4. 重定位
汇编器在处理main.s时候,只要遇到最终位置没有确定的符号,即全局变量或函数,就会在rel.text 或者 rel.data中生成一个帮助链接器进行重定位的表目,用一个数据结构进行维护,在链接器进行链接时候,就会利用这些信息对符号进行重定位。具体见 p 477
5. 静态链接库
静态链接库,是一些已经编译好的函数代码模块,可以在链接时和main.s一起进行链接,那么源程序文件main.cpp中就可以使用静态链接库已经写好的函数或者变量,当然肯定是函数居多。
在main.o中没有找到定义的符号引用,链接器就会在一起链接的静态链接库中寻找,如果找到,就会把相应的模块代码拷贝到最终要生成的可执行文件中,若没找到,则生成错误。
需要注意的是,链接器只拷贝它需要的代码,而不是所有的代码,所以,在链接时候,一定要考虑好源文件和静态链接库之间的依赖关系,按顺序链接,否则,可能会出错。
6. 动态链接库(共享库)
这是个很有用的东东,即可以在应用程序中在运行时候加载(需要特殊的头文件和函数),也可以在加载时候完成链接。它的特性是,整个存储器中是独一份的,只有一个副本供使用,即多个进程可以共享整个玩意儿。即,它不需要像静态链接库那样拷贝到最后的可执行文件中,动态加载到存储器就可以,大大节省了存储空间。
但有一个问题,即,如果共享库中有使用全局数据或者调用函数,在重定位阶段需要修改代码为所使用符号的绝对地址,程序才能正常运行,但由于不同的进程对应着不同的共享库加载地址,也即,不同的进程会对代码进行不同的修改,这样的话,因为只有一个副本,那多进程共享就行不通的啦。因此,产生一个这样的机制,来解决整个问题,使得代码运行与共享库的加载地址没有直接的关系。即PIC(position independent code)
首先,我们来看,代码只有一份,也就是代码段物理地址唯一,不同进程加载时候,进程间所加载的虚拟地址不同。但是数据段的物理地址肯定是不同的,代码段不能修改,我们可以修改数据段。这个过程得益于一个逻辑,即数据段总是分配为紧随代码段后面(虚拟地址),即,数据段和代码段之间的相对地址是不会随加载地址改变而改变的。
于是,我们在数据段开头的位置,维护一个GOT表,它为每个符号定义了一个重定位表目,加载时,动态链接器会重定位GOT表中的表目,使得它包含正确的绝对地址。那么,我们在程序中只使用数据段与代码指令之间的相对地址就可以了,然后从GOT表中得到最终的正确的绝对地址,那么就实现了position independent. 实现细节见 p 488
动态链接器这一段,是我在看了深入理解计算机系统对于PIC自己的一些解读,尤其是物理地址和虚拟地址那一块儿,由于那一章还没有看到,也不知道对不对,但是,我觉得这样可以解释通!!