Linux下动态链接实现原理

讲动态链接之前,得先说说符号重定位。

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

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/ff8ee0804422736d6a822e7015c50de4.html