一个让人抓狂的“玄学”问题
做嵌入式开发或 Linux 系统编程的同学,几乎都踩过这个坑:
代码写了
make 通过了
.so 也生成了
运行却报:undefined symbol
第一反应通常是怀疑人生:“我函数明明写了啊!”
这类问题,本质上不是玄学,而是动态链接与 ELF 符号机制的问题。只要掌握几个工具,就能把问题拆得清清楚楚。
今天我们围绕一个实际场景,系统讲透 Linux 下三大二进制分析工具:(还有一个strace,后期在分享吧)
它们就是分析 .so 动态库的“三支探针”。
一、问题的本质:函数真的在动态库里吗?
当程序运行时报:
undefined symbol: xor_transform_32
常见原因有:
函数没有被编译进目标文件
函数被 static 修饰,未导出
被 visibility=hidden 隐藏
被 strip 掉
链接顺序错误
版本脚本屏蔽
C++ 名字被 mangling
所以第一步不是改代码,而是确认一件事:
这个函数是否真实存在于 .so 的动态符号表中?
这时候就要上工具。
二、nm:最快速的“点名册”
如果你只想确认函数在不在,nm 是最直接的。
实战命令
nm -D librzos.so.1 | grep -E "derive_raw_key|bin_to_hex|xor_transform_32"
输出示例:
000000000004b7c0 T bin_to_hex000000000004b6e0 T derive_raw_key000000000004b770 T xor_transform_32
关键知识点解析
1️⃣ -D 是关键
nm 默认查看静态符号表
-D 表示查看 动态符号表(.dynsym)
如果你忘了 -D,可能会误判。
2️⃣ 字母代表什么?
输出第二列是符号类型:
重点看:
T
代表:
如果看到:
说明这个函数只是被引用了,并没有在这个 .so 里实现。
nm 的核心价值
它回答一个最基础的问题:
这个符号是否存在?是导出还是引用?
这是排查问题的第一步。
三、objdump:更底层的“透视镜”
如果你想知道:
它是不是函数?
它大小是多少?
是否动态可见?
属于哪个段?
那就上 objdump。
实战命令
objdump -tT librzos.so.1 | grep xor_transform_32
输出:
000000000004b770 g F .text 0000000000000050 xor_transform_32000000000004b770 g DF .text 0000000000000050 Base xor_transform_32
逐字段深度解析
第一行:
000000000004b770 g F .text 0000000000000050 xor_transform_32
第二行:
g DF .text 0000000000000050 Base
DF 表示:
说明这个符号不仅存在,而且被导入动态符号表。
objdump 的优势
相比 nm:
信息更完整
可区分静态符号和动态符号
能看函数大小
可反汇编
例如:
objdump -d librzos.so.1 | less
可以直接看到机器指令。
四、readelf:最权威的 ELF 读取工具
readelf 是真正“直读 ELF 结构”的工具。
它不依赖 BFD,比 objdump 更原始、更精确。
实战命令
readelf -s librzos.so.1 | grep -E "derive_raw_key|bin_to_hex|xor_transform_32"
输出:
373: 000000000004b6e0 132 FUNC GLOBAL DEFAULT 12 derive_raw_key533: 000000000004b770 80 FUNC GLOBAL DEFAULT 12 xor_transform_32
字段结构详解
Num: Value Size Type Bind Vis Ndx Name
以下面为例解释一下:
132 FUNC GLOBAL DEFAULT 12 derive_raw_key
DEFAULT 是关键
如果显示:
HIDDEN
说明:
__attribute__((visibility("hidden")))
外部无法访问。
五、动态符号表 vs 静态符号表
很多人搞不清这两个概念。
静态符号表 .symtab
动态符号表 .dynsym
给动态链接器 ld.so 用
决定是否能被外部调用
必须存在
验证方式:
看是否存在:
六、真实排查流程
当程序报 undefined symbol 时,建议按以下顺序排查:
第一步:确认 .so 是否正确
确保加载的是你期望的库。
第二步:确认符号存在
nm -D libxxx.so | grep func_name
没有 → 没导出有 → 继续
第三步:确认不是隐藏
readelf -s libxxx.so | grep func_name
查看 visibility。
第四步:检查是否 static
如果代码里写了:
static void xor_transform_32(...)
那它永远不会导出。
第五步:检查是否被 strip
如果 stripped,只影响调试符号,不影响动态符号。
第六步:C++ 名字问题
C++ 会产生符号重整:
nm -D libxxx.so | c++filt
七、进阶知识:函数为什么“消失”?
在现代编译器优化下,函数可能被:
inline
dead code elimination
LTO 优化合并
--gc-sections 删除
解决办法:
或关闭:
-fdata-sections-ffunction-sections
八、三个工具如何配合?
实战建议:
日常排查 → nm
深入分析 → readelf
研究底层 → objdump
九、从“猜测式调试”到“证据式分析”
很多工程师遇到 undefined symbol 时会:
但真正的高手做法是:
用工具验证事实。
动态链接问题,本质上是 ELF 符号表问题。
只要你能读懂:
符号是否存在
是否 GLOBAL
是否 DEFAULT
是否在 .dynsym
那问题基本就解决了。
十、总结
当你怀疑:
“代码真的编译进 .so 了吗?”
不要猜。
直接用三把“探针”:
nm -Dobjdump -tTreadelf -s
它们会给你一个确定性的答案。
在嵌入式和系统开发领域,解决问题的能力,本质上是能否把黑盒变成透明。