动态分析:覆盖率测试、性能剖析与调用图生成

说实话,很多嵌入式工程师写完代码就跑一下,能跑通就觉得完事了。但我在实际项目中吃过亏——代码跑是能跑,可那些没执行到的分支里藏着大坑。今天我们就聊聊怎么用工具把代码的“底裤”扒干净。

一、覆盖率测试:你代码里有多少“死胡同”?

覆盖率测试,说白了就是检查你的测试用例到底跑了多少代码。我见过一个项目,自测报告写“全部通过”,结果用gcov一跑,覆盖率才32%。剩下的68%代码从来没被执行过。

1.1 gcov:GCC自带的“照妖镜”

gcov是GCC工具链自带的,不需要额外安装。用法很简单:

// 编译时加两个关键选项
gcc -fprofile-arcs -ftest-coverage -o test_program test.c

// 运行程序
./test_program

// 生成覆盖率报告
gcov test.c

运行后会生成 .gcov 文件,里面每一行代码前面都有执行次数。比如:

        -:    0:Source:test.c
        -:    1:#include <stdio.h>
        5:    2:int main() {
        5:    3:    int x = 10;
        5:    4:    if (x > 5) {
        5:    5:        printf("big\n");
        -:    6:    } else {
        0:    7:        printf("small\n");
        -:    8:    }
        5:    9:    return 0;
        -:   10:}

看到第7行的 0 了吗?这说明else分支从来没被执行过。我在项目中遇到过类似情况,一个错误处理分支覆盖率为0,结果线上真的触发了那个分支,系统直接崩溃。

注意:gcov会拖慢程序运行速度,大约2-3倍。生产环境千万别开这个选项。

1.2 lcov:让报告更直观

gcov生成的文本报告看着费劲。lcov可以把结果转成HTML,带颜色标注:绿色是已覆盖,红色是未覆盖。

// 生成lcov数据
lcov --capture --directory . --output-file coverage.info

// 生成HTML报告
genhtml coverage.info --output-directory coverage_report

我个人习惯在持续集成里跑lcov,每次提交代码自动生成报告。这样团队里每个人都能看到自己的代码覆盖情况。

小技巧:嵌入式项目里,很多代码是硬件相关的,没法在PC上跑。我建议用“单元测试+模拟器”的方式,把算法逻辑单独抽出来测试。

二、性能剖析:你的程序到底慢在哪?

“感觉程序有点慢”——这种话在项目里我听过太多次了。感觉没用,得用数据说话。gprof就是干这个的。

2.1 gprof:函数级别的性能分析

gprof能告诉你每个函数被调用了多少次、花了多少时间。用法:

// 编译时加-pg选项
gcc -pg -o test_program test.c

// 运行程序(会生成gmon.out文件)
./test_program

// 生成分析报告
gprof test_program gmon.out > analysis.txt

报告里有两张表:

函数名 调用次数 耗时占比 平均耗时(ms)
process_data 1000 45.2% 0.45
sort_array 100 30.1% 3.01
init_system 1 15.3% 153.00

你看,init_system虽然只调用一次,但占了15%的时间。这就是优化的重点。

核心原则:优化要盯着“热点函数”,别在只占1%时间的函数上浪费时间。我曾经见过有人花三天优化一个只占0.5%时间的函数,纯属浪费。

2.2 嵌入式环境下的特殊处理

gprof在嵌入式环境里有点水土不服。为什么?因为嵌入式设备通常没有文件系统,gmon.out写不进去。

我建议的做法:

  • 用串口把性能数据打印出来
  • 或者用内存缓冲区暂存,再通过调试接口读取
  • 实在不行,就手动插时间戳,土办法有时候最管用

三、调用图生成:看清函数之间的“关系网”

代码写多了,函数调用关系会变得很复杂。调用图能帮你理清思路。

3.1 用gprof生成调用图

gprof本身就支持生成调用图:

gprof -q test_program gmon.out

输出类似这样:

index % time    self  children    called     name
                0.01    0.02       10/10         main [1]
[1]     45.2    0.01    0.02       10         process_data [1]
                0.01    0.01        5/5          sort_array [2]
                0.00    0.01        5/5          filter_data [3]

这表示 main 调用了 process_data 10次,process_data 又调用了 sort_arrayfilter_data 各5次。

3.2 可视化调用图

文本调用图不够直观。我一般用Graphviz把调用图转成图片:

gprof -q test_program gmon.out | gprof2dot | dot -Tpng -o callgraph.png

下面是我用SVG画的一个简化版调用图,展示核心逻辑:

函数调用关系图示例 main() 调用1次 process_data() 调用10次,耗时45% init_system() 调用1次,耗时15% sort_array() 调用5次,耗时30% filter_data() 调用5次,耗时10% error_handler() 调用0次(未覆盖) 已覆盖函数 未覆盖函数 已执行调用 未执行调用

四、实战经验总结

说了这么多,我总结几条实战经验:

  1. 覆盖率不是越高越好——嵌入式项目里,硬件初始化代码很难100%覆盖。我一般要求核心算法逻辑覆盖率达到90%以上就行。
  2. 性能剖析要结合场景——在PC上跑gprof的结果,和嵌入式设备上的真实表现可能有差异。我建议在目标硬件上做最终验证。
  3. 调用图是重构的好帮手——每次接手别人的代码,我第一件事就是生成调用图。一眼就能看出哪些函数耦合太深,哪些函数没人调用。
我的习惯:每次提交代码前,跑一遍gcov和gprof。花不了几分钟,但能避免很多线上问题。你想想看,要是上线后发现某个分支从来没执行过,那得多尴尬?

好了,动态分析这块就聊到这儿。工具只是辅助,关键还是要有“用数据说话”的意识。下次你觉得自己代码没问题的时候,不妨先跑一遍覆盖率看看。


公众号:蓝海资料掘金营,微信deep3321