第3章 编码规范基础:MISRA-C 2012核心规则

说实话,我刚入行那会儿,对编码规范这事儿挺不屑的。

觉得能把功能跑通就行,管什么规则不规则。直到有一次,我负责的一个模块在集成测试时出了个极其诡异的bug——数组越界,但编译器没报任何警告。排查了整整两天,最后发现是代码里一个不起眼的指针偏移写错了。嗯,从那以后,我再也不敢小看编码规范了。

3.1 MISRA-C 2012到底是什么?

MISRA-C,全称是Motor Industry Software Reliability Association C语言标准。说白了,就是一群搞汽车电子的老法师们,把C语言里容易踩坑的地方全列了出来,然后告诉你「这些写法别碰」。

2012版本是目前最主流的。我个人的习惯是,不管项目是不是汽车电子,只要涉及嵌入式开发,我都会拿MISRA-C的规则过一遍代码。为什么?因为它的规则背后,全是血淋淋的教训。

3.2 核心规则速览

MISRA-C 2012一共143条规则,分三类:

  • 强制规则(Required):必须遵守,不遵守就是违规
  • 建议规则(Advisory):强烈建议遵守,但允许有理由的例外
  • 指令(Directive):需要人工判断的规则

我挑几个最常碰到的核心规则,给你讲讲。

规则1:禁止使用动态内存分配

malloc、free、realloc这些函数,在MISRA-C里是禁用的。为什么?你想想看,嵌入式系统内存就那么点,动态分配容易产生碎片,而且一旦分配失败,你连个优雅退出的机会都没有。

我在项目中遇到过,有人非要用malloc来管理一个通信缓冲区。结果系统跑了三天,内存碎片导致分配失败,整个设备死机了。从那以后,我团队里谁提malloc我跟谁急。

正确做法:使用静态数组或内存池。

/* 违规写法 */
uint8_t *buffer = (uint8_t *)malloc(256);

/* 合规写法 */
static uint8_t buffer[256];

规则2:禁止使用goto语句

goto这玩意儿,用好了是神器,用不好是灾难。MISRA-C直接一刀切——禁止。我个人觉得这个规则很明智。你想想看,代码里跳来跳去,调试的时候你根本不知道程序会飞到哪去。

替代方案:用break、continue、return,或者重构代码逻辑。

规则3:所有switch语句必须有default分支

这个规则我特别喜欢。很多新手写switch,只处理自己关心的case,忘了还有「其他情况」。结果程序跑着跑着,遇到一个没覆盖到的值,直接行为未定义。

我曾经排查过一个bug,就是某个枚举类型新增了一个成员,但switch里没加对应的case,也没有default。程序就默默地跳过了所有处理逻辑,导致输出全乱套了。

/* 违规写法 */
switch (state) {
    case IDLE:
        /* 处理 */
        break;
    case BUSY:
        /* 处理 */
        break;
}

/* 合规写法 */
switch (state) {
    case IDLE:
        /* 处理 */
        break;
    case BUSY:
        /* 处理 */
        break;
    default:
        /* 错误处理或断言 */
        break;
}

规则4:禁止使用隐式类型转换

C语言有个「好心办坏事」的特性——隐式类型转换。比如把一个int赋值给uint8_t,编译器会自动截断,但不会告诉你。这种bug最难查,因为代码看起来完全没问题。

注意:MISRA-C要求所有类型转换必须显式写出。

/* 违规写法 */
uint8_t a = 300;  /* 隐式截断,a实际是44 */

/* 合规写法 */
uint8_t a = (uint8_t)300;  /* 显式转换,至少你知道自己在做什么 */

3.3 编码规范检查工具配置

光知道规则没用,得有个工具帮你自动检查。我常用的工具有PC-lint、Coverity、还有开源的Cppcheck。这里我以PC-lint为例,说说怎么配置MISRA-C 2012检查。

PC-lint配置步骤

  1. 下载MISRA-C 2012规则配置文件(通常叫au-misra-c2012.lnt)
  2. 在项目根目录创建lint配置文件,比如project.lnt
  3. 在配置文件中引用MISRA规则文件
// project.lnt 示例
// 引用MISRA-C 2012规则
-au-misra-c2012.lnt

// 指定检查的源文件
+os(linux)
-source "src/*.c"

// 忽略某些规则(有充分理由时)
-esym(551, my_function)  // 允许特定函数使用goto

配置好之后,运行lint命令:

lint-nt project.lnt

输出结果会告诉你每条违规的规则编号、位置和原因。嗯,第一次跑的时候,你可能会被几百条警告吓到。别慌,一条一条改就行。

3.4 常见违规示例

我整理了几个我在实际项目中遇到最多的违规类型,你看看自己中招了没。

违规类型 规则编号 典型代码 正确写法
未使用的变量 Dir 4.1 int x; /* 从未使用 */ 删除或添加注释说明
函数参数未使用 Dir 4.1 void foo(int a) { /* 未使用a */ } 用(void)a; 消除警告
布尔表达式中的赋值 Rule 13.2 if (x = 5) { ... } if (5 == x) { ... }
宏定义中的未加括号 Rule 20.7 #define MUL(a,b) a*b #define MUL(a,b) ((a)*(b))

避坑指南:我曾经在宏定义上吃过亏。写了个 #define SQUARE(x) x*x,然后调用 SQUARE(a+1),结果算出来是 a+1*a+1,完全不是我要的平方。从那以后,我写宏必加括号,里三层外三层那种。

3.5 我的建议

MISRA-C 2012的规则很多,你不需要一次性全部记住。我的做法是:

  • 先把强制规则过一遍,心里有个数
  • 配置好静态分析工具,让工具帮你查
  • 每次提交代码前,跑一遍检查,违规清零再提交

你想想看,如果每个嵌入式工程师都遵守这些规则,那些因为内存越界、指针乱飞导致的死机问题,至少能减少一半。嗯,至少我团队的数据是这样的。

小技巧:刚开始用MISRA-C的时候,别追求100%合规。先做到90%,剩下的10%可以写偏差申请(Deviation Request),说明为什么违规以及风险可控。我见过有些团队为了100%合规,把代码改得面目全非,反而引入了新bug。没必要。

好了,这一章就到这里。下一章我会讲怎么用静态分析工具做自动化检查,以及怎么处理那些「看起来违规但实际没问题」的特殊情况。