概念
在 ELF 文件中,查看可以获得它的节的名字。其中有几个带有 plt 和 got 的节。
在此处,给出各节的定义如下:
- .got:Global Offset Table,全局偏移表。这是链接器为外部符号填充的实际偏移表。
- .plt:Procedure Linkage Table,程序链接表。他有两个作用,要么在 .got.plt 中拿到链接地址跳转,要么触发链接器去寻找地址。
- .got.plt:是 .got 的一部分(但是是两个不同的节),是 got 专门为 plt 准备的节,包含了 plt 表需要的地址。(新版 gcc 可能将他叫为 .plt.got)
- .rela.plt:程序链接表的重定位表,记录所有全局函数的动态链接信息,用于在程序加载时修正 plt 表中的跳转指针,使它们指向正确的地址。
实验
接下来将使用 gdb 一步一步跟着汇编走完动态链接的过程。
准备工作
实验代码如下:
1 2 3 4 5 6
| #include <stdio.h> int main() { puts("hello"); printf("hello"); return 0; }
|
查看节的地址与大小:
1 2 3 4 5 6
| @└────> plt: file format elf64-x86-64 9 .rela.plt 00000030 0000000000400468 0000000000400468 00000468 2**3 11 .plt 00000030 00000000004004c0 00000000004004c0 000004c0 2**4 20 .got 00000020 0000000000600fe0 0000000000600fe0 00000fe0 2**3 21 .got.plt 00000028 0000000000601000 0000000000601000 00001000 2**3
|
查看需要动态链接的符号:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @└────>
Relocation section '.rela.dyn' at offset 0x408 contains 4 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000600fe0 000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0 000000600fe8 000400000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0 000000600ff0 000500000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0 000000600ff8 000600000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
Relocation section '.rela.plt' at offset 0x468 contains 2 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000601018 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0 000000601020 000300000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
|
反汇编查看 plt 相关函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @└────> Disassembly of section .plt:
00000000004004c0 <.plt>: 4004c0: ff 35 42 0b 20 00 pushq 0x200b42(%rip) 4004c6: ff 25 44 0b 20 00 jmpq *0x200b44(%rip) 4004cc: 0f 1f 40 00 nopl 0x0(%rax)
00000000004004d0 <puts@plt>: 4004d0: ff 25 42 0b 20 00 jmpq *0x200b42(%rip) 4004d6: 68 00 00 00 00 pushq $0x0 4004db: e9 e0 ff ff ff jmpq 4004c0 <.plt>
00000000004004e0 <printf@plt>: 4004e0: ff 25 3a 0b 20 00 jmpq *0x200b3a(%rip) 4004e6: 68 01 00 00 00 pushq $0x1 4004eb: e9 d0 ff ff ff jmpq 4004c0 <.plt>
|
开始
- 首先断点到 puts 函数,查看调用处:
1 2 3 4 5 6 7 8 9 10 11 12 13
| Dump of assembler code for function main: 0x00000000004005d6 <+0>: push %rbp 0x00000000004005d7 <+1>: mov %rsp,%rbp => 0x00000000004005da <+4>: mov $0x400698,%edi 0x00000000004005df <+9>: callq 0x4004d0 <puts 0x00000000004005e4 <+14>: mov $0x400698,%edi 0x00000000004005e9 <+19>: mov $0x0,%eax 0x00000000004005ee <+24>: callq 0x4004e0 <printf 0x00000000004005f3 <+29>: mov $0x0,%eax 0x00000000004005f8 <+34>: pop %rbp 0x00000000004005f9 <+35>: retq End of assembler dump.
|
可以看到,调用处实际上是使用 call 指令走到 puts 的代码段。下面的 printf 也是如出一辙。
- 查看 puts@plt 的汇编指令
1 2 3 4 5 6
| Dump of assembler code for function puts => 0x00000000004004d0 <+0>: jmpq *0x200b42(%rip) # 0x601018 <puts 0x00000000004004d6 <+6>: pushq $0x0 0x00000000004004db <+11>: jmpq 0x4004c0 End of assembler dump.
|
可以看到,在汇编中,他首先要跳转到 0x601018 地址的位置。这个地址内容是个全局变量,实际上根据节的地址位置和大小可以判断,是处于 .got.plt 的位置内( 0x601000 ~ 0x601028)。所以可以认为,在 .got.plt 中,存在了 puts 函数的地址。
- 查看 .got.plt
1 2 3 4 5
| 0x601018 <puts 0x601028: 0x00000000 0x00000000 0x00000000 0x00000000 0x601038: 0x00000000 0x00000000 0x00000000 0x00000000 0x601048: 0x00000000 0x00000000 0x00000000 0x00000000
|
查看表中内容,发现跳转的地址是 0x4004d6,这不就是我们跳转之前的下一个地址吗!(puts@plt 的第二条指令) 同理,printf 函数也是如此(0x4004e6)。这是因为,之前没有调用过 puts 函数,第一次查找的时候,.got.plt 表中找不到函数的地址,那就先返回继续执行去调用链接器获取地址。
- 准备调用链接器
1 2 3 4
| 00000000004004d0 <puts 4004d0: ff 25 42 0b 20 00 jmpq *0x200b42(%rip) # 601018 <puts 4004d6: 68 00 00 00 00 pushq $0x0 4004db: e9 e0 ff ff ff jmpq 4004c0 <.plt>
|
首先 pushq $0x0,这个是在 got.plt 中的编号,如 puts 是 0,printf 是 1。这个参数是给后续链接器使用的。然后跳到了 .plt 的位置执行(0x4004c0)。可以看到,printf@plt 函数最后也是跳到这个位置执行。
- 调用链接器
1 2 3 4
| 00000000004004c0 <.plt>: 4004c0: ff 35 42 0b 20 00 pushq 0x200b42(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8> 4004c6: ff 25 44 0b 20 00 jmpq *0x200b44(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10> 4004cc: 0f 1f 40 00 nopl 0x0(%rax)
|
首先 push 了 0x601008 到栈中,这是 .got.plt 表中的一个地址。之后跳转到 0x601010 所存储的地址去执行相应的代码。不难看出,0x601010 也是存储在 .got.plt 表中的。查看一下存储的内容:
1 2 3 4
| @(gdb) x/10x 0x601010 0x601010: 0xf7de64a0 0x00007fff 0x004004d6 0x00000000 0x601020 <printf@got.plt>: 0x004004e6 0x00000000 0x00000000 0x00000000 0x601030: 0x00000000 0x00000000
|
可以看到,是让我们跳转到 0x00007ffff7de64a0 去执行相应的代码。那么这块代码是什么呢?
1 2 3 4 5
| @(gdb) info sharedlibrary From To Syms Read Shared Object Library 0x00007ffff7dd0fa0 0x00007ffff7df2cd4 Yes (*) /lib64/ld-linux-x86-64.so.2 0x00007ffff7a2cb90 0x00007ffff7b798ad Yes (*) /lib64/libc.so.6 (*): Shared library is missing debugging information.
|
可以看到,该地址是 ld-linux-x86-64.so 加载的位置。说明执行的是链接器的代码。
1 2 3 4 5 6 7 8 9 10
| 1: x/5i $pc => 0x7ffff7de64a0 <_dl_runtime_resolve_xsavec>: endbr64 0x7ffff7de64a4 <_dl_runtime_resolve_xsavec+4>: push %rbx 0x7ffff7de64a5 <_dl_runtime_resolve_xsavec+5>: mov %rsp,%rbx 0x7ffff7de64a8 <_dl_runtime_resolve_xsavec+8>: and $0xffffffffffffffc0,%rsp 0x7ffff7de64ac <_dl_runtime_resolve_xsavec+12>: sub 0x21616d(%rip),%rsp # 0x7ffff7ffc620 <_rtld_local_ro+384>
|
可以看到这里代码执行的是 ld 中的 _dl_runtime_resolve_xsavec 函数是第一次函数调用时用于查找函数符号的,并且在结尾处会直接去调用找到的函数符号(本文中为 puts 函数)。
- 写回 .got.plt 表
在 puts 上打个断点,这样继续的话就是执行完 _dl_runtime_resolve_xsavec 还未执行 puts 的状态了。
1 2 3 4 5 6 7
|
0x601018 <puts 0x601028: 0x00000000 0x00000000 0x00000000 0x00000000 0x601038: 0x00000000 0x00000000
|
可以看到,此时,got.plt 表中的地址已经被写为 puts 函数实际的地址了(0x00007ffff7a7d8c0 在 0x00007ffff7a2cb90 ~ 0x00007ffff7b798ad 范围内,属于 /lib64/libc.so.6),这样下次调用 puts 就不用再次调用链接器了。
题外话
其实看一下 .got.plt 表的内容,会发现明明 puts 是第一个需要被链接的函数,为什么第一个却不是它呢?
1 2 3 4 5 6
| 0x601000: 0x0000000000600e10 0x00007ffff7ffe1d0 0x601010: 0x00007ffff7de64a0 0x00007ffff7a7d8c0 0x601020 <printf 0x601030: 0x0000000000000000 0x0000000000000000 0x601040: 0x0000000000000000 0x0000000000000000
|
puts 地址实际上是 got[3]:0x00007ffff7a7d8c0,前面还有 3 项。其中:
- got[0]:0x0000000000600e10 自身模块 dynamic 段地址
1 2
| @(gdb) info symbol 0x0000000000600e10 _DYNAMIC in section .dynamic of /root/xxx/plt
|
1 2
| @(gdb) info symbol 0x00007ffff7de64a0 _dl_runtime_resolve_xsavec in section .text of /lib64/ld-linux-x86-64.so.2
|
_dl_runtime_resolve 格式:
1 2 3 4
| _dl_runtime_resolve((link_map*)(got[1]), 0);
|
总结
虚拟地址空间内流程图:
第二次调用: