Skip to the content.

Procedure Linkage Table and Global Offset Table

发表于 2025-05-05

过程链接表(PLT)和全局偏移表(GOT)

在linux进程调试中,经常会看到你所调用的函数多了一个@plt的后缀,例如printf@plt, puts@plt等等。相信你当时一定有些疑问,plt是什么? printf@plt与printf有什么关系?… 下面让我们慢慢揭开他们的面纱。

1. 认识PLT

为了帮助我们更好的理解,我们理论实践相结合。边调试边学习。

示例程序

写一个简单的printf调用程序,然后用gdb调试。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
	printf("plt test\n");
	return 0;
}
(gdb) start
Temporary breakpoint 1 at 0x1139
Starting program: /var/log/test 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Temporary breakpoint 1, 0x0000555555555139 in main ()
(gdb) disass main
Dump of assembler code for function main:
   0x0000555555555135 <+0>:     push   %rbp
   0x0000555555555136 <+1>:     mov    %rsp,%rbp
=> 0x0000555555555139 <+4>:     sub    $0x10,%rsp
   0x000055555555513d <+8>:     mov    %edi,-0x4(%rbp)
   0x0000555555555140 <+11>:    mov    %rsi,-0x10(%rbp)
   0x0000555555555144 <+15>:    mov    -0x10(%rbp),%rax
   0x0000555555555148 <+19>:    mov    (%rax),%rax
   0x000055555555514b <+22>:    mov    %rax,%rsi
   0x000055555555514e <+25>:    lea    0xeaf(%rip),%rdi        # 0x555555556004
   0x0000555555555155 <+32>:    mov    $0x0,%eax
   0x000055555555515a <+37>:    call   0x555555555030 <printf@plt>  //printf@plt 本尊
   0x000055555555515f <+42>:    mov    $0x0,%eax
   0x0000555555555164 <+47>:    leave
   0x0000555555555165 <+48>:    ret
End of assembler dump.

从示例中,我们看到了printf@plt, 那么为什么要用printf@plt, 而不是直接用printf呢? 此时就引出了PLT的概念。

2. PLT(Procedure Linkage Table)

什么是PLT

定义:PLT,全称Procedure Linkage Table,是ELF可执行文件中用于支持动态链接的一种技术机制。 作用:它为程序提供一种间接调用共享库函数的方式,即在程序运行时,根据动态链接器的解析结果调用正确的函数地址。

为什么需要PLT?

  • 动态链接的需要:程序在编译时不知道最终函数(尤其是共享库函数)的实际地址,需要一种机制在运行时动态绑定。
  • 支持Lazy Binding:只有真正调用到某个函数时,才解析其地址,加快程序启动速度。
  • 统一跳转入口:对动态库函数的调用统一经过PLT,方便动态链接器管理。
  • 位置无关性(PIE、共享库都需要)。

以上都是需要PLT的原因,本质问题是:程序在编译/链接时不知道外部函数最终的内存地址(比如printf在libc里的位置)。 没有PLT的话,程序根本没办法在运行时正确跳到动态库的函数。所以必须有一个机制在程序运行时动态绑定符号地址,这就是PLT最初被设计出来的根本目的。

参考上面的示例程序,在编译时不知道程序运行时,libc.so动态加载后的地址,自然也无法获取到so中printf函数的地址。 为了能够正确调用libc.so中的printf函数,PLT机制就应运而生了。

function@plt

继续调试示例程序,查看printf@plt。

(gdb) disass 0x555555555030
Dump of assembler code for function printf@plt:
   0x0000555555555030 <+0>:     jmp    *0x2fca(%rip)        # 0x555555558000 <printf@got.plt>  //这个地址很关键
   0x0000555555555036 <+6>:     push   $0x0
   0x000055555555503b <+11>:    jmp    0x555555555020

这个函数相当简单,只有3条指令。第一条指令就是一个jmp,读取printf@got.plt中的值,跳转到这个值指向的内存。

这时你的脑中又会多出一个疑问:这个printf@got.plt又是什么? 要说清这个,我们先了解下GOT。

3. GOT(Global Offset Table)

什么是GOT

GOT,全称Global Offset Table,是ELF格式(Executable and Linkable Format)中用于支持动态链接的重要数据结构。 它在程序运行时保存了需要动态解析的符号(如函数、全局变量)的实际地址。GOT是实现位置无关代码(PIC, Position Independent Code)的关键技术之一。 它的每一项(每一个GOT表entry)里,保存的是一个地址(64位机器上是8字节)。

为什么需要GOT

  • 支持动态链接:GOT为动态库中的函数和变量提供一种间接访问的机制,程序不需要在编译时确定符号的真实地址。
  • 实现位置无关性:GOT允许程序即使被加载到任意内存地址也能正确访问外部资源。
  • 支持符号重绑定(symbol rebinding):通过修改GOT表项,可以在程序运行期间改变函数指针或全局变量指针(比如LD_PRELOAD)。

以上都是需要GOT的原因,最根本的原因是支持动态链接,程序在编译时不知道符号(函数/变量)的最终地址,必须通过GOT表在运行时动态确定地址。

got.plt 表保存了什么

got.plt表保存的是所有通过PLT调用的外部函数的间接跳转地址。每个外部函数(比如printf()、malloc())都有一个对应的got.plt表项。程序在运行时,通过访问 got.plt 的表项,跳到外部函数的真实地址。

什么需要 got.plt?

  • 普通 got 段是给全局变量、静态数据用的。
  • got.plt 段是专门给PLT函数调用用的。

4. 继续调试

回到 printf@plt的反汇编代码,逐步解读。

   0x0000555555555030 <+0>:     jmp    *0x2fca(%rip)        # 0x555555558000 <printf@got.plt>
   0x0000555555555036 <+6>:     push   $0x0
   0x000055555555503b <+11>:    jmp    0x555555555020

jmp *0x2fca(%rip)

0x2fca(%rip) 也就是 0x555555558000, 去这个地址中找到指令的地址,跳转执行。

gdb) x/gx 0x555555558000
0x555555558000 <printf@got.plt>:        0x0000555555555036	//其实就是printf@plt的第二条指令的地址

也就是说实际上 printf@plt的第一条jmp指令,跳转到 printf@plt的第二条指令。是不是有点多此一举?不要着急,很快你就会明白的。

push $0x0

这条指令将printf的符号索引压入栈中。每个需要重定位的的符号都会在 rela.plt表中有个entry。0 表示 printf在rela.plt表中的索引为0。

# readelf -r ./test

Relocation section '.rela.dyn' at offset 0x540 contains 8 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000003dd0  000000000008 R_X86_64_RELATIVE                    1130
000000003dd8  000000000008 R_X86_64_RELATIVE                    10f0
000000004010  000000000008 R_X86_64_RELATIVE                    4010
000000003fc0  000100000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.34 + 0
000000003fc8  000200000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTM[...] + 0
000000003fd0  000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000003fd8  000500000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCl[...] + 0
000000003fe0  000600000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0

Relocation section '.rela.plt' at offset 0x600 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000004000  000300000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
  • 这个entry的Offset保存了 printf@got.plt地址。 这里的0x4000是原始地址,需要加上程序基地址0x555555554000。

0x555555554000 + 0x4000 = 0x555555558000 正好就是

//查看printf@got.plt地址
(gdb) info address printf@got.plt
Symbol "printf@got.plt" is at 0x555555558000 in a file compiled without debugging.

//通过link_map获取基地址,也就是可执行文件中符号地址与内存中符号地址的差异量
(gdb) x/gx 0x555555557ff0
0x555555557ff0:	0x00007ffff7ffe2e0	//link_map地址
(gdb) x/gx 0x00007ffff7ffe2e0
0x7ffff7ffe2e0:	0x0000555555554000	//link_map.l_addr就是基地址

//通过进程映射获取基地址
(gdb) info proc mappings	//这里可以看到
process 14245
Mapped address spaces:

          Start Addr           End Addr       Size     Offset  Perms  objfile
      0x555555554000     0x555555555000     0x1000        0x0  r--p   /var/log/gui_upload/test	//基地址
      0x555555555000     0x555555556000     0x1000     0x1000  r-xp   /var/log/gui_upload/test
  • 这个entry的 Sym. Name + Addend 保存了 符号的字符串名称, 后续在so中找。

也就是说后续要在 libc.so中查找 printf@GLIBC_2.2.5 对应符号的地址。

jmp 0x555555555020

这一步是跳转0x555555555020后继续执行指令。

(gdb) x/3i 0x555555555020
   0x555555555020:      push   0x2fca(%rip)        # 0x555555557ff0	//0x555555557ff0 中存放了link_map的地址
   0x555555555026:      jmp    *0x2fcc(%rip)        # 0x555555557ff8
   0x55555555502c:      nopl   0x0(%rax)

这里第一条指令是 将 line_map地址 压入栈中。然后跳转到 0x555555557ff8中存放的指令地址去执行。

(gdb) x/gx 0x555555557ff0
0x555555557ff0:	0x00007ffff7ffe2e0	//link_map地址

(gdb) x/gx 0x555555557ff8
0x555555557ff8:	0x00007ffff7fdf720	//跳转指令地址
(gdb) x/64i 0x00007ffff7fdf720	//实际上是<_dl_runtime_resolve_fxsave>
   0x7ffff7fdf720:	push   %rbx
   0x7ffff7fdf721:	mov    %rsp,%rbx
   0x7ffff7fdf724:	and    $0xffffffffffffffc0,%rsp
   0x7ffff7fdf728:	sub    0x1d561(%rip),%rsp        # 0x7ffff7ffcc90 <_rtld_global_ro+464>
   0x7ffff7fdf72f:	mov    %rax,(%rsp)
   0x7ffff7fdf733:	mov    %rcx,0x8(%rsp)
   0x7ffff7fdf738:	mov    %rdx,0x10(%rsp)
   0x7ffff7fdf73d:	mov    %rsi,0x18(%rsp)
   0x7ffff7fdf742:	mov    %rdi,0x20(%rsp)
   0x7ffff7fdf747:	mov    %r8,0x28(%rsp)
   0x7ffff7fdf74c:	mov    %r9,0x30(%rsp)
   0x7ffff7fdf751:	mov    $0xee,%eax
   0x7ffff7fdf756:	xor    %edx,%edx
   0x7ffff7fdf758:	mov    %rdx,0x240(%rsp)
   0x7ffff7fdf760:	mov    %rdx,0x248(%rsp)
   0x7ffff7fdf768:	mov    %rdx,0x250(%rsp)
   0x7ffff7fdf770:	mov    %rdx,0x258(%rsp)
   0x7ffff7fdf778:	mov    %rdx,0x260(%rsp)
   0x7ffff7fdf780:	mov    %rdx,0x268(%rsp)
   0x7ffff7fdf788:	mov    %rdx,0x270(%rsp)
   0x7ffff7fdf790:	mov    %rdx,0x278(%rsp)
   0x7ffff7fdf798:	xsave  0x40(%rsp)
   0x7ffff7fdf79d:	mov    0x10(%rbx),%rsi	//printf的符号索引 0
   0x7ffff7fdf7a1:	mov    0x8(%rbx),%rdi	//link_map地址 0x00007ffff7ffe2e0
   0x7ffff7fdf7a5:	call   0x7ffff7fddad6	//<_dl_fixup>
   0x7ffff7fdf7aa:	mov    %rax,%r11	//返回printf的真正地址,保存的r11寄存器
   0x7ffff7fdf7ad:	mov    $0xee,%eax
   0x7ffff7fdf7b2:	xor    %edx,%edx
   0x7ffff7fdf7b4:	xrstor 0x40(%rsp)
   0x7ffff7fdf7b9:	mov    0x30(%rsp),%r9
   0x7ffff7fdf7be:	mov    0x28(%rsp),%r8
   0x7ffff7fdf7c3:	mov    0x20(%rsp),%rdi
   0x7ffff7fdf7c8:	mov    0x18(%rsp),%rsi
   0x7ffff7fdf7cd:	mov    0x10(%rsp),%rdx
   0x7ffff7fdf7d2:	mov    0x8(%rsp),%rcx
   0x7ffff7fdf7d7:	mov    (%rsp),%rax
   0x7ffff7fdf7db:	mov    %rbx,%rsp
   0x7ffff7fdf7de:	mov    (%rsp),%rbx
   0x7ffff7fdf7e2:	add    $0x18,%rsp
   0x7ffff7fdf7e6:	jmp    *%r11		//跳转到printf去执行

_dl_fixup 就不再展开了,简而言之, 它通过 符号索引和link_map,找到对应的rela.plt(见上面), 从而得到最终需要写入的 printf@got.plt地址, 然后通过 printf@GLIBC_2.2.5 字串,遍历link_map,找到对应符号的真实地址。最后写入到 printf@got.plt中。再跳转到真正的printf地址去执行。

这里有两个关键点:

  • 将真正printf的符号地址 写入 printf@got.plt, 供后续使用
  • 跳转 printf的符号地址,继续本次调用的执行。

更新后的printf@got.plt

我们在main退出前设置断点, 观察printf第一次调用后的变化。

(gdb) b *0x0000555555555164
Breakpoint 2 at 0x555555555164: file test.c, line 9.
(gdb) continue
Continuing.
[/var/log/test] plt test

Breakpoint 2, main (argc=1, argv=0x7fffffffed88) at test.c:9
9	in test.c

(gdb) disass 0x555555555030
Dump of assembler code for function printf@plt:
   0x0000555555555030 <+0>:	jmp    *0x2fca(%rip)        # 0x555555558000 <printf@got.plt>
   0x0000555555555036 <+6>:	push   $0x0
   0x000055555555503b <+11>:	jmp    0x555555555020
End of assembler dump.
(gdb) x/gx 0x555555558000
0x555555558000 <printf@got.plt>:	0x00007ffff7e809ef	//此时已经更新成printf的真正地址了,后续直接jmp到printf执行。
(gdb) info address printf
Symbol "printf" is at 0x7ffff7e809ef in a file compiled without debugging.

对比 printf@got.plt中的内容

  • 调用前: 0x0000555555555036 ,printf@plt的第二条压栈质量,后续调用_dl_runtime_resolve_fxsave 去解析并保存符号真正地址。
  • 调用后: 0x00007ffff7e809ef ,printf的真正地址,再次调用printf@plt时,直接jmp到printf去执行。

5. 初次调用和后续执行路径

下面是初次调用和后续执行路径的示意图。 可见PLT的核心在于 printf@got.plt 这个地址,而其他代码都没有改变。 这个地址默认保存 printf@plt的下一条指令,最终去解析和保存真正的符号地址。 一旦保存后,这里的jmp就直接跳转到真正的符号地址去执行。

+---------------------+
| 调用 printf()       |
+---------------------+
           |
           v
+------------------------------+
| printf@plt (PLT entry)       |
| jmp *printf@got.plt          |
+------------------------------+
                          |
                          v
                     (是否已绑定?)
                      /        \
                     / 否       \ 是
                    /            \
                   v              v
+-------------------------+      +------------------------+
| printf@got.plt 指向:    |      | printf@got.plt 指向:   |
| _dl_runtime_resolve     |      | 真正的printf地址        |
+-------------------------+      +------------------------+
           |                                |
           v                                |
+-------------------------------+           |
| _dl_runtime_resolve[_rxsave]  |           |
| - 保存寄存器状态                |           |
| - call _dl_fixup              |           |
+-------------------------------+           |
           |                                |
           v                                |
+--------------------------+                |
| _dl_fixup                |                |
| - 找printf符号            |                |
| - 确定真实地址             |                |
| - 写入GOT[printf]         |                |
+--------------------------+                |
           |                                |
           v                                |
+------------------------------+            |
| 返回 printf 的真实地址,存入%11 |            |
+------------------------------+            |
           |                                |
           v                                |
+-----------+        +------------+         |
| jmp *%r11 |------> | 执行printf  | <-------+
+-----------+        +------------+

本文访问次数:...