GOT 表与 PLT 表深度解析:动态链接的核心机制
·
目录
一、动态链接的核心组件:GOT 与 PLT
在程序运行时,动态链接器需要解决外部符号的地址解析问题。GOT(Global Offset Table,全局偏移表)与 PLT(Procedure Linkage Table,过程链接表)作为动态链接的核心组件,通过延迟绑定机制实现了高效的符号解析。
动态链接的关键挑战:
- 程序运行时才确定共享库函数的真实地址
- 避免一次性加载所有共享库导致内存浪费
- 支持多个进程共享同一共享库的代码段
GOT 与 PLT 的协同工作,完美解决了上述问题,成为现代 ELF 文件格式的重要组成部分。
二、GOT 表:全局符号的地址枢纽
2.1 GOT 表的结构与功能
GOT 表是数据段(.got)中的一个数组,存储全局符号的地址引用,其核心作用是:
- 存储全局变量的地址
- 存储外部函数的地址
- 提供动态链接器的控制入口
GOT 表的关键条目:
GOT[0] -> .dynamic段地址(动态链接元数据)
GOT[1] -> 动态链接器标识(link_map结构地址)
GOT[2] -> 动态链接器入口(_dl_runtime_resolve)
GOT[n] -> 外部函数/全局变量的地址
2.2 GOT 表的内存布局示例
通过objdump查看 GOT 表:
$ objdump -x a.out | grep GOT
DYNAMIC SECTION:
GOT (0x0000000000404018)
GOT PLT (0x0000000000404028)
反汇编 GOT 表条目:
0000000000404028 <_GLOBAL_OFFSET_TABLE_>:
404028: 0000000000401000 R_X86_64_JUMP_SLOT puts@GLIBC_2.2.5
404030: 0000000000401010 R_X86_64_JUMP_SLOT printf@GLIBC_2.2.5
404038: 0000000000401020 R_X86_64_JUMP_SLOT __libc_start_main@GLIBC_2.2.5
三、PLT 表:函数调用的动态解析器
3.1 PLT 表的指令序列
PLT 表是代码段(.plt)中的一系列短代码片段,每个外部函数对应一个 PLT 条目,典型结构:
plt_entry:
jmp *GOT[entry] ; 跳转到GOT表中的函数地址
push $0 ; 压入符号ID
jmp PLT[0] ; 跳转到动态链接器入口
3.2 PLT 表的工作流程
以调用printf为例,PLT 表的执行流程:
- 程序调用
printf时,跳转到printf@plt printf@plt执行jmp *printf@got- 首次调用时,
printf@got指向printf@plt+6(下一条指令) - 执行
push $0和jmp PLT[0] PLT[0]调用动态链接器解析printf的真实地址- 动态链接器将真实地址写入
printf@got - 后续调用直接从
printf@got获取地址跳转
四、延迟绑定机制:性能与资源的平衡术
4.1 延迟绑定的核心优势
延迟绑定(Lazy Binding)将符号解析推迟到首次调用时,带来显著优势:
- 启动速度提升:避免加载未使用的函数
- 内存效率优化:仅解析实际调用的函数
- 共享库兼容性:支持运行时更新函数地址
4.2 首次调用与后续调用对比
首次调用流程:
graph TD
A[调用函数] --> B[跳转到PLT条目]
B --> C{GOT表已填充?}
C -- 否 --> D[调用动态链接器]
D --> E[解析地址并写入GOT]
E --> F[执行函数]
C -- 是 --> F
graph TD
A[调用函数] --> B[跳转到PLT条目]
B --> C[直接跳转到GOT表中的真实地址]
C --> D[执行函数]
五、实战分析:GOT 与 PLT 的协同案例
5.1 示例程序反汇编
// example.c
#include <stdio.h>
int main() {
printf("Hello GOT/PLT!\n");
return 0;
}
编译后查看 PLT 表:
$ objdump -d a.out | grep printf
0000000000401030 <printf@plt>:
401030: ff 25 28 40 40 00 jmpq *0x404028(%rip) # 404028 <printf@GLIBC_2.2.5>
401036: 68 00 00 00 00 pushq $0x0
40103b: e9 e0 ff ff ff jmpq 401020 <.plt>
首次调用时的 GOT 表状态:
0000000000404028 <printf@GLIBC_2.2.5>:
404028: 0000000000401036 R_X86_64_JUMP_SLOT printf@plt+6
调用后 GOT 表更新为真实地址:
0000000000404028 <printf@GLIBC_2.2.5>:
404028: 00007f20f3d20410 0x7f20f3d20410 # printf真实地址
六、安全视角:GOT 表劫持与防御
6.1 GOT 表劫持攻击
攻击者通过修改 GOT 表条目,将函数调用重定向到恶意代码:
# 攻击步骤(简化)
1. 找到目标函数的GOT表地址
2. 写入恶意代码地址
3. 触发函数调用,执行恶意代码
实际漏洞案例:
- 缓冲区溢出导致 GOT 表地址被覆盖
- 利用 UAF 漏洞修改 GOT 表条目
6.2 防御机制
-
地址空间布局随机化(ASLR):
$ sysctl -w kernel.randomize_va_space=2 # 启用ASLR -
GOT 表写保护:
$ gcc -Wl,-z,relro,-z,now # 启用RELRO保护 -
栈保护机制(Canary):
$ gcc -fstack-protector # 启用栈保护
七、深入理解:GOT 与 PLT 的设计哲学
7.1 硬件与软件的平衡
- 代码段只读:PLT 表位于代码段(只读),GOT 表位于数据段(可写)
- 缓存友好:GOT 表条目连续存储,提升内存访问效率
- 指令优化:PLT 表使用短指令序列减少跳转开销
7.2 动态链接的未来发展
随着容器技术和 Serverless 架构的普及,动态链接面临新挑战:
- 即时编译(JIT):GOT/PLT 机制与 JIT 的协同优化
- 安全增强:硬件级地址空间隔离对 GOT 表的影响
- 性能优化:大规模微服务场景下的符号解析效率
八、总结:GOT 与 PLT 的核心价值
GOT 表与 PLT 表的设计体现了计算机科学中的重要思想:
- 延迟计算:按需加载,提升资源利用率
- 间接寻址:通过中间层实现解耦与动态性
- 空间换时间:用 GOT 表的存储空间换取解析效率
对于开发者,理解 GOT 与 PLT 不仅有助于优化程序性能,更是逆向工程、漏洞分析的基础。在实践中,通过工具如objdump、gdb观察 GOT/PLT 的运行时行为,能深入理解动态链接的核心机制,为高性能、高安全性的程序开发奠定基础。
更多推荐


所有评论(0)