Linux Debugging(七): 使用反汇编理解动态库函数调用方式GOT/PLT
本文主要讲解动态库函数的地址是如何在运行时被定位的。首先介绍一下PIC和Relocatable的动态库的区别。然后讲解一下GOT和PLT的理论知识。GOT是Global Offset Table,是保存库函数地址的区域。程序运行时,库函数的地址会设置到GOT中。由于动态库的函数是在使用时才被加载,因此刚开始GOT表是空的。地址的设置就涉及到了PLT,Procedure Linkage Table,它包含了一些代码以调用库函数,它可以被理解成一系列的小函数,这些小函数的数量其实就是库函数的被使用到的函数的数量。简单来说,PLT就是跳转到GOT中所设置的地址而已。如果这个地址是空,那么PLT的跳转会巧妙的调用_dl_runtime_resolve去获取最终地址并设置到GOT中去。由于库函数的地址在运行时不会变,因此GOT一旦设置以后PLT就可以直接跳转到库函数的真实地址了。最后使用反汇编验证和跳转流程图对上述结论加深理解。
1. 背景-PIC VS Relocatable
在 Linux 下制作动态链接库,“标准” 的做法是编译成位置无关代码(Position Independent Code,PIC),然后链接成一个动态链接库。那么什么是PIC呢?如果是非PIC的,那么会有什么问题?
(1) 可重定位代码(relocatable code):Windows DLL 以及不使用 -fPIC 的 Linux so。
生成动态库时假定它被加载在地址 0 处。加载时它会被加载到一个地址(base),这时要进行一次重定位(relocation),把代码、数据段中所有的地址加上这个 base 的值。这样代码运行时就能使用正确的地址了。当要再加载时根据加载到的位置再次重定位的。(因为它里面的代码并不是位置无关代码)。因为so被每个程序加载的位置都不同,显然这些重定位后的代码也不同,当然不能共享。如果被多个应用程序共同使用,那么它们必须每个程序维护一份so的代码副本了。当然,主流现代操作系统都启用了分页内存机制,这使得重定位时可以使用 COW(copy on write)来节省内存(32 位 Windows 就是这样做的);然而,页面的粒度还是比较大的(例如 IA32 上是 4KiB),至少对于代码段来说能节省的相当有限。不能共享就失去了共享库的好处,实际上和静态库的区别并不大,在运行时占用的内存是类似的,仅仅是二进制代码占的硬盘空间小一些。(2) 位置无关代码(position independent code):使用 -fPIC 的 Linux so。
这样的代码本身就能被放到线性地址空间的任意位置,无需修改就能正确执行。通常的方法是获取指令指针(如 x86 的 EIP 寄存器)的值,加上一个偏移得到全局变量/函数的地址。AMD64 下,必须使用位置无关代码。x86下,在创建so时会有一个警告。但是这样的so可以完全正常工作。PIC 的缺点主要就是代码有可能长一些。例如 x86,由于不能直接使用 [EIP+constant] 这样的寻址方式,甚至不能直接将 EIP 的值交给其他寄存器,要用到 GOT(global
offset table)来定位全局变量和函数。这样导致代码的效率略低。PIC 的加载速度稍快,因为不需要做重定位。多个进程引用同一个 PIC 动态库时,可以共用内存。这一个库在不同进程中的虚拟地址不同,但操作系统显然会把它们映射到同一块物理内存上。
2. GOT和PLT
我们都知道动态库是在运行时绑定的。那么编译器是如何找到动态链接库里面的函数的地址呢?事实上,直到我们第一次调用这个函数,我们并不知道这个函数的地址,这个功能要做延迟绑定 lazy bind。 因为程序的分支很多,并不是所有的分支都能跑到,想想我们的异常处理,异常处理分支的动态链接库里面的函数也许永远跑不到,所以,启动时解析所有出现过的动态库里面的函数是个浪费的办法,降低性能并且没有必要。
Global Offset Table(GOT)
在位置无关代码中,一般不能包含绝对虚拟地址(如共享库)。当在程序中引用某个共享库中的符号时,编译链接阶段并不知道这个符号的具体位置,只有等到动态链接器将所需要的共享库加载时进内存后,也就是在运行阶段,符号的地址才会最终确定。因此,需要有一个数据结构来保存符号的绝对地址,这就是GOT表的作用,GOT表中每项保存程序中引用其它符号的绝对地址。这样,程序就可以通过引用GOT表来获得某个符号的地址。
在x86结构中,GOT表的前三项保留,用于保存特殊的数据结构地址,其它的各项保存符号的绝对地址。对于符号的动态解析过程,我们只需要了解的就是第二项和第三项,即GOT[1]和GOT[2]:GOT[1]保存的是一个地址,指向已经加载的共享库的链表地址;GOT[2]保存的是一个函数的地址,定义如下:GOT[2] = &_dl_runtime_resolve,这个函数的主要作用就是找到某个符号的地址,并把它写到与此符号相关的GOT项中,然后将控制转移到目标函数,后面我们会详细分析。GOT示意如下图,GOT表slot的数量就是3
+ number of functions to be loaded.
Procedure Linkage Table(PLT)
过程链接表(PLT)的作用就是将位置无关的函数调用转移到绝对地址。在编译链接时,链接器并不能控制执行从一个可执行文件或者共享文件中转移到另一个中(如前所说,这时候函数的地址还不能确定),因此,链接器将控制转移到PLT中的某一项。而PLT通过引用GOT表中的函数的绝对地址,来把控制转移到实际的函数。
在实际的可执行程序或者共享目标文件中,GOT表在名称为.got.plt的section中,PLT表在名称为.plt的section中。
3. 反汇编
我们使用的代码是:
#include <iostream> #include <stdlib.h> void fun(int a) { a++; } int main() { fun(1); int x = rand(); return 0; }
动态库里面需要重定位的函数在.got.plt这个段里面,通过readelf我们可以看到,它一共有六个地址空间,前三个我们已经解释了。说明该程序预留了三个所需要重新定位的函数。因此用不到的函数是永远不会被加载的。
[23] .dynamic DYNAMIC 0000000000600e10 00000e10 00000000000001d0 0000000000000010 WA 8 0 8 [24] .got PROGBITS 0000000000600fe0 00000fe0 0000000000000008 0000000000000008 WA 0 0 8 [25] .got.plt PROGBITS 0000000000600fe8 00000fe8 0000000000000048 0000000000000008 WA 0 0 8
反汇编main函数:
(gdb) disas main Dump of assembler code for function main: 0x0000000000400549 <main+0>: push %rbp 0x000000000040054a <main+1>: mov %rsp,%rbp 0x000000000040054d <main+4>: sub $0x10,%rsp 0x0000000000400551 <main+8>: mov $0x1,%edi 0x0000000000400556 <main+13>: callq 0x40053c <fun> 0x000000000040055b <main+18>: callq 0x400440 <rand@plt> 0x0000000000400560 <main+23>: mov %eax,-0x4(%rbp) 0x0000000000400563 <main+26>: mov $0x0,%eax 0x0000000000400568 <main+31>: leaveq 0x0000000000400569 <main+32>: retq End of assembler dump.可以看到其实调用我们自定义的fun和系统库函数rand形成的汇编差不多,没有额外的处理。接着向下看rand:
(gdb) disas 0x400440 Dump of assembler code for function rand@plt: 0x0000000000400440 <rand@plt+0>: jmpq *0x200bc2(%rip) # 0x601008 <_GLOBAL_OFFSET_TABLE_+32> 0x0000000000400446 <rand@plt+6>: pushq $0x1 0x000000000040044b <rand@plt+11>: jmpq 0x400420 End of assembler dump.真正有意思的在# 0x601008 <_GLOBAL_OFFSET_TABLE_+32>。也就是rand@plt首先会跳到这里。我们看一下这里是什么:
(gdb) x 0x601008 0x601008 <_GLOBAL_OFFSET_TABLE_+32>: 0x00400446接着看0x00400446是什么:
(gdb) x/5i 0x00400446 0x400446 <rand@plt+6>: pushq $0x1 0x40044b <rand@plt+11>: jmpq 0x400420可能你注意到了,这里的处理是和刚才的rand@plt的jmpq一样。都是将0x1入栈,然后jmpq 0x400420。因此这样就避免了GOT表是否为是真实值的检查:如果是空,那么去寻址;否则直接调用。
其实接下来处理的就是调用_dl_runtime_resolve_()函数,该函数最终会寻址到rand的真正地址并且会调用_dl_fixup来将rand的实际地址填入GOT表中。
我们将整个程序执行完,然后看一下0x601008 <_GLOBAL_OFFSET_TABLE_+32>是否已经修改成rand的实际地址:
(gdb) x 0x601008 0x601008 <_GLOBAL_OFFSET_TABLE_+32>: 0xf7ab6470
可以看到,rand的地址已经修改为0xf7ab6470了。然后可以通过maps确认一下是否libc load在这个地址:
(gdb) shell cat /proc/`pgrep a.out`/maps 00400000-00401000 r-xp 00000000 08:02 491638 /root/study/got/a.out 00600000-00601000 r--p 00000000 08:02 491638 /root/study/got/a.out 00601000-00602000 rw-p 00001000 08:02 491638 /root/study/got/a.out 7ffff7a80000-7ffff7bd5000 r-xp 00000000 08:02 327685 /lib64/libc-2.11.1.so 7ffff7bd5000-7ffff7dd4000 ---p 00155000 08:02 327685 /lib64/libc-2.11.1.so 7ffff7dd4000-7ffff7dd8000 r--p 00154000 08:02 327685 /lib64/libc-2.11.1.so 7ffff7dd8000-7ffff7dd9000 rw-p 00158000 08:02 327685 /lib64/libc-2.11.1.so 7ffff7dd9000-7ffff7dde000 rw-p 00000000 00:00 0 7ffff7dde000-7ffff7dfd000 r-xp 00000000 08:02 327698 /lib64/ld-2.11.1.so 7ffff7fc4000-7ffff7fc7000 rw-p 00000000 00:00 0 7ffff7ffa000-7ffff7ffb000 rw-p 00000000 00:00 0 7ffff7ffb000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso] 7ffff7ffc000-7ffff7ffd000 r--p 0001e000 08:02 327698 /lib64/ld-2.11.1.so 7ffff7ffd000-7ffff7ffe000 rw-p 0001f000 08:02 327698 /lib64/ld-2.11.1.so 7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0 7ffffffea000-7ffffffff000 rw-p 00000000 00:00 0 [stack] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
没有问题,如我们所分析的那样:
7ffff7a80000-7ffff7bd5000 r-xp 00000000 08:02 327685 /lib64/libc-2.11.1.so以后的调用就直接调用库函数了:
尊重原创,转载请注明出处 anzhsoft: http://blog.csdn.net/anzhsoft/article/details/18776111
参考资料:
1. http://www.linuxidc.com/Linux/2011-06/37268.htm
2. http://blog.chinaunix.net/uid-24774106-id-3349549.html
3. http://www.linuxidc.com/Linux/2011-06/37268.htm
4. http://eli.thegreenplace.net/2011/11/03/position-independent-code-pic-in-shared-libraries/