用 cflow 查看 C 函数调用关系

阅读陌生的 C 项目时, 逐个跳转函数可以看清细节, 但不容易把握整体关系。GNU cflow 从 C 源码提取静态调用关系, 帮助回答:

  • 调用了谁? 从入口函数展开调用树。
  • 被谁调用? 反向查看调用者。
  • 如何直观看图? 生成 DOT, 在 VSCode 中预览调用关系。

它适合辅助阅读代码, 不能代替运行时跟踪。

1. 安装工具

以下命令在 Ubuntu/Debian 或 WSL 的 Linux 终端中执行:

sh 复制代码
sudo apt update
sudo apt install cflow
cflow --version

本文使用 cflow 原生 DOT 输出, 实测环境为 GNU cflow 1.7、Graphviz 2.43.0 (Ubuntu 22.04)。可通过 cflow --help 确认 --format 支持 dot。

2. 用一个例子看调用树

将下面的代码保存为 main.c:

c 复制代码
#include <stdio.h>

void printf_hello(void)
{
    printf("Hello!\n");
}

void printf_world(void)
{
    printf("World!\n");
}

void printf_helloworld(void)
{
    printf_hello();
    printf_world();
}

int main(void)
{
    printf_hello();
    printf("****************************\n");
    printf_world();
    printf("****************************\n");
    printf_helloworld();
    return 0;
}

从 main 开始分析:

sh 复制代码
cflow -T -m main main.c

执行结果如下:

text 复制代码
+-main() <int main (void) at main.c:19>
  +-printf_hello() <void printf_hello (void) at main.c:3>
  | \-printf()
  +-printf()
  +-printf_world() <void printf_world (void) at main.c:8>
  | \-printf()
  \-printf_helloworld() <void printf_helloworld (void) at main.c:13>
    +-printf_hello() <void printf_hello (void) at main.c:3>
    | \-printf()
    \-printf_world() <void printf_world (void) at main.c:8>
      \-printf()
  • 缩进和树形符号: 表示调用层级。
  • 尖括号中的信息 : 函数签名及定义位置, 例如 main.c:19 表示第 19 行。
  • 只有 printf(): 找到了调用, 但输入源码中没有它的定义。

常用选项:

选项 用途
-T 用树状线条显示文本输出
-m 函数名 指定正向分析的入口函数
-d 3 限制遍历深度; DOT 输出可能保留边界节点指向下一层的边
-n 为输出行编号
-r 输出反向关系, 查看函数的调用者
-f dot 输出 Graphviz DOT 格式

例如, cflow -r main.c 可以查看这个文件中各函数的调用者。

3. 生成 DOT 并用 VSCode 预览

sh 复制代码
cflow -f dot -m main main.c > out.dot

生成的 out.dot 是图形描述文件, 接下来用 VSCode 插件打开。

安装 VSCode 预览插件

  1. 按 Ctrl + Shift + X 打开扩展面板。
  2. 搜索并安装 Graphviz Interactive Preview, 发布者为 tintinweb。
  3. 打开生成的 out.dot 文件。
  4. 按 Ctrl + Shift + P, 搜索 Graphviz Preview, 执行 Preview Graphviz / Dot (beside) 在右侧打开预览。

也可以点击编辑器右上角的预览按钮, 或在编辑区右键选择同一预览命令。

操作 用途
缩放、平移 查看较大的调用图
搜索节点 快速定位函数
点击节点 高亮相关调用边; 按 Esc 取消选择
导出 SVG 保存可放大的矢量图

图太大时, 缩小入口和文件范围, 或用 -d 限制深度。

下面展示该 DOT 文件的渲染结果, 便于对照节点和调用边; 插件中的布局可能因渲染版本而异:

  • 箭头方向: 调用者 → 被调用者。
  • 共享节点: 多处调用同一函数时, 图中可以汇聚到一个节点。
  • 库函数 : printf 的实现在库中, 本例不会展开其内部实现。

4. 工程示例: 串口命令处理

以一个简化的串口温度查询工程为例, 从 app_poll() 查看跨文件调用关系。

文件 职责
src/main.c 读取命令并交给协议模块
src/protocol.c 解析 GET_TEMP, 调用采集和发送接口
src/sensor.c 提供温度读取接口
src/uart.c 提供串口接收和发送接口
include/app.h 声明模块接口

完整示例源码见下方。串口接收和温度采集使用主机桩函数, 便于直接编译运行; 接入硬件时替换为实际驱动。

在示例工程根目录执行:

sh 复制代码
cflow --cpp -I include -m app_poll -d 3 -f dot src/main.c src/protocol.c src/sensor.c src/uart.c > uart.dot

在 VSCode 中打开 uart.dot, 执行同一预览命令。该 DOT 的渲染结果如下, 节点包含函数签名、文件名和行号:

  • app_poll() 调用 uart_read_line() 和 protocol_handle()。
  • protocol_handle() 调用命令解析、温度读取和串口发送接口。
  • 深度边界 : 本次 -d 3 -f dot 输出仍包含 uart_write_temperature → printf, 因此图中会出现下一层的 printf 节点。
  • 文件归属: 源文件和行号显示在节点内, 原生输出没有按文件分组的外框。

串口工程完整源码

按以下路径保存 5 个文件, 即可执行上面的分析和编译命令。

include/app.h
c 复制代码
#ifndef APP_H
#define APP_H
void app_poll(void);
const char *uart_read_line(void);
void uart_write_temperature(int value);
void protocol_handle(const char *line);
int parse_command(const char *line);
int sensor_read_temperature(void);
#endif
src/main.c
c 复制代码
#include "app.h"

void app_poll(void)
{
    const char *line = uart_read_line();
    if (line != 0) {
        protocol_handle(line);
    }
}

int main(void)
{
    app_poll();
    return 0;
}
src/protocol.c
c 复制代码
#include "app.h"

int parse_command(const char *line)
{
    const char expected[] = "GET_TEMP";
    unsigned int i = 0;
    while (expected[i] != '\0' && line[i] == expected[i]) {
        ++i;
    }
    return expected[i] == '\0' && line[i] == '\0';
}

void protocol_handle(const char *line)
{
    if (parse_command(line)) {
        int temperature = sensor_read_temperature();
        uart_write_temperature(temperature);
    }
}
src/sensor.c
c 复制代码
#include "app.h"

/* Host stub: replace with a real sensor driver. */
int sensor_read_temperature(void)
{
    return 25;
}
src/uart.c
c 复制代码
#include "app.h"
#include <stdio.h>

/* Host stubs: replace with board UART access in a real project. */
const char *uart_read_line(void)
{
    return "GET_TEMP";
}

void uart_write_temperature(int value)
{
    printf("TEMP:%d\n", value);
}

验证主机示例:

sh 复制代码
gcc -std=c11 -Wall -Wextra -Werror -I include src/main.c src/protocol.c src/sensor.c src/uart.c -o uart_demo
./uart_demo

输出为 TEMP:25。实际工程有条件编译时, 在 cflow 命令中补上与构建一致的 -D 宏名=值; 头文件通过 -I 和 #include 引入。

本次启用 --cpp 时, cflow 对系统 stdio.h 中的部分扩展声明报了重定义警告, 但命令返回成功。已逐项核对本例的 6 条调用边; 其他工程出现警告时也应检查解析结果。

使用时注意:

  • 分析范围: 未传入的源文件不会展开实现, 应包含相关模块, 避免一次扫描无关目录。
  • 预处理配置: 条件编译会影响结果, 宏和头文件路径应与目标配置一致。
  • 静态分析限制: 函数指针、回调注册、汇编和编译器扩展可能导致调用关系缺失或解析不准确。
  • 阅读方式: 图表示源码中的调用关系, 不代表某次运行的执行顺序、调用次数或耗时。

先从一个入口看清主线, 再结合源码确认关键分支和回调, 通常比直接生成整个工程的大图更有效。

需要自动扫描目录、按源文件分组时, 可阅读配套的 cflow 进阶: 结合 tree2dotx。

参考资料

本节 main.c 完整源码已列于正文。

补充: 用命令行导出图片

需要批量导出或在脚本中生成图片时, 安装 Graphviz:

sh 复制代码
sudo apt install graphviz
dot -V

在 DOT 文件所在目录执行:

sh 复制代码
dot -Tpng out.dot -o out.png
dot -Tsvg out.dot -o out.svg

工程示例将 out.dot 替换为 uart.dot, 并修改输出文件名。本文配图使用 Graphviz 2.43.0 生成。

相关推荐
zhang-ge41 分钟前
python包和项目管理器uv在ubuntu下的安装与使用
开发语言·python·uv
微笑星河q2 小时前
C++类和对象(下)初始化列表、类型转换、static、友元、内部类、匿名对象
开发语言·c++
凤年徐2 小时前
【Python】Python 语法速通指南
开发语言·python
quantdash_cc2 小时前
量化数据清洗应该放在数据层还是策略层?从系统架构看清职责边界
开发语言·python·数据分析·系统架构·量化交易·股票数据·quantdash
|_⊙2 小时前
C++ 类和对象零散知识点
开发语言·c++
xiangyun612 小时前
C++ list 模拟实现(二):const迭代器与反向迭代器,模板的黑魔法
开发语言·数据库·c++·算法·stl·list
DLite2 小时前
NLang 0.9.0发布:成为可嵌入语言
开发语言·编程语言·编译技术·nlang
有同事要进步3 小时前
LeetCode 14. 最长公共前缀:纵向扫描一次讲透
开发语言·python
修船大队长3 小时前
CanNm 从休眠唤醒后重复消息报文的周期与数量分析
开发语言·php