嵌入式C语言栈溢出防护:MPU分区配置+静态代码检查双重安全机制
栈溢出在MCU里最麻烦:它不报警,先把全局变量、堆管理块、函数返回地址踩坏,表现为偶发HardFault、参数随机跳变、看门狗无故复位。仅靠“把栈开大点”治标不治本,工程上要用两层防线——编译期静态检查筛高危代码,运行期MPU硬件拦截越界写。
一、MPU做栈底红区,越界即MemManage
Cortex-M3/M4/M7带MPU,可把“栈底再往下”一小段配成不可访问。向下增长的主栈/任务栈一旦越界,就会先碰到红区,触发MemManage而不是静默改内存。
#include "core_cm3.h"
#define STACK_BOTTOM 0x20001000u // 链接脚本里的栈最低地址
#define GUARD_SIZE 256u // 2^n,MPU区域必须2幂对齐
void mpu_stack_guard_init(void){
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk; // 使能MemManage
ARM_MPU_Disable();
/* 区域0:栈底红区,NO_ACCESS,禁止执行 */
ARM_MPU_SetRegion(0,
ARM_MPU_RBAR(0, STACK_BOTTOM - GUARD_SIZE),
ARM_MPU_RASR(1, ARM_MPU_AP_NONE, 0, 0, 0, 0,
ARM_MPU_XN, (31 - __CLZ(GUARD_SIZE-1)) - 1));
/* 区域1:正常栈区,读写、不可执行;大小按实际栈容量配 */
ARM_MPU_SetRegion(1,
ARM_MPU_RBAR(1, STACK_BOTTOM),
ARM_MPU_RASR(1, ARM_MPU_AP_FULL, 0, 1, 1, 0,
ARM_MPU_XN, (31 - __CLZ(0x1000-1)) - 1));
ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);
__DSB(); __ISB();
}
void MemManage_Handler(void){
/* 读CFSR/MMFAR,记录越界地址后安全处理 */
uint32_t mmfsr = SCB->CFSR & 0xFF;
uint32_t addr = (SCB->CFSR & (1u<<7)) ? SCB->MMFAR : 0;
(void)mmfsr; (void)addr;
while(1); // 实际项目写日志、切安全态、请求复位
}
要点:红区基址必须按区域大小2幂对齐;使能MPU后必须DSB+ISB,否则可能用旧属性取指。 RTOS多任务可把“任务栈区”配成独立region、“栈底红区”配成NO_ACCESS,切换任务时重载RNR/RBAR/RASR。
二、静态检查先筛“大栈帧/深调用/VLA”
MPU负责运行时拦截,但最好别等跑飞再修。GCC可输出每函数栈用量:
CFLAGS += -fstack-usage -Wstack-usage=256 -Wframe-larger-than=512 \
-fstack-protector-strong -Os
编译后每个目标文件生成.su,例如:
uart_parse.su: uart_parse 256 static
json_build.su: json_build 1024 dynamic,bounded
用脚本按调用链累加最坏路径:main→uart_parse→json_build若共占1.2KB,而任务栈只有1KB,就提前爆出问题。 对中断嵌套要单独建模:最高优先级ISR栈帧+被它调用的函数一并计入,避免“主循环没超、进中断就踩”。
静态规则建议写进CI:
禁止无深度上限的递归,树遍历改显式栈;
禁止VLA/alloca,大缓冲改静态池或环形缓冲;
单函数局部数组超256字节告警;
ISR内局部变量超32字节告警;
用Cppcheck/PC-Lint扫strcpy、大结构体按值传参、取址后越界写。
三、无MPU场景降级
M0/M0+没有MPU,用三招兜底:链接脚本在栈底填哨兵0xDEADBEEF,定时任务或空闲钩子校验;FreeRTOS开configCHECK_FOR_STACK_OVERFLOW=2用栈底填充法+溢出钩子;关键函数加-fstack-protector-strong插canary。 这些不如MPU硬实时,但能挡住大部分开发期问题。
四、落地顺序
先开-fstack-usage和栈告警,把递归、VLA、大局部数组清掉;按调用图+中断嵌套算各任务/主栈最坏深度,留30%余量配链接脚本;有MPU的芯片再配“栈区读写+XN、栈底红区NO_ACCESS”,MemManage里记录MMFAR;无MPU用哨兵+水位线。静态检查管“写代码别埋雷”,MPU管“真越界立刻停”,两套叠起来,栈相关幽灵Bug基本可归零。





