符号解析
链接器解析多处重定义的全局符号
使用强弱符号
假设在main.c和 func.c 中都定义了 int x; 定义并初始化的x被视为强符号,如果出现多个强符号x,链接器会提示错误 如果只有单个强符号x,链接器会选择强符号x 如果有多个弱符号,链接器随机选择一个弱符号x
与静态库链接
生成静态库
编译系统将应用程序中用到的所有相关目标模块打包成为一个单独文件(静态库),作为链接器的输入。当链接器构造输出可执行文件时,拷贝静态库中被应用程序引用的目标模块。静态库文件形如:xxx.a
xxx.a中可以含有多个xxx.o, 在链接器生成可执行文件的时候会查找xxx.a并对比xxx.o,将需要的xxx.o链接到可执行文件中。后缀a代表archive(存档)
若不使用静态库:
-
将目标模块的识别和替换工作集成在编译器中:
只适用于标准函数较少的情况,即pascal语言,这样做方便程序员写应用程序,但是每次更改标准函数,就需要更换新的编译器。
-
将目标模块都放在一个可重定位的目标文件中:
某些C标准库以xxx.o的形式存在,这是一个可重定位的目标文件,在生成可执行文件的时候,由链接器将其链接到可执行文件中,这样做的好处是实现了分离编译器和标准函数,同时给予应用程序员便利,但是这样做相当于每次都要链接整个巨大的标准库到应用程序中,占用极大的空间。且当标准库更新,整个源文件需要被重新编译,浪费时间。当然也可以将各个目标模块分别生成一个可重定位的目标文件xxx.o,在生成可执行文件的时候只链接需要的目标文件xxx.o即可,但这样做对程序员的开发产生很大不便,且一样在标准库更新的时候需要重新编译。
静态库的解析
静态库xxx.a在解析的时候依赖于链接器的规则,首先需要明确链接器在链接可重定位目标文件到可执行文件的时候使用了三个集合:
- E集合: 该集合用于存放可执行目标文件用到的所有可重定位目标文件
- D集合: 存放定义明确的目标模块(符号)
- U集合: 存放尚未找到定义的符号
初始时三个集合都为空,直觉告诉我们,在链接结束的时候如果U集合不为空说明存在未定义却被引用的模块,链接器返回错误。且U集合中的符号最终都会进入D集合。D集合中的符号都可以在E集合的可重定位目标文件中找到。
很重要的一点是,连接器在解析静态库的时候遵循顺序查找,即例如:
gcc -static ./liba.a ./main.c
这样的编译指令,对于链接器来说,首先搜索liba.a,由于liba.a首先进入链接器查找流程,这时集合U还是空的,所以没有目标文件加入E中,在连接结束时,在main.o中找到的所有U集合中的符号都没有对应可重定位目标文件,所以链接器会报错。正确指令应为:
gcc -static ./main.c ./liba.a
可以想到,对于不同的xxx.a存档,
重定位
- 重定位节和符号定义: 不同目标模块的相同符号类型被合并形成新的节和符号定义。
- 重定位节中的符号引用: 符号引用就是符号对应的运行时内存,链接器依赖于重定位表目来更新符号引用。
重定位表目
重定位表目的存在指示了链接器如何将符号引用重定位到其实际位置,常见的两种类型为PC相关地址引用和绝对地址引用
可执行目标文件
段头表
段头表指示了代码段和数据段的基本信息(存储位置,起始地址等等)
加载可执行目标文件
一般来讲以./xxx执行的目标文件,是由unix shell新建的进程,当然其所属堆栈等独立,存储器映像表示为:
加载可执行文件这一步根据elf可执行文件段头表的信息,将.text代码段和.data数据段加载到进程分配的新数据段和代码段中,其边界等信息均由段头表定义。
动态链接共享库
共享库是一个目标模块,在运行时,可以加载到任意的存储器地址,并在存储器中和一个程序链接起来。这个过程称为动态链接,时有一个叫做动态链接器的程序来执行的。共享库也称为共享目标,在Unix系统中通常用.so后缀来表示。微软的操作系统大量使用共享库,称为DLL,这有利于window的更新和升级。
共享概念: 1.在任何给定的文件系统中,对于一个库只有一个.so文件,所有引用该库的可执行目标文件共享这个.so文件中的代码和数据。 2. 存储器中,一个共享库的.text节只有一个副本可以被不同的正在运行的进程共享。
上图中最后一步是动态链接器处理:
动态链接器执行重定位完成链接: 1. 重定位libc.so,libvector.so的文本和数据到不同的存储器段。在x86_32linux系统中,共享库都被加载到从0x40000000的区域中。
2.重定位可执行目标文件中所有对libc.so和libvector.so定义的符号的引用
此后共享库位置就固定了,在程序执行过程中不变。
#从应用程序中加载和链接共享库
Unix系统为动态链接器提供了简单的api接口可以用于在程序运行时加载和链接共享库。
直接看代码:
dlopen以RTLD_LAZY模式获得libvector.so加载后的文件句柄,使用dlsym函数利用libvector.so的文件句柄查找初始化addvec函数句柄,dlclose在使用完毕后可以卸载共享库
位置无关代码(position-independent code, PIC)
位置无关代码是很重要的概念,之前的PC相关寻址就是位置无关的一种,共享库可能在任何位置加载,其内部符号地址也会出现在任何可能位置,如果要重定位不同位置的符号,就需要寻找到相对位置关系而不是绝对地址。
对于IA32系统,同一个目标模块中的过程调用可以使用PC相关引用,根据重定位表目在call指令下一个地址加一个偏移量即可找到自己.text中对应调用过程位置。但对外部模块定义的过程调用和对全局变量的引用通常不是PIC,因为他们都是在链接时加载到共享库区域任意位置。
PIC数据引用
基于一个事实: 数据段总是处于代码段后面。所以代码段中任何指令和数据段中任何变量之间的距离是一个运行时常量。所以编译器在数据段开始的地方创建一个表,为全局偏移量表(GOT)。 GOT包含每个目标模块中引用的全局变量,为它们生成各自的表目和对应的重定位记录,在加载的时候动态链接器会重定位GOT中每一个表目也就是每一个全局变量,更新他们的新的正确的绝对地址,目标模块对全局变量的引用可以通过GOT表来查找而不是每次加载都重定位自己数据段中的全局变量,通过修改重定位GOT避免了对数据段的修改,做到了代码的位置无关。
PIC函数调用
PIC数据引用的过程需要通过GOT,这就对数据访问增加了运行开销,函数调用也可以通过GOT但是ELF编译系统通过延迟绑定技术,利用一个新的数据结构过程链接表(PLT) 和GOT的交互做到第一次调用函数时做一个初始化操作,后续再次调用时可以做到快速访问,具体细节略。