物联网终端固件越界怎么防:Cortex-M3 MPU 八个区域七步配置

网关固件里 Modbus、MQTT、OTA 挤在一个地址空间,一个野指针就能改写协议栈缓冲,而软件长度检查只能降低概率、划不出边界。Cortex-M3 的 MPU 用 8 个区域给内核访问画硬边界,本文讲清 RASR 的 SIZE、AP、XN、SRD 位定义与 MemManage 处理,附寄存器级配置代码与七步清单。

2026-10-21
Cortex-M3MPU存储器保护MemManage物联网终端嵌入式

物联网网关的固件越写越大,Modbus、MQTT、OTA、日志四拨代码挤在一个地址空间里,一个野指针就能把协议栈的缓冲改写掉。结论先给:Cortex-M3 的 MPU 能把地址空间切成最多 8 个区域,用 RASR 的 AP、XN、SIZE 位分别控制"能不能读写、能不能取指、覆盖多大",越界访问会在指令执行的当下触发 MemManage 故障,比事后校验缓冲区可靠得多。前提是芯片真的集成了 MPU——先读 MPU_TYPE 的 DREGION 位确认。

一、软件断言为什么挡不住越界

场景很常见:一台 4G 网关同时跑采集任务和 MQTT 任务,OTA 升级后偶发死机。日志显示 MQTT 任务的栈被写花,但代码里每个缓冲区都有长度检查。问题在于,长度检查是"写之前"的软件判断,野指针、数组下标算错、结构体成员偏移变化都可能绕过检查;更麻烦的是中断或 DMA 在检查之后、写入之前插进来。软件检查只能降低概率,不能划出边界。

MPU 的思路不一样:它在总线上给每段地址配一张"通行证",内核每次取指、读写都先比对区域属性,不合规就当场产生 MemManage 故障并记录故障地址。代价是要提前规划内存布局,而且 MPU 只管内核访问,DMA 不受它约束。

二、先确认有没有 MPU:8 个区域从哪来

寄存器地址作用关键位
MPU_TYPE0xE000ED90类型寄存器,复位值 0x00000800DREGION[15:8]=0x08 表示 8 个区域;为 0 表示未集成 MPU
MPU_CTRL0xE000ED94总开关与背景区控制ENABLE[0]、HFNMIENA[1]、PRIVDEFENA[2]
MPU_RNR0xE000ED98区域号选择REGION[7:0]
MPU_RBAR0xE000ED9C区域基址ADDR[31:N]、VALID[4]、REGION[3:0]
MPU_RASR0xE000EDA0区域属性与大小XN[28]、AP[26:24]、SRD[15:8]、SIZE[5:1]、ENABLE[0]

MPU_CTRL 的三个位各有分工:ENABLE 是总开关;PRIVDEFENA 决定特权模式下未被区域覆盖的部分是否按默认存储器映射放行;HFNMIENA 决定 HardFault、NMI 与 FAULTMASK 升级期间 MPU 是否继续生效。复位值全为 0,也就是上电默认关闭、无背景区、故障处理中不启用 MPU。

三、SIZE 编码:32 字节到 4GB

SIZE 编码区域大小典型用途
b0010032B单个寄存器块、标志位
b00111256B小型 DMA 描述符
b010011KB通信帧缓冲
b010114KB任务栈
b0110116KB日志区
b0111164KBSRAM 数据区
b10001256KB程序区(小容量型号)
b100111MB程序区(大容量型号)
b101014MB外部 QSPI Flash 映射
b111114GB整片地址空间背景区

区域大小不是直接写字节数,而是写 5 位编码,实际大小等于 2(SIZE+1)。最小 32B(b00100),最大 4GB(b11111),中间每加 1 翻一倍。基址必须按区域大小对齐:64KB 区域只能落在 64KB 的整数倍地址上,否则地址低位无效,配置行为不可预测。

四、AP 与 XN:把权限拆成三件事

AP[2:0]特权许可用户许可典型用途
b000不可访问不可访问彻底封死的保留区
b001读/写不可访问外设寄存器,只允许内核特权访问
b010读/写只读共享参数区,任务只读、系统可写
b011读/写读/写普通 SRAM 数据区
b101只读不可访问特权只读区
b110只读只读程序区、校准表
b111只读只读只读区(与 b110 等效)

XN=1 禁止从该区域取指,这一位在防跑飞上价值最大——数据缓冲区、栈、外设区都应该设 XN。这样函数指针被改写后跳过去,触发的是取指故障,而不是执行一段随机数据。

外设区还要注意 TEX、C、B 三个属性位:它们决定存储器类型与缓存策略,寄存器访问通常配成"器件/强序",避免总线重排把读写顺序打乱。TEX=000、C=0、B=1 对应共享器件属性,是外设区常用的组合。

五、SRD:一个区域切 8 份

SRD[15:8] 把区域等分成 8 个子区域,某位置 1 就禁能对应的 1/8,最低位对应第一段。它适合处理"大区域里挖一个小洞"的情况:比如 512KB 的 SRAM 区域,底部 64KB 要留给另一块属性不同的缓冲区,把 SRD 设成 b11111110 即可。注意 32B、64B、128B 三种小区域不支持子区域,设置了结果不可预测。

六、PRIVDEFENA:一开 MPU 就死机的元凶

PRIVDEFENA=1 时,特权访问没有被任何区域覆盖的部分,按默认存储器映射放行;等于 0 时没有区域覆盖就拒绝访问。很多人只配了两三个区域就把 ENABLE 置 1,结果中断向量、系统区、未规划的 SRAM 全部触发 MemManage,系统立刻死。稳妥的顺序是:先把 Flash、SRAM、外设、栈区都配好,再打开 ENABLE;调试阶段可以先置 PRIVDEFENA=1 做兜底,量产再收紧。

还有一个细节:使能 MPU 后,系统分区和向量表始终可访问,这是硬件保证的;但用户模式代码如果没有对应区域,连自己的代码都取不了。RTOS 里切到用户任务前,必须先把任务代码区、数据区、栈区配全。

七、MemManage 处理:别让它升级成 HardFault

SHCSR(0xE000ED24)的 MEMFAULTENA(bit16)必须置 1,否则 MPU 违例不会被 MemManage 接管,而是升级为 HardFault,HFSR 的 FORCED 位置 1,故障地址还在 MMAR 里,但处理入口变了。MemManage 处理函数里读 MMFSR:IACCVIOL 表示取指违例,DACCVIOL 表示数据访问违例,MMARVALID=1 时 MMAR(0xE000ED34)给出触发地址。

产品上的处理策略不建议"记录后继续跑":越界已经发生,数据可能已被改写。更稳妥的做法是记录 PC 与 MMAR、把当前任务标记为异常、停止输出并进入安全状态,由看门狗复位。

八、七步配置清单

步骤动作要点
1读 MPU_TYPEDREGION=8 才继续;为 0 则该芯片无 MPU
2规划区域列出每段的基址、大小、读写与取指权限
3对齐基址基址必须是区域大小的整数倍
4写 RNR/RBAR/RASR先选区号,再写基址与属性
5开 MEMFAULTENASHCSR bit16 置 1
6置 PRIVDEFENA 后 ENABLE先兜底再收紧,避免一开就死
7故意越界验证写只读区、从数据区取指,确认能进 MemManage

九、一份可直接套用的区域规划

区号地址范围大小SIZEAPXN用途
00x08000000~0x0807FFFF512KBb10010b1100程序区,只读可执行
10x20000000~0x2000FFFF64KBb01111b0111SRAM 数据区,可读写不可取指
20x20010000~0x20010FFF4KBb01011b0111主栈,独立区域便于发现溢出
30x40000000~0x4001FFFF128KBb10000b0011外设区,仅特权访问
40x1FFFF000~0x1FFFF7FF2KBb01010b1101系统存储与 UID,只读
#define MPU_CTRL  (*(volatile uint32_t *)0xE000ED94UL)
#define MPU_RNR   (*(volatile uint32_t *)0xE000ED98UL)
#define MPU_RBAR  (*(volatile uint32_t *)0xE000ED9CUL)
#define MPU_RASR  (*(volatile uint32_t *)0xE000EDA0UL)

/* size_code:5 位大小编码;ap:3 位许可;xn:1 禁止取指 */
static void mpu_region(uint32_t no, uint32_t base,
                       uint32_t size_code, uint32_t ap, uint32_t xn)
{
    MPU_RNR  = no;
    MPU_RBAR = base & 0xFFFFFFE0UL;        /* 清掉 VALID 与 REGION */
    MPU_RASR = (xn << 28) | (ap << 24) |
               (size_code << 1) | 1UL;      /* bit0 区域使能 */
}

void mpu_init(void)
{
    mpu_region(0, 0x08000000UL, 0x12, 0x6, 0); /* Flash 512KB */
    mpu_region(1, 0x20000000UL, 0x0F, 0x3, 1); /* SRAM 64KB */
    mpu_region(2, 0x20010000UL, 0x0B, 0x3, 1); /* 栈 4KB */
    mpu_region(3, 0x40000000UL, 0x10, 0x1, 1); /* 外设 128KB */

    SCB->SHCSR |= (1UL << 16);               /* 使能 MemManage */
    MPU_CTRL = (1UL << 2) | 1UL;              /* PRIVDEFENA + ENABLE */
}

十、四个常见坑

基址没对齐。区域大小 64KB,基址却写成 0x20008000,低 15 位无法被硬件忽略,配置结果不可预测。规划时先把大小定下来,再把基址向下取整到大小边界。

区域重叠的优先级。MPU 允许区域重叠,编号大的区域优先级高,会覆盖低编号区域。利用这一点可以把"小洞"放在高编号区域,而不必拆掉大区域。

把栈区配成只读。异常压栈会写栈,只读权限会直接产生 MSTKERR,表现成"一进中断就死"。栈区必须是可读写。

以为 DMA 也受 MPU 管。MPU 只约束内核发起的访问,DMA 控制器直接访问存储器,绕过 MPU。指望 MPU 防住 DMA 越界,方向就错了——DMA 要靠描述符校验和源/目的地址检查。

一句话总结:MPU 不是"更严格的编译器检查",而是给内核访问画硬边界:8 个区域、SIZE 编码、AP 与 XN 三个维度,配好之后越界会在当场触发 MemManage 并留下地址。先把区域规划表和基址对齐做对,再谈权限收紧。

本文基于《Cortex-M3 技术参考手册》第 9 章(存储器保护单元)与第 8 章(故障状态寄存器)整理优化。

需要定制开发?

沧州艾诺威电子 — 国家高新技术企业,20+项国家专利
嵌入式系统开发 · 物联网方案 · AI智能硬件 · 一站式交付

立即微信咨询

电话:13930711029 | 邮箱:tech@czinv.com | 24小时内响应