2、符号解析:符号表的概念、强符号与弱符号、多重定义规则

好,咱们接着聊链接。上一章我们说了链接器怎么把目标文件拼在一起。但拼的时候,最怕遇到什么?

没错,就是符号冲突。

你想想看,两个不同的 .c 文件里,都定义了一个叫 global_var 的全局变量。链接器该用哪个?报错?还是默默选一个?

这背后,就是符号解析的规则在起作用。我个人觉得,搞懂这块,才算真正入了链接的门。

2.1 符号表:链接器的“通讯录”

每个目标文件(.o 文件)里,都藏着一张表,叫符号表。

说白了,它就是一份清单。记录了当前模块里,哪些符号是“我提供的”,哪些是“我需要别人给我的”。

链接器拿到所有 .o 文件后,第一件事就是翻看每个人的通讯录。看看谁缺什么,谁能补什么。

符号表里主要记三类东西:

  • 全局符号:本模块定义,且可以被其他模块引用的。比如非 static 的函数和全局变量。
  • 外部符号:本模块引用,但定义在其他模块的。比如你调用了 printf,但自己没写。
  • 局部符号:带 static 的变量和函数。只在当前模块可见,链接器基本不管它。

我记得刚入行时,遇到一个诡异的链接错误。明明函数名写对了,头文件也包含了,就是报“未定义引用”。折腾半天,发现是 .c 文件里把函数定义成了 static。嗯,局部符号嘛,别人当然看不见。

2.2 强符号与弱符号:谁说了算?

链接器在解析符号时,会给每个符号贴个标签:强符号 或 弱符号。

规则很简单:

  • 函数和已初始化的全局变量,默认是强符号。
  • 未初始化的全局变量,默认是弱符号。

你可以用 __attribute__((weak)) 手动把一个符号声明为弱符号。这在做库的时候特别有用。

一个小技巧:

我习惯在写底层驱动库时,把默认的中断处理函数声明为弱符号。这样用户如果自己实现了同名函数,就会覆盖我的默认实现。链接器不会报错,因为弱符号可以被强符号覆盖。

2.3 多重定义规则:冲突怎么解?

好,现在到了最核心的部分。如果多个目标文件里,定义了同名的全局符号,链接器怎么处理?

规则就三条,我总结一下:

  1. 强符号不允许重复定义。 如果两个 .o 文件都定义了同一个强符号,链接器直接报错:multiple definition of ...
  2. 一个强符号可以覆盖多个弱符号。 链接器会选强符号,忽略所有弱符号。
  3. 如果全是弱符号,选占用空间最大的那个。 嗯,你没看错,是选最大的。

为什么会这样?你想想看,弱符号通常意味着“不确定”,链接器就赌一把,选个最大的,至少不会越界。

注意:

我曾经踩过一个坑。两个 .c 文件里,都定义了一个未初始化的全局数组。一个声明为 int buf[100],另一个声明为 int buf[1000]。链接器默默选了 1000 的那个。结果代码里按 100 的尺寸操作,直接写越界了。这种 bug 极难定位,因为编译链接都不报错。

2.4 实战:用代码验证规则

光说不练假把式。咱们写几行代码看看。

/* file1.c */
#include <stdio.h>

int global_var;          // 弱符号
int strong_func() {     // 强符号
    return 1;
}

int main() {
    printf("global_var = %d\n", global_var);
    printf("strong_func = %d\n", strong_func());
    return 0;
}
/* file2.c */
int global_var = 100;    // 强符号(已初始化)
int strong_func() {      // 强符号,会报错
    return 2;
}

编译试试:

$ gcc file1.c file2.c
/tmp/ccXXXX.o: multiple definition of `strong_func'
/tmp/ccYYYY.o: first defined here
collect2: error: ld returned 1 exit status

看到了吧?strong_func 两个都是强符号,直接报错。

把 file2.c 里的 strong_func 注释掉,再编译:

$ gcc file1.c file2.c
$ ./a.out
global_var = 100
strong_func = 1

这次 global_var 是强符号(100)覆盖了弱符号(未初始化),所以输出 100。而 strong_func 只有一个,正常调用。

2.5 知识体系总览

为了让你看得更清楚,我画了一张图,把符号解析的核心逻辑串起来:

符号解析核心逻辑 目标文件 (.o) 符号表 全局符号 外部符号 局部符号 (static) 强符号 / 弱符号 多重定义规则 ① 强符号不能重复 → 报错 ② 强符号覆盖弱符号 → 选强 ③ 全弱符号 → 选占用空间最大的

这张图把整个流程串起来了。从 .o 文件出发,提取符号表,区分全局/外部/局部,再判断强弱,最后按多重定义规则裁决。

2.6 避坑指南

最后,分享几个我踩过的坑:

  • 别依赖弱符号的“选最大”规则。 不同编译器行为可能不同,而且容易引发内存越界。最好显式初始化所有全局变量。
  • 链接顺序有时会影响结果。 虽然强弱规则是确定的,但有些链接器在处理弱符号时,会受输入文件顺序影响。我建议把库文件放在最后。
  • 用 nm 命令查看符号表。 遇到链接问题,先跑 nm your.o,看看符号是 T(强)还是 t(局部),还是 U(未定义)。这招帮我省了不少时间。

一句话总结:

符号解析就是链接器在“拉郎配”。强符号是“正室”,弱符号是“备胎”。备胎再多,也干不过正室。但如果全是备胎,那就选个最壮的。

嗯,符号解析这块,说白了就是这些规则。搞懂了,链接错误你一眼就能看穿。下一章我们聊聊更刺激的——静态库和动态库的链接过程。

可可精品资料库