当前位置:首页>Linux>代码真的编译进 .so 了吗?用 Linux 三大“探针”工具把动态库扒个底朝天

代码真的编译进 .so 了吗?用 Linux 三大“探针”工具把动态库扒个底朝天

  • 2026-09-02 20:19:21
代码真的编译进 .so 了吗?用 Linux 三大“探针”工具把动态库扒个底朝天

一个让人抓狂的“玄学”问题

做嵌入式开发或 Linux 系统编程的同学,几乎都踩过这个坑:

  • 代码写了

  • make 通过了

  • .so 也生成了

  • 运行却报:undefined symbol

第一反应通常是怀疑人生:“我函数明明写了啊!”

这类问题,本质上不是玄学,而是动态链接与 ELF 符号机制的问题。只要掌握几个工具,就能把问题拆得清清楚楚。

今天我们围绕一个实际场景,系统讲透 Linux 下三大二进制分析工具:(还有一个strace,后期在分享吧)

  • nm

  • objdump

  • readelf

它们就是分析 .so 动态库的“三支探针”。


一、问题的本质:函数真的在动态库里吗?

当程序运行时报:

undefined symbol: xor_transform_32

常见原因有:

  1. 函数没有被编译进目标文件

  2. 函数被 static 修饰,未导出

  3. 被 visibility=hidden 隐藏

  4. 被 strip 掉

  5. 链接顺序错误

  6. 版本脚本屏蔽

  7. 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
在 text 段,全局函数
t
在 text 段,局部函数
U
未定义(引用外部)
B
BSS 段
D
data 段

重点看:

T

代表:

  • 在代码段

  • 全局可见

  • 已导出

如果看到:

U xor_transform_32

说明这个函数只是被引用了,并没有在这个 .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
global
F
function
.text
位于代码段
0x50
大小 80 字节

第二行:

g    DF .text  0000000000000050  Base

DF 表示:

  • D:动态符号

  • F:函数

说明这个符号不仅存在,而且被导入动态符号表。


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
字段
含义
132
函数大小(字节)
FUNC
类型
GLOBAL
全局
DEFAULT
可见性
12
所在 section
derive_raw_key
名称

DEFAULT 是关键

如果显示:

HIDDEN

说明:

__attribute__((visibility("hidden")))

外部无法访问。


五、动态符号表 vs 静态符号表

很多人搞不清这两个概念。

静态符号表 .symtab

  • 给调试器用

  • strip 后会消失

  • 不参与运行时链接

动态符号表 .dynsym

  • 给动态链接器 ld.so 用

  • 决定是否能被外部调用

  • 必须存在

验证方式:

readelf -S librzos.so.1

看是否存在:

.dynsym

六、真实排查流程

当程序报 undefined symbol 时,建议按以下顺序排查:


第一步:确认 .so 是否正确

ldd your_program

确保加载的是你期望的库。


第二步:确认符号存在

nm -D libxxx.so | grep func_name

没有 → 没导出有 → 继续


第三步:确认不是隐藏

readelf -s libxxx.so | grep func_name

查看 visibility。


第四步:检查是否 static

如果代码里写了:

static void xor_transform_32(...)

那它永远不会导出。


第五步:检查是否被 strip

file libxxx.so

如果 stripped,只影响调试符号,不影响动态符号。


第六步:C++ 名字问题

C++ 会产生符号重整:

nm -D libxxx.so | c++filt

七、进阶知识:函数为什么“消失”?

在现代编译器优化下,函数可能被:

  • inline

  • dead code elimination

  • LTO 优化合并

  • --gc-sections 删除

解决办法:

__attribute__((used))

或关闭:

-fdata-sections-ffunction-sections

八、三个工具如何配合?

工具
适合场景
nm
快速确认符号
objdump
查看属性 + 反汇编
readelf
精确 ELF 结构分析

实战建议:

  • 日常排查 → nm

  • 深入分析 → readelf

  • 研究底层 → objdump


九、从“猜测式调试”到“证据式分析”

很多工程师遇到 undefined symbol 时会:

  • 重编译

  • 清缓存

  • 重启机器

  • 怀疑系统

但真正的高手做法是:

用工具验证事实。

动态链接问题,本质上是 ELF 符号表问题。

只要你能读懂:

  • 符号是否存在

  • 是否 GLOBAL

  • 是否 DEFAULT

  • 是否在 .dynsym

那问题基本就解决了。


十、总结

当你怀疑:

“代码真的编译进 .so 了吗?”

不要猜。

直接用三把“探针”:

nm -Dobjdump -tTreadelf -s

它们会给你一个确定性的答案。

在嵌入式和系统开发领域,解决问题的能力,本质上是能否把黑盒变成透明。

最新文章

随机文章