讲动态链接之前,得先说说符号重定位。
C/C++ 程序的编译是以文件为单位进行的,因此每个 c/cpp 文件也叫作一个编译单元(translation unit), 源文件先是被编译成一个个目标文件, 再由链接器把这些目标文件组合成一个可执行文件或库,链接的过程,其核心工作是解决模块间各种符号(变量,函数)相互引用的问题,对符号的引用本质是对其在内存中具体地址的引用,因此确定符号地址是编译,链接,加载过程中一项不可缺少的工作,这就是所谓的符号重定位。
因为编译是以源文件为单位进行的,编译器此时并没有一个全局的视野,因此对一个编译单元内的符号它是无力确定其最终地址的,而对于可执行文件来说,在现代操作系统上,程序加载运行的地址是固定或可以预期的,因此在链接时,链接器可以直接计算分配该文件内各种段的绝对或相对地址,所以对于可执行文件来说,符号重定位是在链接时完成的,但对动态链接库来说,因为动态库的加载是在运行时,且加载的地址不固定,因此没法事先确定该模块的起始地址,所以对动态库的符号重定位,只能推迟。
符号重定位既指在当前目标文件内进行重定位,也包括在不同目标文件,甚至不同模块间进行重定位,这里面有什么不同吗?如果是同一个目标文件内,或者在同一个模块内,链接后,各个符号的相对地址就已经确定了,看起来似乎不用非得要知道最后的绝对地址才能引用这些符号,这说起来好像也有道理,但事实不是这样,x86 上 mov 之类访问程序中数据段的指令,它要求操作数是绝对地址,而对于函数调用,虽然是以相对地址进行调用,但计算相对地址也只限于在当前目标文件内进行,跨目标文件跨模块间的调用,编译期也是做不到的,只能等链接时或加载时才能进行相对地址的计算,因此重定位这个过程是不能缺少的,事实上目前来说,对于动态链接即使是当前目标文件内,如果是全局非静态函数,那么它也是需要进行重定位的,当然这里面有别的原因,比如说使得能实现 LD_PRELOAD 的功能等。
链接时符号重定位链接时符号重定位指的是在链接阶段对符号进行重定位,一般来说,构建一个可执行文件可以简单分为两个步骤:编译及链接,如下例子,我们尝试使用静态链接的方式构建一个可执行文件:
// file: a.c int g_share = 1; int g_func(int a) { g_share += a; return a * 3; } // file: main.c extern int g_share; extern int g_func(int a); int main() { int a = 42; a = g_func(a); return 0; }正如前面所说,此时符号的重定位在链接时进行,那么在编译时,编译器是怎么生成代码来引用那些还没有重定位的符号呢?让我们先编译一下,再来看看目标文件的内容:
// x86_64, linux 2.6.9 -bash-3.00$ gcc -c a.c main.c -g -bash-3.00$ objdump -S a.o然后得到如下输出(对于 main.o 中对 g_func 的引用,实现是一样的,故略):
a.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <g_func>: int g_share = 1; int g_func(int a) { 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 89 7d fc mov %edi,0xfffffffffffffffc(%rbp) g_share += a; 7: 8b 45 fc mov 0xfffffffffffffffc(%rbp),%eax a: 01 05 00 00 00 00 add %eax,0(%rip) # 10 <g_func+0x10> return a * 2; 10: 8b 45 fc mov 0xfffffffffffffffc(%rbp),%eax 13: 01 c0 add %eax,%eax } 15: c9 leaveq 16: c3 retq从中可以看到,目标文件里的 .txt 段地址从 0 开始,其中地址为7的指令用于把参数 a 放到寄存器 %eax 中,而地址 a 处的指令则把 %eax 中的内容与 g_share 相加,注意这里 g_share 的地址为:0(%rip). 显然这个地址是错的,编译器当前并不知道 g_share 这个变量最后会被分配到哪个地址上,因此在这儿只是随便用一个假的来代替,等着到接下来链接时,再把该处地址进行修正。那么,链接器怎么知道目标文件中哪些地方需要修正呢?很简单,编译器编译文件时时,会建立一系列表项,用来记录哪些地方需要在重定位时进行修正,这些表项叫作“重定位表”(relocatioin table):
-bash-3.00$ objdump -r a.o a.o: file format elf64-x86-64 RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 000000000000000c R_X86_64_PC32 g_share+0xfffffffffffffffc