2、依赖关系分析基础:依赖图的构建、依赖关系的类型、依赖分析工具介绍
做嵌入式系统这么多年,我越来越觉得一件事——依赖关系分析,是整个文件系统裁剪的基石。你想想看,如果连谁依赖谁都没搞清楚,那裁剪就变成了瞎蒙。我见过太多团队,上来就删文件,结果编译报错、链接失败、运行时崩溃,最后不得不把删掉的东西一个个加回来。
说白了,依赖分析就是给你的系统画一张「关系网」。这张网画清楚了,裁剪才有方向。
2.1 依赖图:系统的「关系网」
依赖图是什么?简单说,它是一个有向图。每个节点代表一个文件、一个模块或者一个库。每条有向边代表「依赖关系」。
举个例子:main.c 里调用了 uart.c 里的函数,那图上就有一条从 main.c 指向 uart.c 的箭头。
我个人习惯把依赖图分成三个层次来构建:
- 文件级依赖图:单个源文件之间的 #include 关系
- 模块级依赖图:功能模块之间的调用关系
- 系统级依赖图:整个系统与外部库、内核接口的关系
嗯,这里要注意:依赖图不是静态的。随着代码迭代,依赖关系会变。我建议每次做裁剪前,都重新生成一次依赖图,别用三个月前的旧图。
核心原则:依赖图越清晰,裁剪越安全。依赖图越乱,裁剪越容易翻车。
下面这张图,是我做裁剪时常用的依赖分析流程:
2.2 依赖关系的三种类型
依赖关系不是铁板一块。我把它分成三类,每一类的处理方式都不一样。
2.2.1 编译时依赖
这是最直观的依赖。说白了,就是 #include 和宏定义带来的依赖。
比如:
// main.c
#include "uart.h" // 编译时依赖:需要 uart.h 中的声明
#include "config.h" // 编译时依赖:需要宏定义
void main() {
uart_init();
}
编译时依赖的特点是:缺了头文件,编译直接报错。我在项目中遇到过,有人把 config.h 删了,结果整个项目几百个文件全部编译失败。嗯,这种依赖最好处理,也最容易发现。
小技巧:用 gcc -M 可以自动生成源文件的编译时依赖列表。我每次裁剪前都会跑一遍,生成一个完整的依赖清单。
2.2.2 链接时依赖
链接时依赖,指的是符号引用关系。你调用了某个函数,链接器就要找到它的定义。
举个例子:
// main.c
extern void uart_send(char c); // 声明,编译通过
void main() {
uart_send('A'); // 链接时需要找到 uart_send 的实现
}
如果 uart_send 所在的 uart.o 没有被链接进来,链接器就会报 undefined reference。
这里有个坑——链接时依赖有时候是隐形的。我记得有一次,一个全局变量在 a.c 里定义,在 b.c 里用 extern 引用。结果 a.c 被误删了,链接器报错,排查了半天才发现是全局变量的问题。
注意:链接时依赖不一定在代码中显式出现。静态库之间的依赖、弱符号的覆盖关系,都可能产生隐形的链接时依赖。
2.2.3 运行时依赖
这是最隐蔽、也最危险的一类依赖。编译和链接都通过了,但程序跑起来就崩。
运行时依赖包括:
- 动态库加载:程序启动时
dlopen加载的 .so 文件 - 配置文件:程序运行时读取的 .conf、.json 等文件
- 设备节点:访问
/dev/下的设备文件 - 固件文件:驱动加载的 .fw 文件
- 数据文件:运行时读取的资源文件、字体、图标等
我曾经在一个项目中,裁剪掉了 /etc/fstab 文件,结果系统启动时挂载失败,直接进入了紧急模式。编译和链接都没问题,但运行时就是不行。从那以后,我养成了一个习惯——运行时依赖必须通过实际运行来验证。
2.3 依赖分析工具介绍
手工分析依赖太累了,尤其是大型项目。我推荐几个工具,能帮你省不少力气。
| 工具 | 适用场景 | 输出格式 | 我的评价 |
|---|---|---|---|
gcc -M / -MM |
编译时依赖分析 | Makefile 格式 | 最基础,但够用 |
nm |
链接时符号分析 | 文本列表 | 查看未定义符号必备 |
ldd |
动态库依赖分析 | 树形结构 | 查运行时依赖首选 |
strace |
运行时文件访问追踪 | 系统调用日志 | 抓隐形依赖的神器 |
depends.exe |
Windows DLL 依赖 | 图形界面 | Windows 下好用 |
我个人的工作流是这样的:
- 先用
gcc -MM生成编译时依赖,保存到文件 - 再用
nm -u查看所有未定义符号,确认链接时依赖 - 然后用
ldd检查动态库依赖 - 最后用
strace -e openat,stat跑一遍程序,抓取所有打开的文件
四步走完,依赖关系基本就清楚了。
实战建议:别只依赖一个工具。每个工具都有盲区。比如 ldd 只能看到显式的动态库依赖,但 dlopen 加载的库它看不到。这时候就得靠 strace 来补位。
2.4 依赖分析的常见误区
最后聊几个我踩过的坑:
- 误区一:只分析编译时依赖。很多人做完
gcc -M就觉得够了。其实运行时依赖才是大头。 - 误区二:忽略条件编译。
#ifdef会导致某些依赖只在特定配置下出现。我建议用实际使用的配置文件来生成依赖图。 - 误区三:依赖图不更新。代码改了,依赖图没改,裁剪时就会漏掉新引入的依赖。
- 误区四:过度依赖自动化工具。工具能帮你发现 90% 的依赖,但剩下的 10% 需要人工审查。尤其是那些通过函数指针、回调机制建立的依赖关系。
嗯,依赖分析这件事,说白了就是「慢工出细活」。别着急,把依赖图画清楚了,后面的裁剪工作才能顺风顺水。