欢迎访问我的个人博客: luomuxiaoxiao.com


 上篇文章我们提到,为了保证代码复用和节省计算机资源,在链接时,动态链接库的代码段和数据段等是不会被复制到最终生成的可执行文件中的,这些部分会在程序加载的时候复制到内存,并做动态链接,使原来可执行文件能够对其中定义的符号正常引用。也就是说在这个时候,可执行文件代码段中对动态链接库包含的符号引用的地址才真正确定下来。但是我们查看各个segment的属性可以知道,.text segment是只读的,也就是说在编译成可以执行文件之后,就不能被修改了,那么如何确保它能够正确的引用在加载时才能确定下来的动态链接库里的符号呢?这就需要我们这篇文章里的GOT和PLT作为跳板来实现了。

一、什么是PLT和GOT

 GOT全称Global Offset Table,即全局偏移量表。它在可执行文件中是一个单独的section,位于.data section的前面。每个被目标模块引用的全局符号(函数或者变量)都对应于GOT中一个8字节的条目。编译器还为GOT中每个条目生成一个重定位记录。在加载时,动态链接器会重定位GOT中的每个条目,使得它包含正确的目标地址。

 PLT全称Procedure Linkage Table,即过程链接表。它在可执行文件中也是一个单独的section,位于.text
section的前面。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目实际上都是一小段可执行的代码。

二、PLT和GOT里面有什么

2.1 PLT和GOT结构及关系

 下面我们依然以如何编译目标文件中的hello.c为例来研究GOT和PLT。在这个程序中我们使用到了库函数中的printf函数。

 首先,我们从section角度查看相关的结构。使用命令readelf -S hello,得到:

  [12] .plt              PROGBITS         00000000004003f0  000003f0
       0000000000000030  0000000000000010  AX       0     0     16
  [13] .plt.got          PROGBITS         0000000000400420  00000420
       0000000000000008  0000000000000000  AX       0     0     8
  [14] .text             PROGBITS         0000000000400430  00000430
       0000000000000182  0000000000000000  AX       0     0     16

	...

  [23] .got              PROGBITS         0000000000600ff8  00000ff8
       0000000000000008  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000601000  00001000
       0000000000000028  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         0000000000601028  00001028
       0000000000000010  0000000000000000  WA       0     0     8

 上述内容可知:

  1. .plt起始于0x4003f0,每个条目大小为0x10(16个字节),共3个条目;
  2. .plt.got起始于0x400420,大小为0x8(8个字节);
  3. .got起始于0x600ff8,每个条目大小为0x8(8个字节),共1个条目;
  4. .got.plt起始于0x601000,每个条目大小为0x8(8个字节),共5个条目;

然后我们使用命令objdump -d hello,得到以下内容:

Disassembly of section .plt:

00000000004003f0 <puts@plt-0x10>:
  4003f0:       ff 35 12 0c 20 00       pushq  0x200c12(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  4003f6:       ff 25 14 0c 20 00       jmpq   *0x200c14(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  4003fc:       0f 1f 40 00             nopl   0x0(%rax)

0000000000400400 <puts@plt>:
  400400:       ff 25 12 0c 20 00       jmpq   *0x200c12(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  400406:       68 00 00 00 00          pushq  $0x0
  40040b:       e9 e0 ff ff ff          jmpq   4003f0 <_init+0x28>

0000000000400410 <__libc_start_main@plt>:
  400410:       ff 25 0a 0c 20 00       jmpq   *0x200c0a(%rip)        # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
  400416:       68 01 00 00 00          pushq  $0x1
  40041b:       e9 d0 ff ff ff          jmpq   4003f0 <_init+0x28>

Disassembly of section .plt.got:

0000000000400420 <.plt.got>:
  400420:       ff 25 d2 0b 20 00       jmpq   *0x200bd2(%rip)        # 600ff8 <_DYNAMIC+0x1d0>
  400426:       66 90                   xchg   %ax,%ax

 结合上面的分析,可以知道:

  1. .plt里存放了三个条目分别是puts@plt-0x10, puts@plt, __libc_start_main@plt这三个函数的plt;
  2. .plt第一个条目将0x601008里的内容压栈,然后跳转到了0x601010,而0x601010 = 0x601000 + 2 * 0x8,也就是说跳转到了.got.plt的第3个条目(GOT是一个数组,因此下标从0开始计数,第三个条目就是GOT[2]);
  3. .plt第二个条目是printf函数的plt。它的第一条指令表示跳转到到0x601018,也就是.got.plt的第4个条目(GOT[3])。第二条指令将0压栈,第三条指令跳转到.plt第一个条目继续执行;
  4. .plt第三个条目是__libc_start_main函数的plt。它的第一条指令表示跳转到到0x601020,也就是.got.plt的第5个条目(GOT[4])。第二条指令将0压栈,第三条指令也跳转到.plt第一个条目继续执行;
  5. .plt.got里面的指令时跳转到0x600ff8,也就是.got的第一个条目(GOT[0]);

直观一点如下图所示:
PLT_GOT

注:图中省略了其他各个section。

2.2 使用debugger工具演绎PLT和GOT的使用

 下面我们使用GDB工具来详细分析printf函数调用过程。

2.2.1 编译

 如果想要使用GDB调试程序,需要在编译时添加-g参数。命令如下:

gcc -g -o hello hello.c
2.2.2 printf函数的调用

请点击此处继续阅读


想第一时间查看我的文章吗?请关注我的微信公众号号,搜索“落木萧萧技术论坛”或登陆我的个人博客:www.luomuxiaoxiao.com,更多精彩文章等你。

qrcode
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐