规则详解-基础篇:禁止使用动态内存分配(Rule 20-4),禁止使用setjmp/longjmp(Rule 20-7)
好,我们进入基础篇的两个硬核规则。说实话,这两条规则在MISRA C里属于那种「新手觉得无所谓,老手看了直摇头」的类型。我最早接触MISRA时也觉得「不用malloc?那我链表怎么办?」——后来在项目里吃了几次亏,才明白这些规则背后全是血泪教训。
Rule 20-4:禁止使用动态内存分配
这条规则禁止的不仅仅是malloc和free,还包括calloc、realloc以及C11的aligned_alloc。说白了,堆上分配内存这件事,在MISRA合规的嵌入式项目里是禁区。
为什么禁止?
我直接说三个最要命的问题:
- 内存碎片化:嵌入式系统往往连续运行几个月甚至几年。频繁分配释放,堆会碎成一片一片的。我曾经在一个工控项目里见过,系统跑了72小时后,明明还有30%的堆空间,但
malloc一个128字节的块就是失败——因为找不到连续的空闲块。 - 分配失败无保障:
malloc返回NULL时你怎么办?很多代码直接忽略检查,或者检查了也不知道怎么处理。安全关键系统里,「分配失败」必须是一个可预测的状态,而不是运行时才暴露的炸弹。 - 释放时机不确定:谁负责释放?什么时候释放?double-free、use-after-free、内存泄漏——这些bug在静态分析阶段很难全部抓出来。
核心观点:动态内存分配引入了运行时不确定性,而MISRA C追求的是「编译时就能确定行为」。静态分配虽然死板,但安全。
替代方案
不用动态分配,那数据结构和变长需求怎么处理?我列几个实际项目里常用的做法:
| 需求场景 | 替代方案 | 说明 |
|---|---|---|
| 变长数组 | 静态数组 + 最大长度宏 | 定义#define MAX_BUF_SIZE 256,实际使用uint8_t buf[MAX_BUF_SIZE],用变量记录当前有效长度 |
| 链表/队列 | 静态池(Static Pool) | 预分配固定数量的节点数组,运行时从池中取用,用完归还 |
| 动态字符串 | 固定长度字符数组 | 比如char name[32],截断或返回错误,不接受变长 |
| 运行时对象创建 | 状态机 + 预分配对象 | 所有可能用到的对象在初始化时全部创建好,运行时切换状态 |
我的习惯:静态池是我最推荐的方案。你预先算好最大并发数,分配一个数组,自己实现pool_alloc和pool_free。这样既避免了堆碎片,又保留了「分配/释放」的语义。我在一个车载ECU项目里就是这么做的,跑了三年没出过内存问题。
代码示例:静态池 vs malloc
/* 违反MISRA:使用动态内存分配 */
#include <stdlib.h>
typedef struct {
uint32_t id;
uint8_t data[64];
} Message;
Message* create_message(uint32_t id) {
Message* msg = (Message*)malloc(sizeof(Message));
if (msg != NULL) {
msg->id = id;
}
return msg; /* 调用者必须记得free,否则泄漏 */
}
/* 合规方案:静态池 */
#define POOL_SIZE 10
static Message msg_pool[POOL_SIZE];
static bool pool_used[POOL_SIZE] = {false};
Message* pool_alloc(void) {
for (int i = 0; i < POOL_SIZE; i++) {
if (!pool_used[i]) {
pool_used[i] = true;
return &msg_pool[i];
}
}
return NULL; /* 池满,分配失败 */
}
void pool_free(Message* msg) {
for (int i = 0; i < POOL_SIZE; i++) {
if (&msg_pool[i] == msg) {
pool_used[i] = false;
return;
}
}
/* 如果走到这里,说明传入的指针不属于池,属于逻辑错误 */
}
避坑指南:我曾经在一个项目里看到有人用「全局大数组 + 自己写malloc」来绕过规则检查。嗯,这其实只是把堆搬到了BSS段,碎片化问题一个都没少。MISRA禁止的是「动态分配」这个行为模式,不是禁止某个函数名。你手写的分配器如果也是动态管理,照样违规。
Rule 20-7:禁止使用setjmp/longjmp
这条规则禁止<setjmp.h>里的setjmp和longjmp。这两个函数提供了一种「非局部跳转」——你可以从深层嵌套的函数调用里直接跳回某个保存点。
为什么禁止?
你想想看,longjmp本质上是一个不受控制的goto,而且它能跨越函数边界。这会带来几个严重问题:
- 资源泄漏:跳转时,中间函数栈上的局部变量不会被正常析构。如果那些函数打开了文件、申请了互斥锁、分配了资源——全泄漏了。我记得有个同事在通信协议栈里用了
longjmp做错误恢复,结果每次异常跳转都漏掉一个socket句柄,系统跑了半天就报「打开文件过多」。 - 栈状态不可预测:
longjmp恢复的是保存点的栈帧,但中间栈帧已经被破坏了。如果你在中间函数里修改了全局变量或硬件寄存器,跳回去之后这些状态是错乱的。 - 静态分析失效:控制流图被
longjmp打碎,静态分析工具无法准确追踪变量生命周期和路径覆盖。MISRA合规要求可分析性,longjmp直接让分析工具「瞎了」。
一句话总结:setjmp/longjmp把结构化编程的「单入单出」原则彻底打破了。你无法从代码文本上看出执行路径会怎么跳,调试时也根本没法复现。
替代方案
不用longjmp,那错误处理和异常恢复怎么做?
| 场景 | 替代方案 | 说明 |
|---|---|---|
| 深层错误恢复 | 返回错误码 + 逐层传递 | 每个函数返回状态码,调用者检查并决定是否继续 |
| 超时/中断处理 | 状态机 + 定时器回调 | 用状态机管理流程,超时事件触发状态切换,而不是跳转 |
| 异常退出 | 全局错误标志 + 主循环检查 | 设置一个全局错误变量,主循环在每次迭代时检查并跳转到恢复点 |
我建议:如果你觉得「逐层返回错误码」太啰嗦,可以试试「集中式错误处理」模式——定义一个goto error_handler标签,所有错误路径都跳到这里做清理。但注意,这个goto只能在同一个函数内跳转,不能跨函数。这比longjmp安全得多,静态分析也能处理。
代码示例:longjmp vs 状态机
/* 违反MISRA:使用setjmp/longjmp */
#include <setjmp.h>
static jmp_buf env;
void process_data(void) {
if (some_error) {
longjmp(env, 1); /* 直接跳回主函数 */
}
/* 正常处理 */
}
int main(void) {
if (setjmp(env) == 0) {
process_data();
} else {
/* 错误恢复 */
cleanup();
}
}
/* 合规方案:状态机 + 错误码 */
typedef enum {
STATE_INIT,
STATE_PROCESS,
STATE_ERROR,
STATE_DONE
} State_t;
State_t run_workflow(void) {
State_t state = STATE_INIT;
ErrorCode_t err = ERR_NONE;
while (state != STATE_DONE) {
switch (state) {
case STATE_INIT:
err = init_subsystem();
state = (err == ERR_NONE) ? STATE_PROCESS : STATE_ERROR;
break;
case STATE_PROCESS:
err = process_data();
state = (err == ERR_NONE) ? STATE_DONE : STATE_ERROR;
break;
case STATE_ERROR:
cleanup();
state = STATE_DONE; /* 或者重试逻辑 */
break;
default:
/* 不应到达 */
break;
}
}
return state;
}
避坑指南:有些RTOS提供了类似longjmp的任务级跳转机制,比如FreeRTOS的taskEXIT_CRITICAL配合taskYIELD做异常恢复。嗯,这些本质上也是非局部跳转。MISRA的精神是「控制流要可预测」,所以这类机制在合规项目里同样不推荐。用状态机吧,虽然代码量多一点,但每个路径都是可见的、可测试的。
知识结构总览
下面这张图把两条规则的核心逻辑串起来了。你可以看到,两条规则都指向同一个目标——消除运行时不确定性。
这两条规则,一个管「资源怎么分配」,一个管「控制流怎么跳」。它们看起来是独立的,但底层逻辑一致——让代码的行为在写出来的时候就能被完整理解。动态内存分配让资源生命周期变得模糊,longjmp让执行路径变得不可追踪。去掉它们,你的代码会「笨」一点,但会「稳」很多。
我的经验:刚开始做MISRA合规时,团队里最大的阻力就是这两条规则。大家都觉得「不用malloc怎么写程序?」「不用longjmp怎么处理深层错误?」——但坚持下来之后,我们发现代码的可读性、可测试性和可靠性都上了一个台阶。说白了,安全关键系统不需要「灵活」,需要的是「确定」。