动态链接比静态链接灵活,但牺牲了性能,据统计ELF程序在静态链接下比动态库快大约1%~5%。
主要原因是,动态链接下对于全局和静态数据的访问都要进行复杂的GOT定位,然后间接寻址,对于模块间的调用也要先定位GOT,然后进行间接跳转。
另外,动态链接的链接过程是在运行时完成的,动态链接器会寻找并转载所需要的对象,然后进行符号查找地址重定位等工作。
延迟绑定的实现步骤如下:
建立一个 GOT.PLT 表,该表用来放全局函数的实际地址,但最开始时,该里面放的不是真实的地址而是一个跳转,接下来会讲。
对每一个全局函数,链接器生成一个与之相对应的影子函数,如 fun@plt。
所有对 fun 的调用,都换成对 fun@plt 的调用,每个fun@plt 长成如下样子:
fun@plt:
jmp *(fun@got.plt)
push index
jmp _init
其中第一条指令直接从 got.plt 中去拿真实的函数地址,如果已经之前已经发生过调用,got.plt 就已经保存了真实的地址,如果是第一次调用,则 got.plt 中放的是 fun@plt 中的第二条指令,这就使得当执行第一次调用时,fun@plt中的第一条指令其实什么事也没做,直接继续往下执行,第二条指令的作用是把当前要调用的函数在 got.plt 中的编号作为参数传给 _init(),而 _init() 这个函数则用于把 fun 进行重定位,然后把结果写入到 got.plt 相应的地方,最后直接跳过去该函数。
仍然是使用前面的例子,我们看看 g_func2 是怎样调用 g_func 的:
0000052f <g_func2>:
52f: 55 push %ebp
530: 89 e5 mov %esp,%ebp
532: 53 push %ebx
533: 83 ec 14 sub $0x14,%esp
536: e8 00 00 00 00 call 53b <g_func2+0xc>
53b: 5b pop %ebx
53c: 81 c3 91 11 00 00 add $0x1191,%ebx
542: c7 45 f8 02 00 00 00 movl $0x2,0xfffffff8(%ebp) // a = 2
549: 83 ec 0c sub $0xc,%esp
54c: 6a 03 push $0x3 // push argument 3 for g_func.
54e: e8 d5 fe ff ff call 428 <g_func@plt>
553: 83 c4 10 add $0x10,%esp
556: 89 45 f4 mov %eax,0xfffffff4(%ebp)
559: 8b 45 f4 mov 0xfffffff4(%ebp),%eax
55c: 03 45 f8 add 0xfffffff8(%ebp),%eax
55f: 8b 5d fc mov 0xfffffffc(%ebp),%ebx
562: c9 leave
563: c3 ret
如上汇编,指令 536, 53b, 53c, 用于计算 got.plt 的具体位置,计算方式与前面对数据的访问原理是一样的,经计算此时, %ebx = 0x53b + 0x1191 = 0x16cc, 注意指令 54e, 该指令调用了函数 g_func@plt:
00000428 <g_func@plt>:
428: ff a3 0c 00 00 00 jmp *0xc(%ebx)
42e: 68 00 00 00 00 push $0x0
433: e9 e0 ff ff ff jmp 418 <_init+0x18>
注意到此时, %ebx 中放的是 got.plt 的地址,g_func@plt 的第一条指令用于获取 got.plt 中 func 的具体地址, func 放在 0xc + %ebx = 0xc + 0x16cc = 0x16d8, 这个地址里放的是什么呢?我们查一下重定位表:
-bash-3.00$ objdump -R liba.so
liba.so: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
000016e0 R_386_RELATIVE *ABS*
000016e4 R_386_RELATIVE *ABS*
000016bc R_386_GLOB_DAT g_share
000016c0 R_386_GLOB_DAT __cxa_finalize
000016c4 R_386_GLOB_DAT _Jv_RegisterClasses
000016c8 R_386_GLOB_DAT __gmon_start__
000016d8 R_386_JUMP_SLOT g_func
000016dc R_386_JUMP_SLOT __cxa_finalize
可见,该地址里放的就是 g_func 的具体地址,那此时 0x16d8 放的是真正的地址了吗?我们再看看 got.plt:
Contents of section .got.plt:
16cc fc150000 00000000 00000000 2e040000 ................
16dc 3e040000
16d8 处的内容是: 2e040000, 小端序,换回整形就是 0x000042e, 该地址就是 fun@plt 的第二条指令!