静态库符号系统梳理

什么是符号

在ELF世界里,“符号”就是编译器和链接器用来定位东西的名字标签。

它可以是:

本质:

符号 = 名字 + 地址 + 类型 + 绑定属性

从源码到静态库发生了什么

流程

.c -> gcc -c -> .o -> ar -> .a -> ld -> 可执行文件

.o 目标文件

ELF结构里有:

用:

nm file.o
readelf -s file.o
objdump -t file.o

.a 静态库

静态库本质就是:一堆 .o 的打包集合(不是合并)

命令

ar rcs libxxx.a a.o b.o c.o

可以

at t libxxx.a

查看里面有哪些 .o 。

符号类型分类

看 nm 输出

00000000 T func
         U printf
00000000 D global_var
00000000 B uninit_var
字母 含义
T 在 text 段的已定义函数
U 未定义符号(需要外部解析)
D 已初始化全局变量
B 未初始化全局变量
W 弱符号
t 局部函数
d 局部数据

强符号与弱符号

这是静态库最容易踩的坑。

强符号(Strong)

弱符号(Weak)

__attribute__((weak))

规则:

  1. 强 > 弱
  2. 强 + 强 -> 链接报错
  3. 弱 + 弱 -> 选一个
  4. 强 + 弱 -> 选强

这东西在 libc 里大量使用。

静态库的“按需链接”机制

链接器不是把整个 .a 拿进来

它是:只把需要的 .o 拿进来。

举例:

lib.a
  a.o  (提供 foo)
  b.o  (提供 bar)

如果你程序只用 foo:

这带来一个致命规则:链接顺序重要!

静态库是“单向扫描、按需装载”的。

链接器的行为是:

  1. 从左到右扫描命令行参数
  2. 遇到目标文件(.o)时,把里面的未定义符号记录下来
  3. 遇到静态库(.a)时:

重点在于:它不会回头。链接器没有记忆力。它只活在当下。

动态库(.so)是整体加入,不是按 .o 拆分。它不会按需拆 .o。 所以顺序影响没那么大。

符号解析流程

链接器工作流程:

  1. 读入第一个目标文件
  2. 建立未解析符号列表
  3. 遇到库
  4. 重复直到没有未解析符号

静态库不能循环依赖。

解决方式

gcc a.o -lfoo -lbar -lfoo
# 或者
-Wl,--start-group ... --end-group

C++名字修饰

C++符号会被 mangling

void foo(int)

变成

_Z3fooi

取消修饰

extern "C" void foo(int)

静态库与重定位

.o 里函数地址不是最终地址。

它是,可重定位符号 + relocation表。

链接时:

这一步叫:符号解析 + 重定位。

常见坑总结

undefined reference

multiple definition

静态变量符号不可见

static int x;

变成局部符号,外部访问不到。

实战调试工具链

遇到问题,按这个流程:

nm -C xxx.o
readelf -s xxx.o
ar t libxxx.a
nm -C libxxx.a
ldd (动态库用)
objdump -d

静态库符号系统的本质

静态库:一堆可重定位目标文件

链接器:符号匹配器 + 重定位修补器

符号:编译世界里的身份证