15、安全编码规范:MISRA C、CERT C编码标准介绍,静态分析工具的使用
说到C语言的安全编码,我不得不先讲个真实的故事。几年前我接手过一个嵌入式项目,前任工程师留下了一堆代码。编译没问题,功能也跑得通,但总觉得哪里不对劲。后来我用静态分析工具扫了一遍,好家伙,光MISRA C违规就报了三百多条。其中有一条是隐式类型转换,看起来人畜无害,但恰恰就是这条规则,在特定硬件上会导致数据截断——嗯,那个bug我们查了整整两周。
所以今天这章,我想跟你聊聊安全编码标准这件事。说白了,就是一群人把踩过的坑、流过的血,总结成了一套规则。你照着写,大概率不会出事;你不照着写,那就得靠运气了。
15.1 为什么需要安全编码标准?
你想想看,C语言有多灵活?指针随便转、类型随便换、内存随便操作。这种自由度在写玩具程序时很爽,但在工业级产品里,那就是灾难的温床。
我个人的习惯是:在项目启动的第一天,就选定一套编码标准。别等到代码写了两万行再回头改,那时候改的成本,够你重新写一遍了。
目前业界最主流的两个标准,就是MISRA C和CERT C。它们的目标其实一样:消除未定义行为,减少程序员犯错的可能。
核心观点:安全编码标准不是限制你的创造力,而是帮你把精力花在真正需要思考的业务逻辑上,而不是跟编译器的未定义行为斗智斗勇。
15.2 MISRA C 标准详解
MISRA C最早是汽车行业搞出来的,后来发现太好用了,航空航天、医疗设备、工业控制全都在用。它的核心理念就一句话:能不用就不用,能静态确定就别动态。
15.2.1 MISRA C 的核心规则分类
MISRA C 2012版本把规则分成了三类:
| 类别 | 含义 | 违反后果 |
|---|---|---|
| Directive(指令) | 需要人工判断的规则 | 建议遵守,但工具无法完全自动检查 |
| Rule(规则) | 可被工具自动检查的规则 | 必须遵守,否则视为违规 |
| Advisory(建议) | 推荐遵守,但允许有理由的偏离 | 需要记录偏离理由 |
我记得刚接触MISRA时,觉得有些规则简直不可理喻。比如「禁止使用动态内存分配」——不用malloc我怎么做链表?后来在嵌入式项目里吃了亏才明白:在资源受限的系统里,malloc失败是常态,不是异常。
15.2.2 几条必须记住的MISRA规则
我挑几条在项目中踩过坑的,给你重点说说:
- Rule 10.1:不允许隐式的整型类型转换。我曾经见过一个bug,uint16_t赋值给uint8_t,数据直接截断,传感器读数永远不对。后来加了显式强制转换,问题解决。
- Rule 12.1:表达式的副作用必须明确。比如
i = i++这种写法,不同编译器结果不同,MISRA直接禁止。 - Rule 17.2:函数不能直接或间接调用自身。递归在MISRA里是禁区,因为栈深度不可控。我见过一个递归深度超过1000次的案例,程序直接崩了。
⚠️ 特别注意:MISRA C不是银弹。它只能消除已知的未定义行为,但不能保证你的业务逻辑正确。我曾经见过完全符合MISRA标准的代码,但算法本身就是错的——工具不会帮你检查这个。
15.3 CERT C 编码标准
CERT C是卡内基梅隆大学软件工程研究所搞的,跟MISRA比,它更侧重于安全漏洞的防御。说白了,MISRA管的是「怎么写更规范」,CERT C管的是「怎么写才不会被黑客利用」。
15.3.1 CERT C 的规则体系
CERT C把规则按漏洞类型分类,每个规则都有一个唯一的ID。比如:
| 规则ID | 描述 | 对应常见漏洞 |
|---|---|---|
| STR31-C | 确保字符串以空字符结尾 | 缓冲区溢出 |
| ARR30-C | 不要越界访问数组 | 内存破坏 |
| MEM30-C | 不要访问已释放的内存 | Use-After-Free |
| INT32-C | 确保整数运算不溢出 | 整数溢出漏洞 |
我个人觉得CERT C比MISRA更贴近实际开发。因为MISRA很多规则是「一刀切」的禁止,而CERT C会告诉你:如果你非要这么做,那必须加哪些防护。
15.3.2 一个典型的CERT C案例
拿整数溢出来说,CERT C的INT32-C规则要求:任何整数运算前,必须检查是否可能溢出。
我曾经在项目中写过这样的代码:
// 不安全的写法
int multiply(int a, int b) {
return a * b; // 如果a和b都很大,结果可能溢出
}
// 符合CERT C的写法
int multiply_safe(int a, int b) {
if (a > 0 && b > 0 && a > INT_MAX / b) {
// 处理溢出错误
return -1;
}
return a * b;
}
你看,多了一行检查,代码就安全了。这就是CERT C的精髓——不是禁止你做事,而是让你做事之前先想清楚后果。
💡 我的建议:如果你的项目是面向互联网的、有安全攻击面的,优先采用CERT C。如果是嵌入式、功能安全相关的,MISRA C更合适。当然,两个都遵守最好——我现在的项目就是MISRA + CERT C双标准。
15.4 静态分析工具的使用
光有标准不行,你得有工具帮你检查。人工代码审查虽然重要,但人眼总有看漏的时候。静态分析工具就是你的第二双眼睛——而且它不会累,不会走神。
15.4.1 主流静态分析工具对比
| 工具名称 | 支持标准 | 适用场景 | 价格 |
|---|---|---|---|
| PC-lint Plus | MISRA C/C++、CERT C | 嵌入式、汽车电子 | 商业收费 |
| Coverity | CERT C、OWASP | 大型企业级项目 | 商业收费 |
| Cppcheck | 部分MISRA规则 | 个人/小团队 | 免费开源 |
| Clang Static Analyzer | CERT C | LLVM生态项目 | 免费开源 |
我个人最常用的是PC-lint Plus,虽然贵,但它的MISRA规则覆盖最全。小项目的话,Cppcheck也够用——我刚开始学安全编码时,就是用Cppcheck练手的。
15.4.2 静态分析工具的工作流程
下面这张图展示了静态分析工具在开发流程中的位置:
15.4.3 使用静态分析工具的避坑指南
我曾经犯过一个错误:把静态分析工具当成「一次性检查」。项目快结束了才跑一遍,结果出来上千条警告,改都改不完。
后来我学乖了,总结了几条经验:
- 尽早集成:在项目的第一天就把工具配置好,每次提交代码前自动运行。
- 不要盲目清零:有些规则可以合理偏离。比如MISRA禁止使用标准库的某些函数,但如果你有充分的理由,可以申请偏离——记得写注释说明原因。
- 配置规则集:不要全开所有规则。根据项目类型,选择适用的规则子集。比如嵌入式项目可以关掉一些关于文件操作的规则。
- 结合人工审查:工具只能检查语法和模式,检查不了设计意图。我见过完全通过静态分析的代码,但逻辑上就是错的——这种只能靠人眼。
总结一下:MISRA C和CERT C是两套经过实战检验的安全编码标准。静态分析工具是落地这些标准的最佳帮手。三者结合,能帮你把90%以上的常见漏洞挡在门外。剩下的10%,靠的是经验、测试和——嗯,一点运气。
📌 行动建议:如果你现在还没有使用任何编码标准,我建议从CERT C的TOP 10规则开始。先挑最容易出问题的几条(比如整数溢出、缓冲区溢出),用Cppcheck跑一遍现有代码。相信我,你会被结果吓一跳——然后你就会主动去学MISRA了。
公众号:蓝海资料掘金营,微信deep3321