plt表链接过程

概念

在 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
@└────> # objdump -h plt | grep -E "plt|got"
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
@└────> # readelf -r plt

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
@└────> # objdump -d plt
Disassembly of section .plt:

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)

00000000004004d0 <puts@plt>:
4004d0: ff 25 42 0b 20 00 jmpq *0x200b42(%rip) # 601018 <puts@GLIBC_2.2.5>
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) # 601020 <printf@GLIBC_2.2.5>
4004e6: 68 01 00 00 00 pushq $0x1
4004eb: e9 d0 ff ff ff jmpq 4004c0 <.plt>

开始

  1. 首先断点到 puts 函数,查看调用处:
1
2
3
4
5
6
7
8
9
10
11
12
13
@(gdb) disassemble main
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@plt>
0x00000000004005e4 <+14>: mov $0x400698,%edi
0x00000000004005e9 <+19>: mov $0x0,%eax
0x00000000004005ee <+24>: callq 0x4004e0 <printf@plt>
0x00000000004005f3 <+29>: mov $0x0,%eax
0x00000000004005f8 <+34>: pop %rbp
0x00000000004005f9 <+35>: retq
End of assembler dump.

可以看到,调用处实际上是使用 call 指令走到 puts 的代码段。下面的 printf 也是如出一辙。

  1. 查看 puts@plt 的汇编指令
1
2
3
4
5
6
@(gdb) disassemble
Dump of assembler code for function puts@plt:
=> 0x00000000004004d0 <+0>: jmpq *0x200b42(%rip) # 0x601018 <puts@got.plt>
0x00000000004004d6 <+6>: pushq $0x0
0x00000000004004db <+11>: jmpq 0x4004c0
End of assembler dump.

可以看到,在汇编中,他首先要跳转到 0x601018 地址的位置。这个地址内容是个全局变量,实际上根据节的地址位置和大小可以判断,是处于 .got.plt 的位置内( 0x601000 ~ 0x601028)。所以可以认为,在 .got.plt 中,存在了 puts 函数的地址。

  1. 查看 .got.plt
1
2
3
4
5
@(gdb) x/16x 0x601018
0x601018 <puts@got.plt>: 0x004004d6 0x00000000 0x004004e6 0x00000000
0x601028: 0x00000000 0x00000000 0x00000000 0x00000000
0x601038: 0x00000000 0x00000000 0x00000000 0x00000000
0x601048: 0x00000000 0x00000000 0x00000000 0x00000000

查看表中内容,发现跳转的地址是 0x4004d6,这不就是我们跳转之前的下一个地址吗!(puts@plt 的第二条指令) 同理,printf 函数也是如此(0x4004e6)。这是因为,之前没有调用过 puts 函数,第一次查找的时候,.got.plt 表中找不到函数的地址,那就先返回继续执行去调用链接器获取地址。

  1. 准备调用链接器
1
2
3
4
00000000004004d0 <puts@plt>:
4004d0: ff 25 42 0b 20 00 jmpq *0x200b42(%rip) # 601018 <puts@GLIBC_2.2.5>
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. 调用链接器
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>
@(gdb) bt
#0 0x00007ffff7de64a0 in _dl_runtime_resolve_xsavec () from /lib64/ld-linux-x86-64.so.2
#1 0x00000000004005e4 in main () at plt.c:3

可以看到这里代码执行的是 ld 中的 _dl_runtime_resolve_xsavec 函数是第一次函数调用时用于查找函数符号的,并且在结尾处会直接去调用找到的函数符号(本文中为 puts 函数)。

  1. 写回 .got.plt 表
    在 puts 上打个断点,这样继续的话就是执行完 _dl_runtime_resolve_xsavec 还未执行 puts 的状态了。
1
2
3
4
5
6
7
@(gdb) bt
#0 0x00007ffff7a7d8c0 in puts () from /lib64/libc.so.6
#1 0x00000000004005e4 in main () at plt.c:3
@(gdb) x/10x 0x601018
0x601018 <puts@got.plt>: 0xf7a7d8c0 0x00007fff 0x004004e6 0x00000000
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
@(gdb) x/10x 0x601000
0x601000: 0x0000000000600e10 0x00007ffff7ffe1d0
0x601010: 0x00007ffff7de64a0 0x00007ffff7a7d8c0
0x601020 <printf@got.plt>: 0x00000000004004e6 0x0000000000000000
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
  • got[1]:0x00007ffff7ffe1d0 本模块的 link_map 的地址。编译期间会初始化为 0。link_map 是一个双向链表的入口,链接进程所有加载的动态库。当链接器查找符号时,通过遍历该链表找到对应的符号。

  • got[2]:0x00007ffff7de64a0 _dl_runtime_resolve_xsavec 的地址。

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);
// 第二个参数 0,为 <puts@plt>:中的 pushq $0x0;
// 同理如果是 printf,就是<printf@plt>:中 pushq $0x1;

总结

虚拟地址空间内流程图:
1

第二次调用:
2