本文发自 http://www.binss.me/blog/plt-and-got/,转载请注明出处。
定义
最近在做lab时,gdb中反汇编后经常出现xxx@plt的指令,这是什么意思呢?花了一个下午进行学习,以文记之。
我们知道,函数调用就是在压入参数后对函数地址call一下,而由于编译时的局部性,对于定义在其他文件中的函数,编译时编译器会留下一个坑,等到链接时链接器会对坑进行填坑。但是对于定义在动态链接库中的函数,比如glibc中的函数printf来说,显然此时是不知道函数地址的,需要等到运行时才能进行填坑。
但由于现代操作系统不允许对代码段进行修改(也不应该修改,考虑动态链接库函数调用动态链接库函数的情况),不能在运行时直接填坑,因此只能采取比较间接的方法。比如函数利用data段存储的坑进行跳转,而data段的坑在运行时进行填坑。但是我们又不想一开始就把所有的坑给填了(后文会提到),于是采取了比较间接的方式:
先让坑指向一段同样位于代码段的代码,这段代码判断坑是否被填,如果没填,填之;如果填了,直接调用函数。
这段代码段被定义为过程连接表(Procedure Linkage Table,PLT),而data段中的坑被定义为全局偏移表(Global Offset Table, GOT)。
实验
对于以下代码:
#include <stdio.h>
int main()
{
printf("hello");
printf("hello again");
return 0;
}
编译后,用objdump查看:
0000000000400526 <main>:
400526: 55 push %rbp
400527: 48 89 e5 mov %rsp,%rbp
40052a: bf d4 05 40 00 mov $0x4005d4,%edi
40052f: b8 00 00 00 00 mov $0x0,%eax
400534: e8 c7 fe ff ff callq 400400 <printf@plt>
400539: bf da 05 40 00 mov $0x4005da,%edi
40053e: b8 00 00 00 00 mov $0x0,%eax
400543: e8 b8 fe ff ff callq 400400 <printf@plt>
400548: b8 00 00 00 00 mov $0x0,%eax
40054d: 5d pop %rbp
40054e: c3 retq
40054f: 90 nop
链接器发现printf定义在动态库中,于是将printf函数的位置设置为地址为0x400400的printf@plt
。
对于PLT表段,内容如下:
Disassembly of section .plt:
00000000004003f0 <printf@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 <printf@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
printf@plt有两个跳转,一个使用0x601018中存的数据,一个跳转到printf@plt-0x10,是什么意思呢?
根据maps信息:
binss@giantvm:~/work/test$ cat /proc/5119/maps
00400000-00401000 r-xp 00000000 fc:00 25175545 /home/binss/work/test/plt
00600000-00601000 r--p 00000000 fc:00 25175545 /home/binss/work/test/plt
00601000-00602000 rw-p 00001000 fc:00 25175545 /home/binss/work/test/plt
00602000-00623000 rw-p 00000000 00:00 0 [heap]
7ffff7a0e000-7ffff7bcd000 r-xp 00000000 fc:00 18747735 /lib/x86_64-linux-gnu/libc-2.23.so
7ffff7bcd000-7ffff7dcd000 ---p 001bf000 fc:00 18747735 /lib/x86_64-linux-gnu/libc-2.23.so
7ffff7dcd000-7ffff7dd1000 r--p 001bf000 fc:00 18747735 /lib/x86_64-linux-gnu/libc-2.23.so
7ffff7dd1000-7ffff7dd3000 rw-p 001c3000 fc:00 18747735 /lib/x86_64-linux-gnu/libc-2.23.so
7ffff7dd3000-7ffff7dd7000 rw-p 00000000 00:00 0
7ffff7dd7000-7ffff7dfd000 r-xp 00000000 fc:00 18747736 /lib/x86_64-linux-gnu/ld-2.23.so
7ffff7fe4000-7ffff7fe7000 rw-p 00000000 00:00 0
7ffff7ff6000-7ffff7ff8000 rw-p 00000000 00:00 0
7ffff7ff8000-7ffff7ffa000 r--p 00000000 00:00 0 [vvar]
7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso]
7ffff7ffc000-7ffff7ffd000 r--p 00025000 fc:00 18747736 /lib/x86_64-linux-gnu/ld-2.23.so
7ffff7ffd000-7ffff7ffe000 rw-p 00026000 fc:00 18747736 /lib/x86_64-linux-gnu/ld-2.23.so
7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
可见0x601018处于data段中,根据objdump的注释信息,我们得知它是GOT表的表项。
我们采用gdb大法,在第一个printf处断点,看看0x601018里面是啥:
(gdb) x 0x601018
0x601018: 0x00400406
可见指向了0x00400406,即0x400400跳转到了它的下一条语句,于是会执行第二个跳转,跳转到printf@plt-0x10。
这是一个典型的函数调用过程,先压入参数,再跳转到函数地址执行,这个地址同样在GOT中,我们看看0x601010是啥:
(gdb) x 0x601010
0x601010: 0xf7dee6a0
(gdb) disas 0xf7dee6a0
No function contains specified address.
然而我们直接这个地址进行反汇编时发现找不到内容?根据上面的maps信息,猜想此时已经进入glibc中,也就是7ffff7a0e000开始的代码段中了。于是我们尝试魔改地址为0x7ffff7dee6a0,再次disas,得到了位于_dl_runtime_resolve_avx函数内一大串汇编代码,对于这种代码,我选择google。这个函数做的就是查找printf函数在动态链接库中的地址,然后将该地址回填到0x601018中,然后调用printf。
这时问题来了,_dl_runtime_resolve_avx是如何得知要向哪里填充什么函数的呢?答案是根据elf文件的relocation信息:
binss@giantvm:~/work/test$ readelf -r plt
...
Relocation section '.rela.plt' at offset 0x398 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
这些信息告诉_dl_runtime_resolve_avx,对于参数为0x0的调用,它要填坑的函数rela.plt段中第一个记录的函数,即printf,填到0x601018中。对于0x1的调用,要填坑的函数就是是__libc_start_main。
在执行完该行代码后,再次查看0x601018:
(gdb) n
6 printf("hello again");
(gdb) x 0x601018
0x601018: 0xf7a63800
(gdb) disas 0x7ffff7a63800
Dump of assembler code for function __printf:
0x00007ffff7a63800 <+0>: sub $0xd8,%rsp
...
它已经指向了填坑后的glibc中printf的地址。
进一步试验
在前面的实验中,我们基于这样一个猜想:参数的值和在rela.plt中的偏移量是一一对应的,即参数为多少就对应第几条记录。为了验证这个问题,我们再搞两个调用进行实验:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main()
{
int random = rand();
int random_sqrt = sqrt(random);
printf("%d", random);
printf("%d", random_sqrt);
return 0;
}
PLT段:
Disassembly of section .plt:
0000000000400520 <printf@plt-0x10>:
400520: ff 35 e2 0a 20 00 pushq 0x200ae2(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
400526: ff 25 e4 0a 20 00 jmpq *0x200ae4(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
40052c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000400530 <printf@plt>:
400530: ff 25 e2 0a 20 00 jmpq *0x200ae2(%rip) # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
400536: 68 00 00 00 00 pushq $0x0
40053b: e9 e0 ff ff ff jmpq 400520 <_init+0x28>
0000000000400540 <__libc_start_main@plt>:
400540: ff 25 da 0a 20 00 jmpq *0x200ada(%rip) # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
400546: 68 01 00 00 00 pushq $0x1
40054b: e9 d0 ff ff ff jmpq 400520 <_init+0x28>
0000000000400550 <sqrt@plt>:
400550: ff 25 d2 0a 20 00 jmpq *0x200ad2(%rip) # 601028 <_GLOBAL_OFFSET_TABLE_+0x28>
400556: 68 02 00 00 00 pushq $0x2
40055b: e9 c0 ff ff ff jmpq 400520 <_init+0x28>
0000000000400560 <rand@plt>:
400560: ff 25 ca 0a 20 00 jmpq *0x200aca(%rip) # 601030 <_GLOBAL_OFFSET_TABLE_+0x30>
400566: 68 03 00 00 00 pushq $0x3
40056b: e9 b0 ff ff ff jmpq 400520 <_init+0x28>
Disassembly of section .plt.got:
0000000000400570 <.plt.got>:
400570: ff 25 82 0a 20 00 jmpq *0x200a82(%rip) # 600ff8 <_DYNAMIC+0x1e0>
400576: 66 90 xchg %ax,%ax
relocation段:
binss@giantvm:~/work/test$ readelf -r plt2
...
Relocation section '.rela.plt' at offset 0x498 contains 4 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000601018 000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000601020 000300000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000601028 000700000007 R_X86_64_JUMP_SLO 0000000000000000 sqrt@GLIBC_2.2.5 + 0
000000601030 000800000007 R_X86_64_JUMP_SLO 0000000000000000 rand@GLIBC_2.2.5 + 0
发现0x2对应sqrt,0x3对应rand。这验证了我们的猜想。
另外对该程序进行gdb,断点main函数进行查看,发现0x601018、0x601028、0x601030都存放了0x00400566,即它们在第一次执行的时候,都跳转到了printf@plt-0x10进行执行。
这里问题又来了,为啥不直接通过pushq 0x200ae2(%rip)
然后jmpq *0x200ae4(%rip)
直接跳转到_dl_runtime_resolve_avx中运行呢?对此我的猜想是,程序通常大量调用动态链接库中的函数,如果每个调用都重复这两行,会导致plt表膨胀(虽然也没膨胀多少),而且不优雅,于是抽出来作为一个公共函数。这个公共函数只有两条指令,而其他函数都有三条指令,不舒服,因此再补个nopl占位。这样一来,这些函数就对齐了,我们可以认为每三个指令就是PLT的一个表项。
另外我们还观察到一个现象:0x601010中的内容,也就是_dl_runtime_resolve_avx的地址,在main函数的第一条指令执行之前就已经填充了。可以认为,在程序开始执行之前,首先要有一种外部机制(动态链接器)根据rela.plt将这个坑给填了,这样后续的函数才能调用该函数进行填坑。至于为什么动态链接器为啥不顺便把后面的坑都填了,考虑这种情况:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main()
{
int ok;
scanf("%d", &ok);
if (ok)
{
int random = rand();
int random_sqrt = sqrt(random);
printf("%d", random);
printf("%d", random_sqrt);
}
return 0;
}
如果用户输入0,那么程序直接返回,坑也就不用填了,这种就是所谓的lazy策略吧。
总结
PLT是位于text段的一张表,每个表项由三行代码组成,总长度为0x10,有多少个动态链接的函数就有多少个表项。
GOT是位于data段的一张表,每个表项用于存放动态链接函数的地址。在填坑前存放的是PLT表项第二条指令的地址。
当动态链接的函数第一次被调用时,跳转到对应的PLT表项,在第一个指令中根据对应GOT表项进行跳转,由于此时未填坑,跳转到PLT表项的第二条指令,压入参数,在第三个指令中跳转到printf@plt-0x10,由它负责调用_dl_runtime_resolve_avx进行填坑并执行一次调用。
于是在后续调用时,跳转到对应的PLT表项,在第一个指令中根据对应GOT表项进行跳转,由于此时已填坑,跳转到动态链接库的对应函数中进行执行。
1F lanxinyu 5 years, 3 months ago 回复
是否可以在界面增加一个“点赞”按钮
2F 月踏 3 years, 4 months ago 回复
赞
3F cchh 3 years, 3 months ago 回复
获益匪浅,感谢