动态库符号系统梳理

涉及核心:ELF文件结构、符号表、动态链接、可见性、强弱符号、未定义符号、ABI影响。

.so 本质是什么

.so是ELF格式 共享目标文件(Shared Object)

root@ser745692301841:/dev_dir/avant/build/external# file libavant-lua.so
libavant-lua.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=397134715c11e89b57a0138292983920f68ccbb5, with debug_info, not stripped

可以看到 ELF 64-bit LSB shared object , 它本质和可执行文件差不多,只是

符号 Symbol 是什么

在ELF里,符号就是:一个“名字”对应一个地址

包括:函数、全局变量、静态对象、vtable、模板实例

符号存放在:

重点是 .dynsym

root@ser745692301841:/dev_dir/avant/build/external# nm -D libavant-log.so
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 U _Unwind_Resume@GCC_3.0
000000000001711e T _ZN5avant3log6logger15rotate_log_fileEl
0000000000016f62 T _ZN5avant3log6logger4openERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEi
00000000000170c2 T _ZN5avant3log6logger5closeEv
0000000000032f80 D _ZN5avant3log6logger6s_flagE
0000000000016e4a T _ZN5avant3log6loggerC1Ev
0000000000016e4a T _ZN5avant3log6loggerC2Ev
0000000000016f08 T _ZN5avant3log6loggerD1Ev
0000000000016f08 T _ZN5avant3log6loggerD2Ev
0000000000017ffb W _ZN9__gnu_cxx11char_traitsIcE2eqERKcS3_
0000000000018e9a W _ZN9__gnu_cxx11char_traitsIcE4copyEPcPKcm
000000000001879e W _ZN9__gnu_cxx11char_traitsIcE4findEPKcmRS2_
0000000000018e32 W _ZN9__gnu_cxx11char_traitsIcE4moveEPcPKcm
0000000000018f2c W _ZN9__gnu_cxx11char_traitsIcE6assignEPcmc
0000000000017f8e W _ZN9__gnu_cxx11char_traitsIcE6lengthEPKc
000000000001d8e2 W _ZN9__gnu_cxx17__normal_iteratorIPcSt4spanIcLm18446744073709551615EEEC1ERKS1_
000000000001d8e2 W _ZN9__gnu_cxx17__normal_iteratorIPcSt4spanIcLm18446744073709551615EEEC2ERKS1_
00000000000285e6 W _ZN9__gnu_cxx17__normal_iteratorIPcSt4spanIcLm18446744073709551615EEEpLEl
0000000000029b5e W _ZN9__gnu_cxx17__normal_iteratorIPcSt4spanIcLm18446744073709551615EEEppEi
000000000001cc78 W _ZN9__gnu_cxxmiIPcSt4spanIcLm18446744073709551615EEEENS_17__normal_iteratorIT_T0_E15difference_typeERKS7_SA_
000000000001d904 W _ZNK9__gnu_cxx17__normal_iteratorIPcSt4spanIcLm18446744073709551615EEE4baseEv
0000000000029bc4 W _ZNK9__gnu_cxx17__normal_iteratorIPcSt4spanIcLm18446744073709551615EEEdeEv
                 U _ZNKSt13runtime_error4whatEv@GLIBCXX_3.4
000000000001c35c W _ZNKSt16reverse_iteratorIPKNSt8__format6_Arg_tEE4baseEv
000000000001c166 W _ZNKSt16reverse_iteratorIPKNSt8__format6_Arg_tEEdeEv
0000000000026588 W _ZNKSt17basic_format_argsISt20basic_format_contextINSt8__format10_Sink_iterIcEEcEE7_M_typeEm
000000000002601e W _ZNKSt17basic_string_viewIcSt11char_traitsIcEE17find_first_not_ofEcm
0000000000018102 W _ZNKSt17basic_string_viewIcSt11char_traitsIcEE3endEv
0000000000028528 W _ZNKSt17basic_string_viewIcSt11char_traitsIcEE4copyEPcmm
0000000000018fd8 W _ZNKSt17basic_string_viewIcSt11char_traitsIcEE4dataEv
000000000001871a W _ZNKSt17basic_string_viewIcSt11char_traitsIcEE4findEcm
......

符号的三种状态

已定义符号(Defined Symbol)

比如

int add(int a, int b) { return a + b; }

nm显示

T add

含义:

未定义符号(Undefined Symbol)

比如

extern int foo();

但库里没实现

nm 显示:

U foo

意思:我需要foo,但我自己没实现,等别的库提供。

如果运行时找不到:

undefined symbol: foo

程序直接崩。

本地符号(Local Symbol)

static 函数:

static void helper() {}

不会出现在动态符号表里,因为它不可见。

符号可见性

.so的重点,默认情况下

int add();

在GCC下是 default visibility。

你可以控制:

__attribute__((visibility("default")))

或者

__attribute__((visibility("hidden")))

编译选项:

-fvisibility=hidden

这是工程级实践:默认隐藏,只导出API

否则你的库会把一堆内部符号暴露出去。

强符号与弱符号

nm里会看到:

T 强符号
W 弱符号

规则:

弱符号常用于:

符号解析流程

动态链接时发生什么

程序启动时:

  1. loader读取ELF
  2. 查看 .dynsym
  3. 遍历依赖库
  4. 解析未定义符号
  5. 填充 GOT 、PLT 表

如果找不到

symbol lookup error

你就开始怀疑人生。

常见错误类型

编译时报错

undefined reference to xxx

链接阶段找不到实现

原因:

运行时报错

undefined symbol: xxx

编译时找到了,运行时loader找不到

原因:

C++ 特有的坑:名字修饰

C++:

int add(int, int);

实际符号可能是:

_Z3addii

如果C调用C++,C++应该

extern "C" int add(int, int)

否则符号对不上。

ABI 兼容问题

.so 一旦发布:

否则:表面链接成功、运行直接炸

这就是为什么生产环境里:API稳定性比功能还重要。

工程级建议

如果你写库:

  1. 默认 -fvisibility=hidden
  2. 只导出公共API
  3. extern "C" 控制ABI
  4. 提供版本号(soname)
  5. 不要头文件里暴露STL类型

关键命令清单

查看动态符号

name -D libxxx.so

只看已定义

nm -D --defined-only libxxx.so

只看未定义

nm -u libxxx.so

查看依赖

ldd libxxx.so

查看ELF详细信息

readelf -Ws libxxx.so

总结结构图

.so
 ├── .text
 ├── .data
 ├── .bss
 ├── .symtab
 └── .dynsym
        ├── Defined
        ├── Undefined
        ├── Weak
        └── Hidden

写 .so 本质不是“编译出文件”,而是 设计一个稳定的二进制接口。