4. 静态代码分析:工具使用(PC-Lint、Cppcheck)、规则配置、常见缺陷检测

静态代码分析,说白了就是让工具替你把代码「读一遍」。

我刚开始做固件时,总觉得代码能编译过就万事大吉。直到有一次,一个未初始化的变量在电表里跑了三个月才暴露出来——那叫一个惨。从那以后,我把静态分析当成了提交代码前的「安检门」,不过这道门,必须过。

4.1 为什么需要静态分析?

你想想看,动态测试只能覆盖你写了的测试用例。但静态分析不一样,它能把所有可能的执行路径都扫一遍。我个人的习惯是:先静态,再动态。顺序不能乱。

智能电表的固件有几个特点:

  • 资源受限:RAM、Flash 都抠得很紧
  • 实时性要求高:计量、通信不能卡顿
  • 安全等级高:数据不能丢,参数不能乱

这些特点决定了——代码里不能有「暗坑」。静态分析就是帮你把这些坑提前填上。

核心观点:静态分析不是找茬,是帮你省钱。一个 bug 在编码阶段发现,成本是 1;到了测试阶段,成本是 10;到了现场,成本是 1000。

4.2 PC-Lint:老牌工具,规则为王

PC-Lint 这工具,年纪比很多工程师都大。但它依然是嵌入式领域的「金标准」。为什么?因为它够细,够深。

4.2.1 基本用法

我一般这样调用 PC-Lint:

// 命令行方式
lint-nt.exe -u std.lnt myfile.c

// 配合 makefile 使用
lint-nt.exe -u project.lnt *.c > lint_report.txt

这里的 std.lnt 是选项文件,里面配置了你要检查的规则。我个人习惯把规则分成三组:

规则组 说明 我常用的配置
强检查 语法、类型、未初始化 -w4 -e537
信息类 风格、可移植性 -e715 -e818
抑制类 已知误报 -e830 -e845

我的小技巧:不要一上来就开所有规则。先开强检查,跑一遍,把真正的 bug 修掉。然后再逐步放开信息类规则。否则你会被几千条 warning 淹死。

4.2.2 规则配置实战

我在项目中遇到过最典型的一个场景:

// 错误示例
uint8_t status;
if (some_condition) {
    status = 1;
}
// 这里 status 可能未初始化!
process_status(status);

PC-Lint 会报 Warning 530: Symbol 'status' not initialized。这个警告我从来不敢忽略。因为电表里这种「条件赋值」太常见了,一旦条件不满足,变量就是随机值。

配置规则时,我建议你重点关注这几类:

  • 未初始化变量(530, 531)—— 电表死机的头号元凶
  • 指针空值(613, 668)—— 通信模块的常见问题
  • 缓冲区溢出(661, 662)—— 安全漏洞的温床
  • 类型不匹配(734, 737)—— 计量精度误差的来源

注意:PC-Lint 的规则编号是跨版本的。如果你换了版本,记得重新验证一下规则编号是否一致。我曾经因为升级版本,漏掉了一条关键规则,导致一个 bug 流到了系统测试阶段。

4.3 Cppcheck:开源利器,轻量高效

Cppcheck 是我个人非常喜欢的工具。它免费、跨平台、而且对嵌入式场景支持得很好。虽然深度不如 PC-Lint,但胜在「快」和「准」。

4.3.1 基本用法

// 基本检查
cppcheck --enable=all myfile.c

// 针对嵌入式平台
cppcheck --platform=arm32-wchar4 myfile.c

// 输出到文件
cppcheck --enable=warning,style --xml myfile.c 2> report.xml

我一般这样组合使用:

  • 日常开发用 Cppcheck 做快速检查(几秒钟出结果)
  • 提交代码前用 PC-Lint 做深度扫描(几分钟)
  • CI 流水线里两个都跑

4.3.2 常见缺陷检测

Cppcheck 在检测这几类问题上特别拿手:

缺陷类型 Cppcheck 检查项 典型场景
数组越界 arrayIndexOutOfBounds 电表参数表访问
内存泄漏 memleak 动态分配未释放
空指针解引用 nullPointer 通信回调函数
未使用变量 unusedVariable 调试遗留代码
逻辑错误 logicalError 条件判断写反

举个例子,Cppcheck 能发现这种「一眼看不出来」的问题:

// 问题代码
void set_meter_param(uint8_t param_id, uint32_t value) {
    uint32_t param_table[10];
    if (param_id < 10) {
        param_table[param_id] = value;  // 没问题?
    }
    // 但这里 param_id 是 uint8_t,最大值 255
    // 如果传入 255,条件判断为假,但不会报错
    // 真正的 bug 在调用方!
}

Cppcheck 会提示 param_id 的类型范围与条件判断不匹配。嗯,这种细节,人工 review 很容易漏掉。

避坑指南:我曾经在一个电表项目中,因为一个 uint8_t 变量在循环中自增到 256 后溢出,导致死循环。Cppcheck 的 unsignedIntegerOverflow 检查直接抓到了这个问题。从那以后,我把所有整数溢出的检查都开到了最高级别。

4.4 规则配置的最佳实践

工具再好,规则配不对也是白搭。我总结了一套「三步走」的配置方法:

  1. 基线建立:先用默认规则跑一遍,把误报标记出来
  2. 规则裁剪:根据项目特点,关闭不必要的规则,打开关键的
  3. 持续优化:每发现一个新类型的 bug,就增加对应的规则

举个例子,智能电表里经常用到 __attribute__((packed)) 来定义通信协议结构体。PC-Lint 默认会对这种用法报 Warning 826: Suspicious use of #pragma pack。但这是故意的,不是 bug。所以我会在项目配置里把它关掉。

// 项目级 lint 配置示例
// project.lnt
-w4
-e826          // 关闭 packed 结构体警告
-e830          // 关闭 location 信息
-e845          // 关闭位域警告
+libh(.\lib)   // 指定库文件路径
+libdir(.\lib) // 指定库目录

我的习惯:每个项目维护一个 .lint 配置文件,放在版本控制里。这样团队所有人都用同一套规则,不会出现「我的机器上没报错」这种扯皮情况。

4.5 常见缺陷检测清单

最后,我整理了一份「电表固件静态分析必查清单」。每次代码评审前,我都会对照着过一遍:

序号 缺陷类型 检查工具 严重程度
1 未初始化变量 PC-Lint / Cppcheck 致命
2 缓冲区溢出 PC-Lint 致命
3 空指针解引用 Cppcheck 致命
4 整数溢出 Cppcheck 严重
5 类型不匹配 PC-Lint 严重
6 死代码 PC-Lint / Cppcheck 一般
7 未使用变量/函数 Cppcheck 一般
8 逻辑错误 Cppcheck 严重

你想想看,如果每次提交代码前都能把这 8 类问题扫一遍,固件的质量能差到哪去?

好了,静态分析这块就聊到这儿。下一章我们聊聊动态测试——那又是另一番天地了。