C 语言符号表与重定位
更新时间:2026-08-26。本文是
languages/c/主题专家层文档。编译、链接与 ELF 讲了编译四阶段和链接的"是什么",本文钻进去讲链接器具体怎么做——符号表怎么组织、未定义符号怎么被"对上号"、重定位表里那一串R_X86_64_*是什么。这是理解undefined reference报错、动态库延迟绑定的底层功。
本文要回答的问题
nm输出的T/D/B/U各代表什么?符号表里存了什么?- 链接器怎么把"调用了但没定义"的
printf对上号?重定位表干什么? - 动态库为什么能做到"启动时再解析"?GOT 和 PLT 是什么?
一、符号表:目标文件的"花名册"
每个 .o 文件都带一张符号表(.symtab),记录这个文件"定义了哪些符号、引用了哪些符号"。用 nm 查看:
; nm opt-test.o 实测(节选)
0000000000000000 T constant_fold ; T = Text,代码段已定义函数
0000000000000000 T dead_code ; T = 代码段函数
0000000000000000 T overflow_test
U printf ; U = Undefined,未定义(要链接器去找)
U __stack_chk_fail
0000000000000000 D data_start ; D = Data,已初始化全局变量
0000000000000000 B __bss_start ; B = BSS,未初始化全局变量符号类型速查
| 标记 | 含义 | 段 |
|---|---|---|
T / t | 已定义函数(大写=外部可见,小写=static 局部) | .text |
D / d | 已初始化全局/静态变量 | .data |
B / b | 未初始化全局/静态变量 | .bss |
U | 未定义,需要其他目标文件/库提供 | — |
W | 弱符号(weak),可被强符号覆盖 | — |
关键点:U 符号是链接器的"待办清单"——你的代码调用了 printf,但 printf 在 libc 里,所以 .o 里它是 U。链接时链接器必须把每个 U 都匹配到一个 T/D,否则报 undefined reference to 'printf'。
二、重定位表:链接器怎么"改地址"
符号表解决"符号在哪",重定位表解决"引用处的地址怎么填"。因为编译单个 .o 时,函数地址还是未知的(printf 到底在内存哪,只有链接后才知道),所以编译器在"调用 printf"的位置留了个空,等链接器来填。
用 readelf -r 看重定位表:
; readelf -r opt-test.o 实测(节选)
Relocation section '.rela.text' at offset 0x428 contains 12 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000008f 000e00000002 R_X86_64_PC32 0000000000000000 printf - 4
000000000094 000a00000002 R_X86_64_PC32 000000000000002e constant_fold - 4
0000000000c5 000e00000002 R_X86_64_PC32 0000000000000000 printf - 4每一行是一个"待填充的位置":
| 字段 | 含义 |
|---|---|
Offset | 需要修改的指令位置(相对 .text 的偏移) |
Type | 重定位类型(怎么算这个地址) |
Sym. Name | 要引用哪个符号 |
Addend | 附加偏移量 |
常见重定位类型
| 类型 | 含义 | 场景 |
|---|---|---|
R_X86_64_PC32 | 相对 PC 的 32 位偏移(call 指令用) | 函数调用、if 跳转 |
R_X86_64_32 | 绝对 32 位地址 | 引用数据 |
R_X86_64_PLT32 | 相对 PLT 的偏移(动态库调用) | 调用动态库函数 |
R_X86_64_GOTPCREL | 相对 GOT 的偏移 | 访问动态库全局数据 |
实测里
printf是R_X86_64_PC32,因为opt-test.c里printf是静态链接的 libc 符号;如果换成动态库函数,类型会变成R_X86_64_PLT32。
三、动态库的延迟绑定:GOT 与 PLT
静态库在链接期就把函数地址"写死",但动态库(.so)的函数地址到运行时才知道(因为 .so 加载地址不固定)。于是有了 GOT/PLT 机制:

延迟绑定(lazy binding)流程:
- 首次调用
printf:call printf@plt→ PLT 跳转到 GOT 条目 → GOT 里存的是"动态链接器地址" → 动态链接器解析出printf真实地址,回填 GOT。 - 之后调用:GOT 里已是真实地址,直接跳转,无额外开销。
用 readelf -s 看 PLT 符号、objdump -d 看 printf@plt,能直观看到这个两级跳转。详见 动态库加载与链接器。
四、为什么理解重定位对排查有用
| 现象 | 符号/重定位视角的解释 |
|---|---|
undefined reference to 'foo' | 链接器遍历符号表,U foo 没匹配到任何 T/D |
multiple definition of 'g' | 两个 .o 都有 D g(强符号重复定义) |
改 .so 后程序不用重编译 | 函数地址在 GOT 里运行时解析,ABI 不变即可替换 |
perf 里函数名来自哪 | 来自 .symtab(符号表)+ .debug_*(调试信息) |
五、与本站主线衔接
- L5 ELF:
.symtab/.strtab/重定位表的完整格式,见 ELF 与二进制基础。 - 编译四阶段:链接是第四阶段,见 编译、链接与 ELF。
- 动态库加载:
LD_PRELOAD拦截的原理就是 GOT 劫持,见 strace 与动态库。 - perf 符号:火焰图函数名解析依赖符号表,见 perf 使用指南。
一句话总结
符号表是"谁定义了谁引用了"的花名册(T/D/B/U),重定位表是"引用处地址待填"的清单(R_X86_64_PC32 等);链接器把 U 匹配到定义、按重定位类型填地址,动态库则靠 GOT/PLT 把解析推迟到运行期——看懂这套,链接报错和 perf 符号都不再神秘。