26、安全编码标准:MISRA C、CERT C编码标准、SEI CERT规则、行业最佳实践

说到安全编码标准,我猜很多朋友第一反应是:“这东西是不是只有航空航天、汽车电子才需要?” 嗯,我以前也这么想。直到有一次,我在一个物联网项目里,因为一个整数溢出导致设备远程崩溃,客户半夜打电话来骂人…… 从那以后,我彻底明白了:安全编码不是“加分项”,而是“保命符”

今天咱们就聊聊三个最主流的编码标准:MISRA C、CERT C 和 SEI CERT 规则。它们不是互相替代的关系,而是各有侧重。你想想看,一个搞汽车电子的工程师,和一个写网络协议栈的工程师,他们担心的“安全”能一样吗?

核心观点: 安全编码标准不是束缚你的枷锁,而是帮你避开已知陷阱的地图。我个人的习惯是:先理解规则背后的“为什么”,再决定“怎么用”。

安全编码标准知识体系 MISRA C 汽车/嵌入式/强约束 CERT C 通用安全/漏洞预防 SEI CERT 规则+建议/可裁剪 禁止动态内存分配 严格类型匹配 缓冲区溢出预防 整数安全/格式化字符串 规则分级(必须/建议) 可裁剪/可配置 共同基础:避免未定义行为 指针安全 · 类型安全 · 资源管理 · 控制流完整性 行业最佳实践:静态分析 + 代码审查 + 自动化测试

26.1 MISRA C:嵌入式领域的“铁律”

MISRA C 最早是英国汽车工业协会搞出来的。说白了,就是给 C 语言戴上“紧箍咒”。为什么?因为汽车里一个刹车控制器的代码,绝对不能出半点差错。我参与过一个汽车 ECU 项目,客户明确要求:所有代码必须通过 MISRA C 检查,违规数必须为零

MISRA C 的核心思想是:消除未定义行为,强制类型安全。它把规则分成“必需(Required)”和“建议(Advisory)”两类。我个人觉得,最值得关注的是这几条:

  • 规则 1.1: 所有代码必须符合 C 标准,不能依赖编译器扩展。嗯,这条我吃过亏——用了某个编译器的内建函数,换平台直接编译不过。
  • 规则 8.2: 函数必须有显式的参数类型声明。别写 int foo(); 这种“空参数列表”,要写 int foo(void);
  • 规则 17.4: 禁止使用 goto。这条争议很大,但说实话,在嵌入式里你几乎找不到非用 goto 不可的场景。
  • 规则 21.1: 禁止使用 malloc/free。动态内存分配在嵌入式里是“洪水猛兽”,碎片化问题会让你崩溃。

我的小建议: 如果你刚开始接触 MISRA C,别想着一次性通过所有规则。先打开静态分析工具(比如 PC-Lint、Coverity),把“必需”级别的违规修掉。剩下的“建议”规则,可以结合项目实际情况慢慢来。

26.2 CERT C 编码标准:通用安全的“百科全书”

CERT C 是卡内基梅隆大学 SEI 实验室搞的。它不像 MISRA 那样“专治嵌入式”,而是面向所有 C 语言开发者。它的规则覆盖了:预处理器、声明、表达式、整数、字符串、内存管理、输入输出、信号、并发 等方方面面。

我记得有一次,团队里一个新同事写了一段代码:

char buf[10];
gets(buf);  // 经典缓冲区溢出

我一看就头大了。CERT C 的 STR31-C 规则明确说了:gets() 是“死亡函数”,必须用 fgets() 替代。为什么?因为 gets() 不检查输入长度,用户输入超过 10 个字符,栈就直接被踩烂了。

CERT C 把规则分成三个等级:

等级 含义 举例
L1(严重) 可被利用导致安全漏洞 STR31-C:禁止使用 gets()
L2(高危) 可能导致未定义行为 INT32-C:确保整数运算不溢出
L3(建议) 提高代码可维护性 DCL31-C:声明变量时尽量靠近使用处

我个人最看重的是 INT32-C(整数安全)。你想想看,一个 32 位有符号整数,最大值是 2147483647。如果你给它加 1,它就变成了 -2147483648。这在金融计算、传感器数据处理里,简直是灾难。

注意: CERT C 的规则不是“死规定”。它允许你通过“风险评估”来豁免某些规则。但前提是:你必须书面记录为什么豁免,以及采取了什么补偿措施。我曾经在一个项目里豁免了“禁止使用 setjmp/longjmp”的规则,因为那个模块的异常处理确实需要它。但我们在代码审查时反复确认了使用场景。

26.3 SEI CERT 规则:可裁剪的“工具箱”

SEI CERT 规则其实是 CERT C 的“升级版”或“姊妹篇”。它更强调 规则的可裁剪性。什么意思呢?就是同一个规则,在不同的安全等级下,要求可以不一样。

举个例子,关于“指针使用”的规则:

  • 在安全关键系统(比如医疗设备)里,禁止使用函数指针。
  • 在普通商业软件里,建议对函数指针进行非空检查。
  • 在原型验证阶段,可以使用函数指针,但必须加注释说明。

SEI CERT 还引入了“规则优先级”的概念:

  1. P1(必须): 违反即产生安全漏洞。比如“禁止解引用空指针”。
  2. P2(强烈建议): 违反可能导致未定义行为。比如“禁止使用未初始化的变量”。
  3. P3(建议): 违反降低代码质量。比如“避免深层嵌套”。

我在实际项目中,通常这样使用 SEI CERT:先按 P1 规则过一遍代码,修掉所有“必须”违规。然后针对 P2 规则,结合静态分析工具的结果,逐条确认。P3 规则嘛…… 嗯,有时间就改,没时间就记在 TODO 里。

26.4 行业最佳实践:怎么落地?

光知道标准没用,关键是怎么落地。我总结了一套“三步走”的方法:

  1. 选标准: 根据行业选。汽车电子选 MISRA C,通用软件选 CERT C,高安全场景选 SEI CERT。也可以混搭——比如用 MISRA C 的规则 + CERT C 的整数安全规则。
  2. 上工具: 静态分析工具是必须的。我个人推荐 PC-Lint(老牌)、Coverity(商业)、Cppcheck(开源)。别指望人工审查能发现所有问题——人眼会疲劳,工具不会。
  3. 建流程: 把安全编码检查嵌入到 CI/CD 流水线里。每次提交代码,自动跑一遍规则检查。违规数超过阈值,直接拒绝合并。

避坑指南: 我曾经在一个项目里,团队为了“通过 MISRA 检查”,疯狂加类型转换来消除警告。结果代码变得又臭又长,反而引入了新的 bug。记住:安全编码标准的目的是写出更安全的代码,而不是让工具闭嘴。如果某个规则导致代码可读性严重下降,停下来想一想:有没有更好的写法?

最后说一句:安全编码不是“一次性工作”。随着编译器升级、新漏洞被发现,标准本身也在更新。MISRA C 从 1998 年第一版,到现在已经更新到 2023 版了。保持学习,保持警惕——这才是防御式编程的真正内核。