车规级C语言内存防护:MPU配置杜绝关键功能的非法内存访问
车规MCU里指针越界、栈溢出、野指针写外设,光靠代码审查救不回来——破坏已经发生了。MPU的价值是把“静默写坏”变成“硬件立即拦下”:CPU发起访问前查区域权限,不匹配直接MemManage Fault,不会执行那条非法读写。
一、区域规划先于代码
Cortex-M的MPU区域数有限(通常8或16),先画内存地图再配:
Flash代码区:特权/用户只读、可执行
安全任务RAM:仅本任务读写、不可执行
外设寄存器区:仅特权访问、XN=1
栈底guard:无访问权限
未使用地址:默认拒绝
区域大小必须是2的幂,基地址按大小对齐,否则MPU会静默把基址截低。
二、配置流程:先关后开
改MPU必须先把CTRL.EN清0,配完再开,最后DSB+ISB生效:
void mpu_init(void){
MPU->CTRL = 0; // 1.关MPU
// Region0: Flash 只读可执行
MPU->RNR = 0;
MPU->RBAR = 0x08000000U | MPU_RBAR_VALID_Msk;
MPU->RASR = MPU_RASR_ENABLE_Msk
| (15 << MPU_RASR_SIZE_Pos) // 32KB
| MPU_RASR_AP_PRIV_RO_UNPRIV_RO
| MPU_RASR_TEX_0; // Normal
// Region1: 安全任务栈区 RW、不可执行
MPU->RNR = 1;
MPU->RBAR = TASK_RAM_BASE | MPU_RBAR_VALID_Msk;
MPU->RASR = MPU_RASR_ENABLE_Msk
| (12 << MPU_RASR_SIZE_Pos) // 4KB
| MPU_RASR_AP_PRIV_RW_UNPRIV_NO
| MPU_RASR_XN_Msk;
// Region2: 外设区 仅特权、设备属性、不可执行
MPU->RNR = 2;
MPU->RBAR = 0x40000000U | MPU_RBAR_VALID_Msk;
MPU->RASR = MPU_RASR_ENABLE_Msk
| (29 << MPU_RASR_SIZE_Pos) // 512MB
| MPU_RASR_AP_PRIV_RW_UNPRIV_NO
| MPU_RASR_XN_Msk
| MPU_RASR_TEX_0; // Device/Strongly-ordered
// Region3: 栈底guard 无访问
MPU->RNR = 3;
MPU->RBAR = STACK_GUARD_BASE | MPU_RBAR_VALID_Msk;
MPU->RASR = MPU_RASR_ENABLE_Msk
| (7 << MPU_RASR_SIZE_Pos) // 256B
| MPU_RASR_AP_PRIV_NO_UNPRIV_NO
| MPU_RASR_XN_Msk;
MPU->CTRL = MPU_CTRL_ENABLE_Msk
| MPU_CTRL_PRIVDEFENA_Msk; // 特权默认背景区
__DSB(); __ISB();
}
PRIVDEFENA开着方便内核/ISR访问未显式配置区;ASIL-D严配可关掉,全部显式建区。
三、栈guard别太小
guard区大小按“最大中断嵌套深度+最深调用”的栈消耗留余量,32~256字节都常见。
链接脚本里栈底留一块:
__attribute__((aligned(256)))
static uint8_t brake_task_stack[1024 + 256];
#define STACK_GUARD_BASE ((uint32_t)brake_task_stack)
栈一越界踩到guard,MemManage立刻触发,不会覆盖隔壁任务的校准变量。
四、外设区只给特权
刹车执行器、电机PWM、安全状态寄存器,非安全任务不该直接写。配成UNPRIV_NO后,用户态任务碰外设寄存器直接fault,只能通过系统调用/Trusted Function让OS代理写。
注意外设区TEX/C/B按芯片手册设成设备/强序,别开Cache,否则读写合并会出事。
五、MemManage处理:别“修复后继续”
车规场景下MPU触发=系统已偏离预期,正确动作是记录现场→进安全态→上报,不是改指针继续跑:
void MemManage_Handler(void){
uint32_t cfsr = SCB->CFSR;
uint32_t far = (cfsr & SCB_CFSR_MMARVALID_Msk)
? SCB->MMFAR : 0;
fault_log_store(cfsr, far, __builtin_return_address(0));
safe_state_enter(); // 关PWM/输出、置故障标志
wdog_trigger_reset(); // 或等外部看门狗
for(;;);
}
关键:
故障处理代码自身所在Flash区配成特权只读,不能被篡改
MMFAR有效位先判,避免读无效地址
别在handler里做复杂恢复,安全状态优先
六、RTOS多任务切换
每任务不同栈/数据区时,调度器切任务要重配MPU区域(或AUTOSAR OS由ProtectionHook接管)。
切换顺序:存当前→改RBAR/RASR→DSB/ISB→切栈。区域不够用时用重叠优先级:大区域默认拒绝,小区域按任务放开。
七、车规落坑
区域不对齐:以为配了0x20004000/48KB,实际基址被截到0x20000000
guard太小:中断嵌套一深直接跳过guard不触发
XN没开:栈区可执行,漏洞可被注入
PRIVDEFENA滥用:特权ISR写飞也无人拦截
非可信任务拿特权:CONTROL.nPRIV没切回去
只配MPU不处理异常:fault进Default_Handler等于没防
DMA越界MPU管不到:Cortex-M MPU只管CPU访问,DMA要走总线保护/SRI
MPU不是加密也不是沙箱,它干的事很明确:把内存访问策略钉成硬件规则,越界即异常、外设即特权、栈底即红线。车规C项目里配合静态分配、禁止malloc、栈水印,再把MemManage接进安全状态机,ISO 26262要求的“故障可发现、破坏可阻止”才算落地。





