2、依赖关系分析基础:依赖图的构建、依赖关系的类型、依赖分析工具介绍

做嵌入式系统这么多年,我越来越觉得一件事——依赖关系分析,是整个文件系统裁剪的基石。你想想看,如果连谁依赖谁都没搞清楚,那裁剪就变成了瞎蒙。我见过太多团队,上来就删文件,结果编译报错、链接失败、运行时崩溃,最后不得不把删掉的东西一个个加回来。

说白了,依赖分析就是给你的系统画一张「关系网」。这张网画清楚了,裁剪才有方向。

2.1 依赖图:系统的「关系网」

依赖图是什么?简单说,它是一个有向图。每个节点代表一个文件、一个模块或者一个库。每条有向边代表「依赖关系」。

举个例子:main.c 里调用了 uart.c 里的函数,那图上就有一条从 main.c 指向 uart.c 的箭头。

我个人习惯把依赖图分成三个层次来构建:

  • 文件级依赖图:单个源文件之间的 #include 关系
  • 模块级依赖图:功能模块之间的调用关系
  • 系统级依赖图:整个系统与外部库、内核接口的关系

嗯,这里要注意:依赖图不是静态的。随着代码迭代,依赖关系会变。我建议每次做裁剪前,都重新生成一次依赖图,别用三个月前的旧图。

核心原则:依赖图越清晰,裁剪越安全。依赖图越乱,裁剪越容易翻车。

下面这张图,是我做裁剪时常用的依赖分析流程:

依赖分析流程 步骤1:源码扫描 解析 #include、extern 步骤2:构建依赖图 生成有向图结构 步骤3:分类依赖 编译/链接/运行时 步骤4:标记关键路径 识别不可裁剪节点 步骤5:裁剪决策 确定可移除模块 迭代验证,直到依赖图稳定 💡 我的经验: 步骤2和步骤3往往需要反复迭代。第一次构建的依赖图通常不完整, 需要结合编译日志和链接日志来补充遗漏的依赖关系。

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 下好用

我个人的工作流是这样的:

  1. 先用 gcc -MM 生成编译时依赖,保存到文件
  2. 再用 nm -u 查看所有未定义符号,确认链接时依赖
  3. 然后用 ldd 检查动态库依赖
  4. 最后用 strace -e openat,stat 跑一遍程序,抓取所有打开的文件

四步走完,依赖关系基本就清楚了。

实战建议:别只依赖一个工具。每个工具都有盲区。比如 ldd 只能看到显式的动态库依赖,但 dlopen 加载的库它看不到。这时候就得靠 strace 来补位。

2.4 依赖分析的常见误区

最后聊几个我踩过的坑:

  • 误区一:只分析编译时依赖。很多人做完 gcc -M 就觉得够了。其实运行时依赖才是大头。
  • 误区二:忽略条件编译。#ifdef 会导致某些依赖只在特定配置下出现。我建议用实际使用的配置文件来生成依赖图。
  • 误区三:依赖图不更新。代码改了,依赖图没改,裁剪时就会漏掉新引入的依赖。
  • 误区四:过度依赖自动化工具。工具能帮你发现 90% 的依赖,但剩下的 10% 需要人工审查。尤其是那些通过函数指针、回调机制建立的依赖关系。

嗯,依赖分析这件事,说白了就是「慢工出细活」。别着急,把依赖图画清楚了,后面的裁剪工作才能顺风顺水。

可可精品资料库