7、符号与符号表:全局符号、局部符号、外部符号,强符号与弱符号,符号表结构
好,咱们今天聊点实在的——符号与符号表。
说实话,很多C语言开发者写了好几年代码,对“符号”这个概念还是模模糊糊的。你问他编译链接到底干了啥,他大概能说出“把多个.o文件拼在一起”。但具体怎么拼的?拼的时候遇到同名函数会怎样?为什么有时候报“multiple definition”有时候又不报?
嗯,这些问题的答案,全藏在符号表里。
7.1 符号到底是什么?
简单说,符号就是名字。函数名、全局变量名、静态变量名……这些都是符号。编译器在编译每个.c文件时,会生成一个符号表,记录这个文件里定义了哪些符号、引用了哪些符号。
我打个比方:每个.c文件就像一个小作坊,符号表就是作坊的“货物清单”。清单上写着:
- 我自己生产了啥(定义的符号)
- 我需要从外面买啥(引用的外部符号)
链接器的工作,就是把这些小作坊的清单合并,确保“需要买的”能从别的作坊那里“买到”。
核心概念:符号是链接器工作的基本单元。没有符号表,链接器就是个瞎子。
7.2 符号的三种“户口”
根据符号的作用域和可见性,我把它们分成三类。你在项目里肯定都见过:
| 符号类型 | 定义位置 | 可见范围 | 典型例子 |
|---|---|---|---|
| 全局符号 | 函数外定义,不加static | 整个程序 | int g_val = 10; |
| 局部符号 | 加static修饰 | 本文件内 | static int helper() |
| 外部符号 | 用extern声明 | 引用其他文件的符号 | extern int g_val; |
这里有个坑,我当年刚入行时踩过:全局变量默认就是外部可见的。你写个 int a; 在文件顶部,别的文件就能通过 extern int a; 访问它。如果你不想这样,记得加 static。
我的习惯:除非明确需要跨文件访问,否则全局变量一律加static。这能避免很多莫名其妙的链接错误。
7.3 强符号与弱符号——链接器的“裁判规则”
好,重点来了。为什么有时候两个文件定义了同名函数,链接器会报错?有时候又不报?
这就要说到强符号和弱符号了。
- 强符号:函数定义、已初始化的全局变量。比如
int x = 5; - 弱符号:未初始化的全局变量。比如
int x;(注意,没赋值)
链接器处理同名符号时,遵循三条铁律:
- 同名的强符号只能有一个。多了就报“multiple definition”。
- 一个强符号可以搭配多个弱符号。链接器选强符号。
- 多个弱符号同名时,选占用空间最大的那个。嗯,这个有点玄学,但标准就是这么定的。
我曾经在一个嵌入式项目里遇到过:两个.c文件都定义了 int buffer[256]; 和 int buffer[512];,结果链接器选了512的那个,导致另一个文件访问越界。查了两天才找到原因……
避坑指南:永远不要依赖弱符号的“自动选择”机制。如果你需要覆盖符号,用 __attribute__((weak)) 显式声明,别靠运气。
7.4 符号表长什么样?
咱们用 readelf -s 或者 nm 命令,就能看到目标文件里的符号表。我随便拿个例子:
// file: test.c
int global_var = 42;
static int static_var = 10;
void func(void) {
static int local_static = 0;
int auto_var = 5;
}
编译后用 nm test.o 查看:
0000000000000000 D global_var
0000000000000004 d static_var
0000000000000000 T func
0000000000000004 d local_static.0
看到没?每个符号都有:
- 地址:在目标文件中的偏移(链接前是相对地址)
- 类型:D表示已初始化的全局数据,T表示代码段,d表示局部静态数据
- 名字:符号名称
链接器拿到所有目标文件的符号表后,会做两件事:
1. 解析引用:找到每个外部符号的定义
2. 重定位:把符号的地址改成最终的可执行地址
7.5 符号表内部结构
如果你看过ELF文件格式,就知道符号表其实是一个结构体数组。每个条目大概长这样:
typedef struct {
uint32_t st_name; // 符号名字在字符串表中的索引
unsigned char st_info; // 符号类型和绑定信息
unsigned char st_other; // 可见性
uint16_t st_shndx; // 所在段编号
uint64_t st_value; // 符号值(地址或偏移)
uint64_t st_size; // 符号大小
} Elf64_Sym;
这里 st_info 字段很关键,它编码了:
- 绑定信息(STB_*):全局(STB_GLOBAL)、局部(STB_LOCAL)、弱符号(STB_WEAK)
- 类型(STT_*):函数(STT_FUNC)、对象(STT_OBJECT)、节(STT_SECTION)
链接器就是靠这些信息,来决定符号的可见性和冲突处理规则。
一句话总结:符号表是链接器的“地图”。地图画得清不清楚,直接决定了链接能不能成功。
7.6 一张图看懂符号与符号表
下面这张SVG图,把整个流程串起来了:
这张图展示了一个典型的链接冲突场景。a.c和c.c都定义了强符号 g,链接器直接报错。而b.c里的弱符号 g 根本没机会上场。
7.7 实战建议
最后,分享几个我多年积累的经验:
- 用
nm排查链接问题:当遇到“undefined reference”或“multiple definition”时,先用nm看看目标文件里的符号表,问题往往一目了然。 - 慎用全局变量:全局变量默认是强符号,容易引发冲突。我建议用
static限制作用域,或者用getter/setter函数封装。 - 弱符号的妙用:在库中定义弱符号,允许用户覆盖。比如
__attribute__((weak)) int default_hook() { return 0; },用户如果定义了同名强符号,就会覆盖库的默认实现。 - 检查符号可见性:用
readelf -s查看符号的绑定属性,确认哪些是全局的、哪些是局部的。
一个小技巧:在编译时加 -fvisibility=hidden,然后显式用 __attribute__((visibility("default"))) 导出需要的符号。这样能有效控制符号的暴露范围,减少链接冲突。
符号表这东西,说深了可以写一本书。但咱们做工程的人,掌握上面这些就够用了。记住:符号是链接的基石,理解符号表,你就理解了链接器的一半。
公众号:蓝海资料掘金营,微信deep3321