第一个静态分析实战:编写一个包含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 */
}

这段代码能编译通过,也能运行。但你要是把它提交到汽车电子或医疗设备项目里,代码评审那一关绝对过不了。

注意: 这段代码故意写得"脏",目的是让Cppcheck抓到违规。千万别在生产代码里这么写。我曾经在一个客户现场看到过类似的代码——全局变量满天飞,函数声明缺参数,结果静态分析一跑,两百多个警告,改了一整个迭代才清完。

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一次,很容易造成类型不一致。

正确做法: 在对应的头文件里加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 ) 这种逗号表达式绕过去,但我个人建议——拆开写,可读性更好,也不容易出错。

我的习惯: 赋值和条件判断永远分开两行写。先赋值,再判断。多一行代码,少一个bug。

4.6 知识结构总览

下面这张图把刚才讲到的几个违规和对应的MISRA规则串了起来。你可以把它当作一个快速参考。

第一个静态分析实战:违规与规则对应关系 misra_demo.c 全局变量无extern声明 函数声明缺参数 if(x = y) 赋值误用 隐式类型转换 魔数 42 空函数体 对应MISRA规则编号 • 全局变量 → Rule 8.4:外部链接对象需有兼容声明可见 • 函数声明 → Rule 8.2:函数类型需有完整参数信息 • 赋值当条件 → Rule 13.4:不应使用赋值运算符的结果 • 隐式类型转换 → Rule 10.3:表达式值不应赋给更宽的基本类型 • 魔数 → Rule 5.4:标识符不应重复使用 / Rule 8.13:函数指针不应使用

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里。 别等到项目快交付了才跑一次静态分析。我建议每次提交代码都自动触发扫描,增量检查新增的违规。这样问题在引入当天就被发现,修复成本最低。