文章目录
浅谈GOT表与PLT表
我们都知道动态链接库是我们程序开发中比较基础的手段,我们将公共的函数封装在一个so库中,即可用减少主程序的大小,也能增加公共代码的复用度。
那么对于一个共享库中的导出变量和函数的使用,是怎么引用和解析的呢?这里就是我们所说的GOT和PLT表的作用,下面我们详细来看一下基本流程和原理。
1. 实例
下面我们从一个最熟悉的例子除非,来讲述相关知识点,例子如下:
#include <stdio.h>
int main(int argc, char* argv[])
{
printf("hello world.\n");
return 0;
}
然后我们需要对这个代码进行编译如下:
$ gcc -g -z lazy elf.c -o elf.o
这里有个非常重要的参数是必须添加的,那就是-z lazy
,需要延迟加载动态库函数,不然我们的调试就看不到效果了(可执行文件加载的时候就已经解析完了共享库函数了)。
2. PLT表
PLT表是过程链接表(Procedure Linkage Table,PLT),这个表的存在就是为了使得代码能够方便的访问共享的函数或者变量,每一个引用的共享函数在PTL表中都会存在一个条目,我们可用看一下程序对PLT的使用,如下:
f$ objdump -d elf.o
elf.o: file format elf64-x86-64
0000000000000510 <puts@plt>:
510: ff 25 02 0b 20 00 jmpq *0x200b02(%rip) # 201018 <puts@GLIBC_2.2.5>
516: 68 00 00 00 00 pushq $0x0
51b: e9 e0 ff ff ff jmpq 500 <.plt>
000000000000063a <main>:
63a: 55 push %rbp
63b: 48 89 e5 mov %rsp,%rbp
63e: 48 83 ec 10 sub $0x10,%rsp
642: 89 7d fc mov %edi,-0x4(%rbp)
645: 48 89 75 f0 mov %rsi,-0x10(%rbp)
649: 48 8d 3d 94 00 00 00 lea 0x94(%rip),%rdi # 6e4 <_IO_stdin_used+0x4>
650: e8 bb fe ff ff callq 510 <puts@plt>
655: b8 00 00 00 00 mov $0x0,%eax
65a: c9 leaveq
65b: c3 retq
65c: 0f 1f 40 00 nopl 0x0(%rax)
我们可用看到callq 510 <puts@plt>
这一条汇编代码,对应的源码就是:
printf("hello world.\n");
也就是说的可执行文件中,并没有对导出函数puts
直接进行调用,而是通过一个打桩函数puts@plt
完成对puts
的真实引用,这样就可用做到不用修改任何可执行的代码就可以对动态库已经装载。
3. 动态解析
我们先来看一下动态解析的流程图:
基本流程如下:
- 主程序调用打桩函数
puts@plt
。 -
puts@plt
代码如下(其中jmpq *0x200b02(%rip) # 0x8201018
这一条语句跳转到GOT表中):
(gdb) disassemble 0x8000510
Dump of assembler code for function puts@plt:
0x0000000008000510 <+0>: jmpq *0x200b02(%rip) # 0x8201018
0x0000000008000516 <+6>: pushq $0x0
0x000000000800051b <+11>: jmpq 0x8000500
-
0x8201018
是GOT中的一项,值为0x0000000008000516
,也就是jmpq *0x200b02(%rip)
下一条语句的地址。 -
jmpq 0x8000500
这一条语句跳转到真实的动态解析过程,这个语句代码如下(这个小代码中主要是jmpq *0x200b04(%rip)
跳转语句):
(gdb) x /5i 0x8000500
0x8000500: pushq 0x200b02(%rip) # 0x8201008
0x8000506: jmpq *0x200b04(%rip) # 0x8201010
0x800050c: nopl 0x0(%rax)
0x8000510 <puts@plt>: jmpq *0x200b02(%rip) # 0x8201018
0x8000516 <puts@plt+6>: pushq $0x0
-
jmpq *0x200b04(%rip) # 0x8201010
调用的GOT表中的动态解析函数_dl_runtime_resolve_xsavec
。 -
_dl_runtime_resolve_xsavec
有两处功能:- 修改GOT表的条目(为了解决不需要每次都动态解析)。
- 跳转到
_IO_puts
执行动态库函数。
当动态解析完成知乎,整个调用关系如下:
过程如下:
(gdb) x /3i 0x8000650
0x8000650 <main+22>: callq 0x8000510 <puts@plt>
0x8000655 <main+27>: mov $0x0,%eax
0x800065a <main+32>: leaveq
(gdb) x /3i 0x8000510
0x8000510 <puts@plt>: jmpq *0x200b02(%rip) # 0x8201018
0x8000516 <puts@plt+6>: pushq $0x0
0x800051b <puts@plt+11>: jmpq 0x8000500
(gdb) x /1xg 0x8201018
0x8201018: 0x00007fffff080aa0
(gdb) x /5i 0x00007fffff080aa0
0x7fffff080aa0 <_IO_puts>: push %r13
0x7fffff080aa2 <_IO_puts+2>: push %r12
0x7fffff080aa4 <_IO_puts+4>: mov %rdi,%r12
0x7fffff080aa7 <_IO_puts+7>: push %rbp
0x7fffff080aa8 <_IO_puts+8>: push %rbx
4. 总结
下面我们用一个图总结一项可执行文件调用共享库函数的整个调用过程,如下: