第一个静态分析实战:编写一个包含MISRA违规的简单C程序
好,咱们开始动手了。
前面几章讲了那么多理论,什么MISRA规则分类、Cppcheck的安装配置,说实话,光听不练容易犯困。我个人习惯是——先写一段有问题的代码,让工具跑一遍,亲眼看到警告长什么样,然后再回头理解规则。这样印象深得多。
4.1 准备一个"靶子"程序
我特意写了一个小例子。它不长,但包含了几个典型的MISRA C:2012违规。你可以在自己的开发环境里跟着敲一遍。
/* file: misra_demo.c */
#include <stdio.h>
#include <stdint.h>
/* 全局变量 —— 违反 Rule 8.4 */
int32_t global_counter;
/* 函数声明缺失参数 —— 违反 Rule 8.2 */
void process_data();
/* 主函数 */
int main(void)
{
int32_t x = 10;
int32_t y = 20;
int32_t result;
/* 赋值运算符在条件表达式中 —— 违反 Rule 13.4 */
if (x = y)
{
result = x + y;
}
/* 隐式类型转换 —— 违反 Rule 10.3 */
uint16_t u16_val = 1000;
uint32_t u32_val = u16_val * 500;
/* 魔数 —— 违反 Rule 5.4 和 Rule 8.13 */
for (int32_t i = 0; i < 42; i++)
{
global_counter++;
}
/* 未使用的函数参数 —— 违反 Rule 2.7 */
process_data();
return 0;
}
/* 函数定义与声明不匹配 —— 违反 Rule 8.2 */
void process_data(int32_t input)
{
/* 空函数体 —— 违反 Rule 2.2 */
}
这段代码能编译通过,也能运行。但你要是把它提交到汽车电子或医疗设备项目里,代码评审那一关绝对过不了。
4.2 用Cppcheck扫描这个程序
打开终端,进到 misra_demo.c 所在的目录,执行下面这条命令:
cppcheck --enable=all --std=c11 --addon=misra.py misra_demo.c 2> misra_report.txt
解释一下这几个参数:
| 参数 | 作用 |
|---|---|
--enable=all |
开启所有检查项,包括风格、性能、可移植性等 |
--std=c11 |
指定C语言标准为C11,MISRA C:2012基于C99/C11 |
--addon=misra.py |
加载MISRA检查插件,这是关键 |
2> misra_report.txt |
把警告信息重定向到文件,方便查看 |
跑完之后,打开 misra_report.txt,你会看到类似下面的输出:
misra_demo.c:5:5: style: [misra-c2012-8.4] A compatible declaration shall be visible when an object with external linkage is defined.
misra_demo.c:8:6: style: [misra-c2012-8.2] Function identifiers and function types shall have external linkage.
misra_demo.c:17:9: style: [misra-c2012-13.4] The result of an assignment operator should not be used.
misra_demo.c:23:29: style: [misra-c2012-10.3] The value of an expression shall not be assigned to a wider essential type.
misra_demo.c:26:24: style: [misra-c2012-5.4] Macro and other identifiers shall not be reused.
misra_demo.c:26:24: style: [misra-c2012-8.13] A pointer to a function shall not be used.
misra_demo.c:31:5: style: [misra-c2012-2.7] A function should have a single point of exit.
misra_demo.c:36:6: style: [misra-c2012-2.2] A function shall not contain a dead code.
嗯,警告不少。咱们一个一个来解读。
4.3 解读第一个警告:Rule 8.4
先看第一条:
misra_demo.c:5:5: style: [misra-c2012-8.4] A compatible declaration shall be visible when an object with external linkage is defined.
它指向第5行:
int32_t global_counter;
这条规则说的是:如果一个对象具有外部链接(external linkage),那么在定义它的时候,必须有一个兼容的声明是可见的。
说白了,就是全局变量 global_counter 定义在源文件里,但没有在头文件中声明。其他.c文件如果要用它,只能靠猜——或者自己再extern一次,很容易造成类型不一致。
/* misra_demo.h */
#ifndef MISRA_DEMO_H
#define MISRA_DEMO_H
#include <stdint.h>
extern int32_t global_counter;
#endif /* MISRA_DEMO_H */
为什么MISRA要定这条规则?我在项目里遇到过真实案例:两个模块各自extern了一个全局变量,一个以为是 uint32_t,另一个以为是 int32_t。结果数据传过去,符号位被解释成数值,整个系统跑飞了。查了三天才定位到问题。从那以后,我对全局变量的声明一致性特别敏感。
4.4 第二个警告:Rule 8.2
misra_demo.c:8:6: style: [misra-c2012-8.2] Function identifiers and function types shall have external linkage.
指向第8行:
void process_data();
这条规则要求:函数声明必须包含完整的参数类型信息。
你写 void process_data(); 在C语言里是允许的,但它表示"参数个数和类型未指定"。MISRA不允许这种模糊声明。正确的写法是:
void process_data(int32_t input);
你看,加上参数类型,编译器就能帮你做类型检查了。调用时传错参数,编译阶段就能发现,不用等到运行时崩溃。
4.5 第三个警告:Rule 13.4
misra_demo.c:17:9: style: [misra-c2012-13.4] The result of an assignment operator should not be used.
指向第17行:
if (x = y)
这是C语言里最经典的"坑"之一。程序员本意是 if (x == y),结果少写了一个等号,变成了赋值。赋值表达式的结果就是被赋的值,所以只要 y 非零,条件永远为真。
MISRA直接禁止在条件表达式中使用赋值运算符。你可能会说:"我写 if ((x = get_value()) != 0) 这种模式也不行吗?" 嗯,MISRA C:2012的Rule 13.4确实不允许。如果你确实需要这种写法,可以用 if ( (x = get_value()), x != 0 ) 这种逗号表达式绕过去,但我个人建议——拆开写,可读性更好,也不容易出错。
4.6 知识结构总览
下面这张图把刚才讲到的几个违规和对应的MISRA规则串了起来。你可以把它当作一个快速参考。
4.7 如何解读Cppcheck的输出格式
Cppcheck的警告信息格式很固定,学会看格式,就能快速定位问题:
文件名:行号:列号: 严重级别: [规则ID] 描述信息
举个例子:
misra_demo.c:17:9: style: [misra-c2012-13.4] The result of an assignment operator should not be used.
- 文件名:
misra_demo.c - 行号:
17—— 问题在第17行 - 列号:
9—— 从第9个字符开始 - 严重级别:
style—— 风格问题,MISRA警告通常归为style - 规则ID:
misra-c2012-13.4—— 对应MISRA C:2012的Rule 13.4 - 描述: 告诉你具体违反了哪条规则
style 级别不够醒目,可以用 --suppress=style 过滤掉非MISRA的风格警告,只保留MISRA相关的输出。或者用 --xml 输出XML格式,方便集成到CI/CD流水线里。
4.8 避坑指南
最后分享几个我踩过的坑:
- 不要一次性修所有警告。 我第一次用Cppcheck扫描一个遗留项目,出来800多个警告。我试图一口气改完,结果改到一半发现有些"违规"其实是项目特定的编码风格,改了反而破坏了原有逻辑。正确的做法是:先分类,再分批修复,每修一批都做回归测试。
- 注意误报。 Cppcheck不是100%准确。有些警告可能是误报,尤其是涉及指针别名、复杂宏展开的场景。遇到可疑的警告,先人工确认,再决定是否修复或加入抑制注释。
- 把静态分析集成到CI里。 别等到项目快交付了才跑一次静态分析。我建议每次提交代码都自动触发扫描,增量检查新增的违规。这样问题在引入当天就被发现,修复成本最低。