3、链接器的工作机制:符号解析与重定位,强符号与弱符号,多重定义规则,COMMON块

大家好,我是你们的老朋友。今天咱们来聊聊链接器。

说实话,很多C/C++工程师写了好几年代码,对编译器的脾气摸得门儿清,但一碰到链接错误就抓瞎。尤其是那种“多重定义”或者“符号未定义”的报错,看着就头大。我当年刚入行时,也被这玩意儿折磨得不轻。

链接器到底在干什么?说白了,它就是个“拼图工”。编译器把每个.c文件变成一个个目标文件(.o),链接器负责把这些碎片拼成完整的可执行文件。拼图过程中,有两件事必须干好:符号解析和重定位。

符号解析:找名字

每个目标文件里都有一张“符号表”,记录着它定义了什么、引用了什么。比如你写了个int global_var;,这就是一个符号定义。你在另一个文件里写了extern int global_var;,这就是一个符号引用。

链接器的工作之一,就是遍历所有目标文件,把每个引用都匹配到对应的定义上。如果找不到定义,就报“未定义符号”错误。如果找到多个定义,那就得看下面的规则了。

核心要点:符号解析的本质就是“对号入座”。每个符号引用必须且只能对应一个符号定义。

强符号与弱符号

这里有个概念很多人容易搞混——强符号和弱符号。

简单说:

  • 强符号:函数名、已初始化的全局变量名。比如 int x = 5; 里的 x。
  • 弱符号:未初始化的全局变量名。比如 int y; 里的 y。

为什么这么分?因为C语言有个历史包袱——暂定定义(tentative definition)。你写个 int y; 在文件作用域,编译器不知道你到底是定义还是声明,就先把它当成“弱符号”放着。

我的经验:我在项目中遇到过好几次这样的bug:两个.c文件里都写了 int counter;,结果链接器不报错,但运行时counter的值莫名其妙被互相覆盖。这就是弱符号惹的祸。

多重定义规则

当多个目标文件中出现同名符号时,链接器按以下规则处理:

情况 规则 示例
多个强符号 报错:多重定义 int x=1; 在两个文件中
一个强符号 + 多个弱符号 选择强符号 int x=1; 和 int x;
多个弱符号 选择占用空间最大的那个 int x; 和 double x;

嗯,这里要注意第三点。多个弱符号时,链接器会挑“块头最大”的那个。为什么?因为弱符号最终会被分配到COMMON块中,链接器需要知道分配多大的空间才够用。

警告:我曾经见过一个线上崩溃,就是因为两个.c文件里都声明了 struct { int a; char b[256]; } data; 和 struct { int a; char b[64]; } data;,结果链接器选了256字节的那个,但其中一个文件按64字节来读写,直接踩内存了。

COMMON块:弱符号的“收容所”

COMMON块是链接器内部的一个机制。所有弱符号都会被放进COMMON块里,链接器在最后阶段才决定它们的最终地址和大小。

为什么要搞这么复杂?因为早期的Fortran和C语言混编时,不同文件对同一个全局变量的声明可能不一致。COMMON块给了链接器一个“容错”的机会——先放着,最后再统一分配。

但这也带来了隐患。你想想看,如果两个文件对同一个变量的类型声明不一致,链接器不会报错,只会默默选一个最大的。运行时行为就完全不可预测了。

避坑指南:我个人习惯是,所有全局变量都显式初始化。哪怕初始化为0,也写成 int x = 0;。这样它就变成强符号了,链接器会帮你检查多重定义问题。

重定位:填地址

符号解析搞定后,链接器开始重定位。这一步说白了就是:把每个目标文件里的“占位符”替换成真正的内存地址。

比如你代码里调用了 foo(),编译时编译器不知道 foo 最终在哪个地址,就留个空位。链接器解析完所有符号后,知道了 foo 的地址,就把这个空位填上。

重定位分为两种:

  • 相对重定位:基于当前指令位置的偏移量。比如函数调用。
  • 绝对重定位:直接使用绝对地址。比如全局变量的访问。

这里有个细节:链接器会生成一个重定位表,记录每个需要修改的位置。你可以用 objdump -r 查看目标文件的重定位条目。

一张图看懂链接器的工作流程

下面我用SVG画个流程图,把整个流程串起来:

链接器工作流程 多个目标文件 (.o) 步骤1:符号解析 匹配符号引用与定义,处理强弱符号 有冲突? 是 报错:多重定义 否 步骤2:重定位 合并段,填充真实地址 可执行文件

总结一下

链接器的工作,说白了就是三步:

  1. 符号解析:把每个符号引用对上号,处理强弱符号冲突。
  2. COMMON块处理:弱符号先放一边,最后统一分配。
  3. 重定位:把占位符换成真实地址。

记住一个原则:能用强符号就别用弱符号。显式初始化全局变量,能帮你避免很多莫名其妙的链接问题。

好了,这一章就到这里。下一章咱们聊聊静态库和动态库的区别,以及链接时那些让人抓狂的“未定义引用”到底是怎么回事。

小技巧:遇到链接错误时,先用 nm 或 objdump 查看目标文件的符号表,看看符号是强是弱,定义在哪个文件里。这招我用了十年,屡试不爽。

可可精品资料库