3. 依赖分析工具
做驱动集成,说白了就是跟各种 .ko 和 .so 打交道。你编译好了,insmod 或者加载动态库时,系统突然告诉你「找不到符号」或者「版本不匹配」——这种场景我遇到过太多次了。这时候,手里没几把趁手的分析工具,就像瞎子摸象。
我个人习惯,在集成阶段一定会跑一遍依赖分析。今天聊的这四个工具:ldd、objdump、nm、readelf,是我日常排查依赖问题的「四件套」。每个工具侧重点不同,但组合起来,基本能覆盖 90% 的依赖场景。
3.1 ldd:快速查看动态链接依赖
ldd 全称是 List Dynamic Dependencies。它的用法极其简单,但输出信息量很大。
# 查看一个 .so 或可执行文件的动态依赖
ldd /usr/lib/libexample.so
输出大概长这样:
linux-vdso.so.1 (0x00007ffe3b7f0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8a1c200000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8a1c600000)
每一行左边是「需要的库名」,右边是「实际解析到的路径」。如果某个库找不到,会显示 not found。嗯,这里要注意:ldd 实际上会尝试加载这些库,所以如果你在交叉编译环境下用 ldd 分析 ARM 的二进制,结果可能不准。我建议用 readelf -d 替代。
关键点:ldd 显示的是「运行时」的依赖解析结果,不是编译时的。如果某个库路径不对,ldd 会直接告诉你。
我的经验:有一次客户反馈驱动加载失败,我远程一看,ldd 输出里有个 libssl.so.1.1 => not found。系统里装的是 1.0 版本,版本号差一位,折腾了半天。后来我养成了习惯:集成前先跑一遍 ldd,把 not found 全部消灭掉。
3.2 objdump:查看符号表和重定位信息
objdump 是个大杀器,功能非常多。在依赖分析场景,我主要用它看两个东西:动态符号表和重定位表。
3.2.1 查看动态符号表
objdump -T libexample.so
输出示例:
libexample.so: file format elf64-x86-64
DYNAMIC SYMBOL TABLE:
0000000000002000 g DF .text 0000000000000010 Base example_func
0000000000002010 g DF .text 0000000000000020 Base init_device
0000000000000000 D *UND* 0000000000000000 printf
注意看 *UND* 那一行。这表示 printf 是「未定义符号」,需要从其他库(比如 libc)中解析。说白了,这就是你的依赖缺口。
3.2.2 查看重定位表
objdump -R libexample.so
输出:
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000003000 R_X86_64_JUMP_SLOT printf
0000000000003008 R_X86_64_JUMP_SLOT malloc
重定位表告诉你:运行时需要把 printf 和 malloc 的实际地址填到这些偏移位置。如果这些符号在依赖库里找不到,加载就会失败。
我曾经踩过的坑:有一次编译内核模块,objdump -T 看到一堆 *UND* 符号,我以为没问题,结果 insmod 时直接报 Unknown symbol。后来发现是模块依赖的另一个 .ko 没加载。记住:*UND* 符号必须被其他已加载模块或库提供,否则就是运行时错误。
3.3 nm:列出符号表
nm 比 objdump 更轻量,专门用来列出目标文件中的符号。它的输出格式很直观。
nm -D libexample.so
输出:
0000000000002000 T example_func
0000000000002010 T init_device
U printf
U malloc
符号类型字母的含义:
| 符号类型 | 含义 | 说明 |
|---|---|---|
| T | Text(代码段) | 该符号定义在当前文件中,且是全局的 |
| U | Undefined(未定义) | 该符号需要从外部解析,是依赖项 |
| D | Data(数据段) | 已初始化的全局变量 |
| W | Weak(弱符号) | 弱定义,可以被其他强符号覆盖 |
实用技巧:用 nm -u 只显示未定义符号,快速定位依赖缺失。比如:nm -u *.ko | grep -v "0" 可以列出所有模块的未解析符号。
3.4 readelf:ELF 文件的瑞士军刀
readelf 是我个人最常用的工具。它不依赖系统的动态链接器,所以交叉编译环境下也能用。而且它显示的信息比 objdump 更底层、更完整。
3.4.1 查看动态段依赖
readelf -d libexample.so | grep NEEDED
输出:
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x0000000000000001 (NEEDED) Shared library: [libm.so.6]
这比 ldd 更可靠,因为它直接从 ELF 文件的 .dynamic 段读取依赖信息,不会去实际加载库。
3.4.2 查看符号版本信息
readelf -V libexample.so
输出:
Version symbols section '.gnu.version' contains 5 entries:
Addr: 0x0000000000003000 Offset: 0x00003000 Link: 5 (.dynsym)
000: 0 (*local*) 2 (GLIBC_2.2.5) 2 (GLIBC_2.2.5) 3 (GLIBC_2.3)
004: 1 (*global*)
版本信息是驱动集成中容易忽略的点。我曾经遇到一个情况:模块依赖 GLIBC_2.14,但目标系统只支持到 GLIBC_2.12,结果加载时直接报 version GLIBC_2.14 not found。用 readelf -V 可以提前发现这种版本不匹配。
3.4.3 查看 ELF 头信息
readelf -h libexample.so
输出:
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Machine: Advanced Micro Devices X86-64
Entry point address: 0x2000
Start of section headers: 123456 (bytes into file)
这里可以快速确认文件是 32 位还是 64 位,目标架构是什么。集成时如果架构不匹配,加载会直接失败。
我的习惯:拿到一个陌生的 .ko 或 .so,我第一件事就是 readelf -h 看架构,然后 readelf -d | grep NEEDED 看依赖,最后 readelf -s 看符号。三步走,基本能摸清这个文件的底细。
3.5 工具对比与选择
| 工具 | 主要用途 | 适用场景 | 注意事项 |
|---|---|---|---|
| ldd | 查看运行时动态库依赖 | 本地调试、快速验证 | 交叉编译环境不可用 |
| objdump | 查看符号表、重定位、反汇编 | 深入分析符号依赖 | 输出信息量大,需过滤 |
| nm | 列出符号(轻量) | 快速查看未定义符号 | 不显示版本信息 |
| readelf | ELF 文件全面分析 | 交叉编译、版本检查、架构确认 | 功能最全,但参数多 |
3.6 实战:一次完整的依赖分析流程
假设你拿到一个驱动模块 mydriver.ko,加载时报 Unknown symbol。我会这样排查:
- 第一步:
readelf -h mydriver.ko确认架构和位数是否正确。 - 第二步:
nm -u mydriver.ko列出所有未定义符号。 - 第三步:
cat /proc/kallsyms | grep symbol_name检查内核是否导出了这些符号。 - 第四步:如果符号在其他模块中,用
modprobe --show-depends mydriver查看模块依赖链。 - 第五步:如果符号在动态库中,用
readelf -d mydriver.ko | grep NEEDED确认依赖库,再用ldconfig -p | grep libname检查库是否存在。
重要提醒:内核模块的依赖分析和用户态动态库略有不同。内核模块使用 EXPORT_SYMBOL 导出的符号,而不是动态链接器。所以 nm -u 看到的未定义符号,必须在内核符号表或已加载的其他模块中找到。我建议用 cat /proc/kallsyms 来验证。
好了,这四个工具的基本用法和输出解读就聊到这里。你想想看,其实依赖分析没那么神秘,无非就是搞清楚「谁需要谁」以及「谁提供了谁」。工具只是手段,关键是理解背后的 ELF 结构和链接机制。下次遇到加载失败,别慌,拿出这四件套,一步步排查,问题基本都能定位到。
总结一句话:依赖分析不是玄学,是工程。工具给你数据,经验给你判断。多练几次,你也能一眼看出问题出在哪。