Appearance
.plt / .got / .interp / .dynamic —— 动态链接基础设施
为什么调用
printf时,第一次比后续慢?为什么LD_BIND_NOW=1能让程序启动变慢?为什么RELRO能让 GOT 表部分只读?答案都在.plt、.got、.interp和.dynamic这四个节里。它们是动态链接的"硬件"层——不是概念,而是 ELF 里真实的字节和跳转。本篇把这四个节的结构、协作流程、以及延迟绑定的每一步指令级细节讲透。
更新时间:2026-08-06
一、术语前置
| 缩写 | 全称 | 含义 |
|---|---|---|
| PLT | Procedure Linkage Table | 过程链接表,跳板代码,位于 .plt 节 |
| GOT | Global Offset Table | 全局偏移表,存放函数/变量的运行时地址,位于 .got / .got.plt |
| RELRO | Relocation Read-Only | 安全机制,把部分 GOT 设为只读防篡改 |
| Lazy Binding | 延迟绑定 | 第一次调用时才解析地址,后续调用直接走 GOT |
ld.so | Dynamic Linker/Loader | 动态链接器,/lib64/ld-linux-x86-64.so.2,负责加载 .so 并完成重定位 |
| BND | Immediate Binder | PLT 延迟绑定中,首次调用的回跳地址 |
二、整体架构:四节如何协作

三、.interp —— 谁来做动态链接
.interp 只存一行:动态链接器的路径。如 /lib64/ld-linux-x86-64.so.2。
bash
$ readelf -p .interp ./cpu_demo
String dump of section '.interp':
[ 0] /lib64/ld-linux-x86-64.so.2对应 Program Header 里的 INTERP 段。内核 execve 加载 ELF 时,看到 PT_INTERP 就知道:别直接跳到 Entry point,先去加载 ld.so,把控制权交给它。ld.so 加载完所有 .so、做完重定位后,才跳到真正的 Entry point(_start)。
静态链接的程序没有
.interp——readelf -l也看不到INTERP段。
四、.dynamic —— 动态链接的"目录"
ld.so 加载后,需要知道:符号表在哪?字符串表在哪?重定位表在哪?要加载哪些 .so?这些信息全在 .dynamic 节里。
.dynamic 是一个 Elf64_Dyn 结构体数组,每个条目是一对 {d_tag, d_val}:
| d_tag | 含义 | d_val 示例 |
|---|---|---|
DT_NEEDED | 依赖的 .so 名 | libstdc++.so.6(在 DT_STRTAB 中的偏移) |
DT_STRTAB | 动态字符串表地址 | .dynstr 的虚拟地址 |
DT_SYMTAB | 动态符号表地址 | .dynsym 的虚拟地址 |
DT_PLTGOT | GOT 表地址 | .got.plt 的地址 |
DT_JMPREL | PLT 重定位表地址 | .rela.plt 的虚拟地址 |
DT_RELA | 普通重定位表地址 | .rela.dyn 的虚拟地址 |
DT_INIT / DT_FINI | init/fini 函数地址 | .init / .fini 入口 |
DT_HASH / DT_GNU_HASH | 符号哈希表地址 | 加速符号查找 |
DT_FLAGS_1 | 标志位 | DF_1_NOW = LD_BIND_NOW=1 |
bash
$ readelf -d ./cpu_demo | head
Dynamic section at offset 0x7de8 contains 28 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libstdc++.so.6]
0x00000001 (NEEDED) Shared library: [libm.so.6]
0x00000001 (NEEDED) Shared library: [libc.so.6]
...
.dynamic是 ld.so 的"藏宝图"——它用DT_*tag 指向所有动态链接所需的节,不依赖 Section Header Table。这就是 strip 删掉.symtab后动态链接照样工作的原因。
五、.plt —— 过程链接表(跳板)
5.1 为什么需要 PLT
你写 printf("hello"),编译器不知道 printf 在运行时会被加载到哪个地址——因为它来自 libc.so,加载地址是 ASLR 随机的。所以编译器生成:
asm
call printf@plt ; 不是 call printf(不知道地址),是 call PLT 里的跳板.plt 里每个外部函数有三条指令组成的固定模板:
asm
; ── printf@plt 的模板 ──
jmp *GOT[n] ; ① 先跳 GOT 存着的地址(已解析→直接函数;未解析→回下一行)
push $reloc_index ; ② 推入重定位索引(告诉 ld.so "帮我解析第几个")
jmp .plt[0] ; ③ 跳到 PLT[0] 公共入口- 已解析:
GOT[n]存着printf的真实地址 → 第一次jmp直接命中,O(1) - 未解析:
GOT[n]初始值 =PLT[n+1]的地址(即上面第②行)→jmp跳回来,推索引,进公共入口调 ld.so
5.2 PLT[0]:公共入口
.plt 的第 0 项不是某个函数,而是所有未解析函数的公共入口:
asm
; ── .plt[0] 公共入口 ──
push GOT[1] ; 推入 link_map 指针(标识当前动态库)
jmp *GOT[2] ; 跳到 ld.so 的 _dl_runtime_resolve()_dl_runtime_resolve(link_map, reloc_index) 用 reloc_index 去 .rela.plt 查到符号名,用 link_map 在已加载的动态库里搜符号地址,找到后填回 GOT[n],再跳到目标函数。下次调用同一个函数时,GOT 已填好,直接命中。
六、.got / .got.plt —— 全局偏移表
GOT 是一个指针数组,存放运行时才能确定的地址。分两块:
6.1 .got.plt —— PLT 专用的 GOT
前 3 项是保留的:
| GOT 索引 | 内容 | 用途 |
|---|---|---|
GOT[0] | .dynamic 节的地址 | ld.so 快速定位动态信息 |
GOT[1] | link_map 指针 | 标识当前动态库,_dl_runtime_resolve 的参数 |
GOT[2] | _dl_runtime_resolve 的地址 | PLT[0] 的最后一条 jmp 跳这 |
从 GOT[3] 开始,每项对应一个外部函数,初始值 = 对应 PLT stub 的第②条指令地址,解析后变成函数的真实地址。
6.2 .got —— 非 PLT 的 GOT
存放全局变量的地址(跨 .so 访问的全局变量需要重定位),以及某些不会走延迟绑定的函数地址。
6.3 RELRO 安全机制
LD_BIND_NOW=1 或链接时 -Wl,-z,relro -Wl,-z,now:
| 阶段 | 做了什么 | 效果 |
|---|---|---|
| Partial RELRO | .got 在加载后设只读 | .got.plt 仍可写(延迟绑定要写) |
| Full RELRO | LD_BIND_NOW=1:启动时解析所有符号,然后把 .got.plt 也设只读 | 完全不可篡改;启动稍慢,运行期无开销 |
七、延迟绑定完整流程图

八、.gnu.hash —— 符号哈希表
ld.so 在解析符号时,要在 .dynsym(动态符号表)里找到 "printf"。如果线性扫描 60+ 个条目每次都做 strcmp,太慢。.gnu.hash(或老的 .hash)是布隆过滤器 + 哈希桶的加速结构,把 O(n) 降到接近 O(1)。
bash
$ readelf -S ./cpu_demo | grep hash
[ 5] .gnu.hash GNU_HASH 00000000004002f8 ... A 6 0 8.dynamic 中的 DT_GNU_HASH tag 指向它的地址。
九、实操速查
bash
# 看动态链接信息全貌
readelf -d ./cpu_demo
# 看 PLT 里有哪些外部函数
objdump -d -j .plt ./cpu_demo
# 看 GOT 条目
objdump -R ./cpu_demo | grep JUMP_SLOT # .rela.plt → GOT 的映射
readelf -r ./cpu_demo | grep JUMP_SLOT
# 看动态符号哈希
readelf -s --gnu-hash ./cpu_demo
# 强制全解析(Full RELRO 效果)
LD_BIND_NOW=1 ./cpu_demo
# 看动态链接器路径
readelf -p .interp ./cpu_demo十、与相关文档的关系
| 文档 | 覆盖内容 | 本文的关系 |
|---|---|---|
| elf-format.md §四 | 常见 Section 简要列表 | 本文是动态链接四节的专题深入 |
| symbol-table.md | .dynsym / .dynstr 结构 | 本文 PLT 依赖 .dynsym 做符号查找 |
| relocation.md | .rela.plt / .rela.dyn 详解 | 本文的延迟绑定写回 GOT 本质上就是一次重定位 |
| compile-link-load.md §四 | 运行时加载全流程 | 本文聚焦 ELF 层面动态链接的"硬件" |
一句话总结:
.interp指定谁来做动态链接(ld.so),.dynamic告诉它信息在哪(符号表、重定位表、字符串表),.plt提供跳板模板(jmp *GOT → push index → jmp PLT[0]),.got.plt记住解析后的真实地址——第一次走完整解析流程(延迟绑定),之后直接跳 GOT(零开销)。RELRO则把 GOT 设只读防篡改。