本文引用并修改了http://jzhihui.iteye.com/blog/1447570 中的动态链接部分,然后加了自己的理解和注释。
用来测试的C代码为:
/* hello.c */
#include <stdio.h>
int main()
{
printf(“hello world!\n”);
return 0;
}
$ gcc –o hello hello.c
当执行一个用户程序的时候,控制权是先交到解释器,由解释器加载动态库,然后控制权才会到用户程序。而动态库的加载过程,大致的过程就是将每一个依赖的动态库都加载到内存,并形成一个链表,后面的符号解析过程主要就是在这个链表中搜索符号的定义。
我们主要就是以Hello World为例,分析程序是如何调用printf的:
查看一下gcc编译生成的Hello World程序的汇编代码(main函数部分):
08048374 <main>:
8048374: 8d 4c 24 04 lea 0x4(%esp),%ecx
……
8048385: c7 04 24 6c 84 04 08 movl $0x804846c,(%esp)
804838c: e8 2b ff ff ff call 80482bc <puts@plt>
8048391: b8 00 00 00 00 mov $0x0,%eax
从上面的代码可以看出,经过编译后,printf函数的调用已经换成了puts函数。其中的call指令就是调用puts函数。但从上面的代码可以看出,它调用的是puts@plt这个标号,它代表什么意思呢?在进一步说明符号的动态解析过程以前,需要先了解两个概念,一个是global offset table,一个是procedure linkage table。
Global Offset Table(GOT)
在位置无关代码中,一般不能包含绝对虚拟地址(如共享库)。当在程序中引用某个共享库中的符号时,编译链接阶段并不知道这个符号的具体位置,只有等到动态链接器将所需要的共享库加载时进内存后,也就是在运行阶段,符号的地址才会最终确定。因此,需要有一个数据结构来保存符号的绝对地址,这就是GOT表的作用,GOT表中每项保存程序中引用其它符号的绝对地址。这样,程序就可以通过引用GOT表来获得某个符号的地址。
在x86结构中,GOT表的前三项保留,用于保存特殊的数据结构地址,其它的各项保存符号的绝对地址。对于符号的动态解析过程,我们只需要了解的就是第二项和第三项,即GOT[1]和GOT[2]:GOT[1]保存的是一个地址,指向已经加载的共享库的链表地址(前面提到加载的共享库会形成一个链表);GOT[2]保存的是一个函数的地址,定义如下:GOT[2] = &_dl_runtime_resolve,这个函数的主要作用就是找到某个符号的地址,并把它写到与此符号相关的GOT项中,然后将控制转移到目标函数,后面我们会详细分析。
Procedure Linkage Table(PLT)
过程链接表(PLT)的作用就是将位置无关的函数调用转移到绝对地址。在编译链接时,链接器并不能控制执行从一个可执行文件或者共享文件中转移到另一个中(如前所说,这时候函数的地址还不能确定),因此,链接器将控制转移到PLT中的某一项。而PLT通过引用GOT表中的函数的绝对地址,来把控制转移到实际的函数。
在实际的可执行程序或者共享目标文件中,GOT表在名称为.got.plt的section中,PLT表在名称为.plt的section中。
大致的了解了GOT和PLT的内容后,我们查看一下puts@plt中到底是什么内容:
Disassembly of section .plt:
0804828c <__gmon_start__@plt-0x10>:
804828c: ff 35 68 95 04 08 pushl 0x8049568
8048292: ff 25 6c 95 04 08 jmp *0x804956c
8048298: 00 00
......
0804829c <__gmon_start__@plt>:
804829c: ff 25 70 95 04 08 jmp *0x8049570
80482a2: 68 00 00 00 00 push $0x0
80482a7: e9 e0 ff ff ff jmp 804828c <_init+0x18>
080482ac <__libc_start_main@plt>:
80482ac: ff 25 74 95 04 08 jmp *0x8049574
80482b2: 68 08 00 00 00 push $0x8
80482b7: e9 d0 ff ff ff jmp 804828c <_init+0x18>
080482bc <puts@plt>:
80482bc: ff 25 78 95 04 08 jmp *0x8049578
80482c2: 68 10 00 00 00 push $0x10
80482c7: e9 c0 ff ff ff jmp 804828c <_init+0x18>
可以看到puts@plt包含三条指令,程序中所有对有puts函数的调用都要先来到这里(Hello World里只有一次)。可以看出,除PLT0以外(就是gmon_start@plt-0x10所标记的内容),即
80482c2: 68 10 00 00 00 push $0x10
其它的所有PLT项的形式都是一样的,而且最后的jmp指令都是0x804828c,即PLT0为目标的。所不同的只是第一条jmp指令的目标和push指令中的数据。PLT0则与之不同,但是包括PLT0在内的每个表项都占16个字节,所以整个PLT就像个数组(实际是代码段)。另外,每个PLT表项中的第一条jmp指令是间接寻址的。比如我们的puts函数是以地址0x8049578处的内容为目标地址进行中跳转的。
顺着这个地址,我们进一步查看此处的内容:
(gdb) x/w 0x8049578
0x8049578 <_GLOBAL_OFFSET_TABLE_+20>: 0x080482c2
从上面可以看出,这个地址就是GOT表中的一项。它里面的内容是0x80482c2,即puts@plt中的第二条指令。前面我们不是提到过,GOT中这里本应该是puts函数的地址才对,那为什么会这样呢?原来链接器在把所需要的共享库加载进内存后,并没有把共享库中的函数的地址写到GOT表项中,而是延迟到函数的第一次调用时,才会对函数的地址进行定位(注:若已经定位,则直接就是函数的绝对地址了。没有定位的话,就直接跳转回原来指令的下一条,继续执行定位操作)。
puts@plt的第二条指令是pushl $0x10,那这个0x10代表什么呢?
Relocation section '.rel.plt' at offset 0x25c contains 3 entries:
Offset Info Type Sym.Value Sym. Name
08049570 00000107 R_386_JUMP_SLOT 00000000 __gmon_start__
08049574 00000207 R_386_JUMP_SLOT 00000000 __libc_start_main
08049578 00000307 R_386_JUMP_SLOT 00000000 puts
其中的第三项就是puts函数的重定向信息,0x10即代表相对于.rel.plt这个section的偏移位置(每一项占8个字节)。其中的Offset这个域就代表的是puts函数地址在GOT表项中的位置,从上面puts@plt的第一条指令也可以验证这一点。向堆栈中压入这个偏移量的主要作用就是为了找到puts函数的符号名(即上面的Sym.Name域的“puts”这个字符串)以及puts函数地址在GOT表项中所占的位置,以便在函数定位完成后将函数的实际地址写到这个位置。
puts@plt的第三条指令就跳到了PLT0的位置。这条指令只是将0x8049568这个数值压入堆栈,它实际上是GOT表项的第二个元素,即GOT[1](共享库链表的地址)。
随即PLT0的第二条指令即跳到了GOT[2]中所保存的地址(间接寻址),即_dl_runtime_resolve这个函数的入口。
_dl_runtime_resolve的定义如下:
_dl_runtime_resolve:
pushl %eax # Preserve registers otherwise clobbered.
pushl %ecx
pushl %edx /*保存寄存器的值*/
movl 16(%esp), %edx # Copy args pushed by PLT in register. Note
movl 12(%esp), %eax # that `fixup' takes its parameters in regs. /*把先前压入堆栈的puts函数地址在GOT表项中的偏移量以及共享库链表的地址作为参数*/
call _dl_fixup # Call resolver.
popl %edx # Get register content back.
popl %ecx /*恢复寄存器的值*/
xchgl %eax, (%esp) # Get %eax contents end store function address. /*把返回值压入堆栈(返回值通过寄存器eax传递,返回值为puts函数的绝对地址),并恢复寄存器eax的原来值*/
ret $8 # Jump to function address.
/*跳转到puts处执行,并废除原来压入堆栈的_dl_fixup函数的两个参数()。*/