链接器通过符号表和重定位信息找到函数和变量定义:先扫描所有.o文件建立全局符号哈希表,再对UNDEF符号查表绑定地址;若未找到则报undefined reference错误。

链接器怎么找到函数和变量的定义?
链接器本身不看源码,只处理 .o(目标文件)里的符号表和重定位信息。每个 .o 文件在编译后都包含三类关键内容:.text(机器码)、.data(已初始化数据)、.bss(未初始化数据),以及两个核心元数据:symbol table(符号表)和 .rela.text/.rela.data(重定位表)。
符号表里记录了所有 extern、global、static(带本地作用域的)符号,每条记录含名称、类型(FUNC/OBJECT)、绑定(GLOBAL/LOCAL)、大小、所在节区偏移。链接器第一遍扫描所有输入 .o,把 GLOBAL 符号按名称建哈希表;第二遍再扫,对每个 UNDEF 符号(比如调用的 printf 或未定义的 foo()),查这个表——查到就绑定地址,查不到就报 undefined reference 错误。
常见卡点:
-
static函数/变量不会进入全局符号表,所以其他.o文件无法引用——不是链接失败,是根本“看不见” - 函数声明但没定义(比如只写了
void bar();没实现),编译能过,链接必报undefined reference to 'bar' - C++ 的名字修饰(name mangling)会让
void foo(int)变成类似_Z3fooi的符号名;如果头文件声明和实现文件签名不一致(比如一个写const int&,另一个写int&),符号对不上,链接器就找不到定义
为什么调用 printf 时地址还是 0?
因为调用指令(如 x86-64 的 call)在目标文件里填的是占位地址——通常是 0x0 或某个预留值。这个位置被标记在 .rela.text 表中,记录了:要修改哪条指令(偏移)、改哪个字段(R_X86_64_PLT32 还是 R_X86_64_PC32)、要填谁的地址(符号名)。链接器在重定位阶段,根据符号最终地址和当前指令位置,算出相对偏移,写回机器码。
立即学习“C++免费学习笔记(深入)”;
典型重定位类型差异:
-
R_X86_64_PC32:用于直接调用同模块函数,填入「目标地址 - 当前指令下一条地址」的 32 位补码 -
R_X86_64_PLT32:用于调用外部函数(如printf),填入 PLT 表项地址,靠 PLT + GOT 间接跳转 -
R_X86_64_64:用于全局变量取地址(&g_var),直接填绝对地址——这会导致代码不可重定位(PIE 禁用)
如果你看到 relocation truncated to fit 错误,大概率是用了 R_X86_64_64 去填一个本该用 PC-relative 的地方,或者符号地址超出了 32 位表示范围。
多个同名 global 符号会怎样?
链接器默认按“强弱符号”规则处理:函数和已初始化变量是强符号,未初始化变量(int g;)是弱符号。规则是:多个强符号 → 链接错误;一个强 + 多个弱 → 用强的;多个弱 → 任选一个(通常第一个)。
这导致几个经典陷阱:
- 头文件里写
int global = 42;并被多个.cpp包含 → 每个.o都生成一个强符号 → 链接时报multiple definition - 想用弱符号做“可覆盖默认实现”,得显式加
__attribute__((weak)),否则普通static或未初始化变量不满足预期行为 -
inline函数在多个单元定义,靠编译器保证符号弱化或内联消除;但若编译器没内联,又没加inline声明,也会触发多重定义
如何用工具看符号和重定位细节?
别猜,直接用系统工具看真实数据:
nm -C main.o # 查看符号(-C 解析 C++ 名字) readelf -s lib.o # 更详细的符号表(含绑定、类型) readelf -r main.o # 查看重定位入口 objdump -d main.o # 反汇编,看 call 指令后跟着的占位地址
特别注意 nm 输出第一列:大写 T 是强定义函数,小写 t 是局部函数;U 是未定义,B/b 是 bss 段变量。如果发现本该定义的符号显示为 U,说明编译时没把它打进去——可能文件根本没参与链接,或者被 static 封装了。
真正容易被忽略的是:链接器不验证类型一致性。它只认名字。哪怕 void f() 和 int f() 在不同文件里定义,只要名字一样,链接器就强行连上,运行时崩在栈错位或返回值解释错误——这种问题必须靠编译期检查(如头文件统一声明)来防,链接器不管。











