I. 动态链接中延迟绑定(Lazy binding)的基本思想和方法
基本思想:函数第一次用到时才进行绑定(符号查找,重定位等),如果不用则不进行绑定。
方法:使用PLT(Procedure Linkage Table)的方法来实现。它使用了一些精巧的指令序列。
II. 理解PLT方法涉及的几个对象
1.调用外部函数的代码
代码里调用外部函数时,转向去调用PLT部分的代码。
2.GOT(global offset table)
保存外部函数的地址(当然也包括外部变量),初始加载时,每个函数的地址不是准确的地址,而是这个函数对应PLT项的Push指令的地址。第一次调用函数后填入真正的地址。
3.PLT
指令序列的集合,存放在虚拟内存空间的代码区域。分两大部分:PLT0, PLTn。PLT0的指令作用是压模块标志进栈和调用__dll_runtime_resolve函数。
4.Rel.PLT
代码里调用外部函数时,转向去调用PLT部分的代码。
2.GOT(global offset table)
保存外部函数的地址(当然也包括外部变量),初始加载时,每个函数的地址不是准确的地址,而是这个函数对应PLT项的Push指令的地址。第一次调用函数后填入真正的地址。
3.PLT
指令序列的集合,存放在虚拟内存空间的代码区域。分两大部分:PLT0, PLTn。PLT0的指令作用是压模块标志进栈和调用__dll_runtime_resolve函数。
4.Rel.PLT
PLT的重定位表。保存需要重定位函数的需要的信息,函数符号是函数__dll_runtime_resolve需要的两个参数(哪个动态链接库,哪个外部函数)中的一个。
III. 一个实例
1)
程序dynlib.c:
extern int foo;
extern int f();
static int g(){
return f();
}
int function(void) {
int k = g();
int c = foo;
return (c+k);
}
extern int f();
static int g(){
return f();
}
int function(void) {
int k = g();
int c = foo;
return (c+k);
}
编译成共享动态库:
gcc -fIPC -shared dynlib.so dynlib.c
gcc -fIPC -shared dynlib.so dynlib.c
2)
函数g的代码:
000006d0 <g>:
6d0: 55 push %ebp
6d1: 89 e5 mov %esp,%ebp
6d3: 53 push %ebx
6d4: 83 ec 04 sub $0x4,%esp
6d7: e8 00 00 00 00 call 6dc <g+0xc>
6dc: 5b pop %ebx
6dd: 81 c3 a4 11 00 00 add $0x11a4,%ebx
6e3: e8 6c fe ff ff call 554 <_init+0x28>(运行PLT里f函数对应项的指令去获取f的地址)
6e8: 89 c0 mov %eax,%eax
6ea: 89 c0 mov %eax,%eax
6ec: 8b 5d fc mov 0xfffffffc(%ebp),%ebx
6ef: c9 leave
6f0: c3 ret
6f1: 8d 76 00 lea 0x0(%esi),%esi
6d0: 55 push %ebp
6d1: 89 e5 mov %esp,%ebp
6d3: 53 push %ebx
6d4: 83 ec 04 sub $0x4,%esp
6d7: e8 00 00 00 00 call 6dc <g+0xc>
6dc: 5b pop %ebx
6dd: 81 c3 a4 11 00 00 add $0x11a4,%ebx
6e3: e8 6c fe ff ff call 554 <_init+0x28>(运行PLT里f函数对应项的指令去获取f的地址)
6e8: 89 c0 mov %eax,%eax
6ea: 89 c0 mov %eax,%eax
6ec: 8b 5d fc mov 0xfffffffc(%ebp),%ebx
6ef: c9 leave
6f0: c3 ret
6f1: 8d 76 00 lea 0x0(%esi),%esi
3)
PLT的内容,它是一些指令序列。
00000544 <.plt>:
544: ff b3 04 00 00 00 pushl 0x4(%ebx) (push library ID)
54a: ff a3 08 00 00 00 jmp *0x8(%ebx) (jump to function _dll_runtime_resolve())
550: 00 00 add %al,(%eax)
552: 00 00 add %al,(%eax)
554: ff a3 0c 00 00 00 jmp *0xc(%ebx)
55a: 68 00 00 00 00 push $0x0
55f: e9 e0 ff ff ff jmp 544 <_init+0x18>
564: ff a3 10 00 00 00 jmp *0x10(%ebx)
56a: 68 08 00 00 00 push $0x8
56f: e9 d0 ff ff ff jmp 544 <_init+0x18>
574: ff a3 14 00 00 00 jmp *0x14(%ebx)
57a: 68 10 00 00 00 push $0x10
57f: e9 c0 ff ff ff jmp 544 <_init+0x18>
584: ff a3 18 00 00 00 jmp *0x18(%ebx)
58a: 68 18 00 00 00 push $0x18
58f: e9 b0 ff ff ff jmp 544 <_init+0x18>
544: ff b3 04 00 00 00 pushl 0x4(%ebx) (push library ID)
54a: ff a3 08 00 00 00 jmp *0x8(%ebx) (jump to function _dll_runtime_resolve())
550: 00 00 add %al,(%eax)
552: 00 00 add %al,(%eax)
554: ff a3 0c 00 00 00 jmp *0xc(%ebx)
55a: 68 00 00 00 00 push $0x0
55f: e9 e0 ff ff ff jmp 544 <_init+0x18>
564: ff a3 10 00 00 00 jmp *0x10(%ebx)
56a: 68 08 00 00 00 push $0x8
56f: e9 d0 ff ff ff jmp 544 <_init+0x18>
574: ff a3 14 00 00 00 jmp *0x14(%ebx)
57a: 68 10 00 00 00 push $0x10
57f: e9 c0 ff ff ff jmp 544 <_init+0x18>
584: ff a3 18 00 00 00 jmp *0x18(%ebx)
58a: 68 18 00 00 00 push $0x18
58f: e9 b0 ff ff ff jmp 544 <_init+0x18>
用(*0xc(%ebx) )获取GOT表里函数f对应保存的值,应该是f的地址值,但使用延迟绑定技术后,初始值时这个函数对应PLT项的push指令值,在本例中就是55a。第一次运行函数时,其实jump *0xc(%ebx),就跳转到55a,这条指令的效果就是跳转到下一条指令,相当于什么也没做。
接下来55f的指令就跳转到PL0的指令地址去,544指令的目的是压模块ID入栈,就是这个动态库是哪个动态库。54a指令去调用函数_dll_runtime_resolve去解析函数f的地址,填入GOT中f对应的项中,并且执行f。
第二次执行函数f时,还会跳转到指令554去,但这时 *0xc(%ebx)的值就是真正函数f的地址值了,这时554的效果就是转到函数f的地址去执行f了。
接下来55f的指令就跳转到PL0的指令地址去,544指令的目的是压模块ID入栈,就是这个动态库是哪个动态库。54a指令去调用函数_dll_runtime_resolve去解析函数f的地址,填入GOT中f对应的项中,并且执行f。
第二次执行函数f时,还会跳转到指令554去,但这时 *0xc(%ebx)的值就是真正函数f的地址值了,这时554的效果就是转到函数f的地址去执行f了。
查看几个指令“55a,56a,57a,58a”,push指令后的整数是“0x0,0x8,0x10,0x18”,为什么每两个数之间差8?
push之后的整数标准这个外部函数在.rel.plt中偏移量。.rel.plt是一个结构数组。结构是:
typedef struct{
Elf32_Addr r_offset;
Elf32_Word r_info;
}Elf32_Rel;
Elf32_Addr r_offset;
Elf32_Word r_info;
}Elf32_Rel;
sizeof(Elf32_Rel)=8。第一个结构偏移是0,第二个地址偏移就是0x8,第三个就是0x10,所以每两个整数之间差8。
4)
GOT的内容。
Hex dump of section '.got':
0x00001880 0000055a(加载时,初始值是PLT中函数f对应项的push指令地址,本例中就是55a;第一次运行函数f后就写入函数f的地址)
0x00001880 0000055a(加载时,初始值是PLT中函数f对应项的push指令地址,本例中就是55a;第一次运行函数f后就写入函数f的地址)
00000000(_dll_runtime_resolve地址)
00000000(加载时写入库的名字)
000017b0 ............Z...
0x00001890 00000000 0000058a 0000057a 0000056a j...z...........
0x000018a0 00000000 00000000 00000000 00000000 ................
0x000018a0 00000000 00000000 00000000 00000000 ................
5)
Rel.plt内容
Relocation section '.rel.plt' at offset 0x50c contains 4 entries:
Offset Info Type Symbol's Value Symbol's Name
0000188c 00001607 R_386_JUMP_SLOT 00000000 f
00001890 00001707 R_386_JUMP_SLOT 00000000 __register_frame_info
00001894 00001907 R_386_JUMP_SLOT 00000000 __deregister_frame_info
00001898 00001d07 R_386_JUMP_SLOT 00000000 __cxa_finalize
Offset Info Type Symbol's Value Symbol's Name
0000188c 00001607 R_386_JUMP_SLOT 00000000 f
00001890 00001707 R_386_JUMP_SLOT 00000000 __register_frame_info
00001894 00001907 R_386_JUMP_SLOT 00000000 __deregister_frame_info
00001898 00001d07 R_386_JUMP_SLOT 00000000 __cxa_finalize
保存了Plt需要重定位的函数信息。PLT中push指令后的整数就是外部函数在.rel.plt中对应项的偏移量。
IV参考资料
《程序员的自我修养--链接、装载与库》作者:俞甲子,石凡,潘爱民
《漫谈兼容内核》系列,作者:毛德操