4. 内存保护单元(MPU):MPU工作原理、区域划分与权限控制、MPU配置实战
4.1 MPU到底在保护什么?
说实话,很多做嵌入式开发的工程师,对MPU的理解就是「一个开关,打开就完事了」。但我在项目中吃过亏——有一次产品在高温环境下频繁死机,查了三天才发现是任务栈溢出,把关键数据区给踩了。如果当时配好了MPU,这个问题半小时就能定位。
MPU的全称是Memory Protection Unit,内存保护单元。它不是一个独立芯片,而是集成在处理器内部的一个硬件模块。它的核心职责就一句话:谁可以访问哪块内存,做什么操作。
你想想看,如果没有MPU,任何代码都可以随意读写任何地址。任务A的栈可以覆盖任务B的栈,用户程序可以篡改系统配置寄存器。这在安全敏感的场景下,简直是灾难。
核心要点:MPU不是用来提高性能的,它是用来保证系统健壮性和安全性的。说白了,它就是个硬件级别的「门禁系统」。
4.2 MPU的工作原理
MPU的工作机制其实不复杂。它维护一组配置寄存器,每个寄存器定义了一个内存区域的属性。每次CPU发起内存访问时,MPU会在硬件层面检查这次访问是否合法。
检查流程大致如下:
- CPU发出地址和访问类型(读/写/执行)
- MPU将地址与已配置的区域进行匹配
- 如果匹配到某个区域,检查权限是否允许
- 允许则放行,不允许则触发异常
这里有个关键点:匹配规则是优先匹配最具体的区域。如果地址落在多个区域的重叠部分,MPU会选择编号最小的那个区域进行权限检查。我在项目中就踩过这个坑——两个区域重叠了,结果权限配置冲突,导致程序莫名其妙地触发异常。
注意:如果地址没有匹配到任何已配置的区域,MPU默认会拒绝访问。这意味着你必须显式配置所有需要访问的内存区域,包括代码区、数据区、外设寄存器区等。我曾经见过有人只配了RAM区,忘了配Flash区,结果程序一跑就进异常。
4.3 区域划分与权限控制
MPU的区域划分,说白了就是把内存空间切成若干块,每块单独设置访问权限。以ARM Cortex-M系列为例,它支持8个或16个内存区域,每个区域可以独立配置:
| 配置项 | 说明 | 典型值 |
|---|---|---|
| 基地址 | 区域的起始地址 | 0x20000000 |
| 大小 | 区域的大小,必须是2的幂 | 0x10000 (64KB) |
| 可读 | 是否允许读操作 | 是/否 |
| 可写 | 是否允许写操作 | 是/否 |
| 可执行 | 是否允许取指令 | 是/否 |
| 特权级 | 仅在特权模式下可访问 | 是/否 |
我个人习惯把系统分成这几个区域:
- 代码区(Flash):可读、可执行,不可写。防止代码被篡改。
- 数据区(SRAM):可读、可写,不可执行。防止栈溢出攻击。
- 外设区:根据外设需求配置,通常只允许特权模式访问。
- 系统配置区:比如SCB、NVIC等,只允许特权模式读写。
经验之谈:我建议把任务栈放在独立区域,每个任务一个区域。这样一旦某个任务栈溢出,MPU会立刻触发异常,而不是悄悄破坏其他任务的数据。调试时能省下大量时间。
4.4 MPU配置实战
光说不练假把式。我们直接看代码。以下是一个基于ARM Cortex-M4的MPU配置示例,使用CMSIS库:
// 配置MPU区域0:Flash代码区
MPU->RNR = 0; // 选择区域0
MPU->RBAR = 0x08000000; // 基地址:Flash起始
MPU->RASR = (0x1C << 1) | // 大小:256KB (2^(1C+1))
(0x1 << 16) | // 可执行:允许
(0x1 << 17) | // 可读:允许
(0x0 << 18) | // 可写:禁止
(0x1 << 24); // 使能该区域
// 配置MPU区域1:SRAM数据区
MPU->RNR = 1;
MPU->RBAR = 0x20000000;
MPU->RASR = (0x19 << 1) | // 大小:128KB
(0x0 << 16) | // 不可执行
(0x1 << 17) | // 可读
(0x1 << 18) | // 可写
(0x1 << 24);
// 配置MPU区域2:外设寄存器区
MPU->RNR = 2;
MPU->RBAR = 0x40000000;
MPU->RASR = (0x1D << 1) | // 大小:512KB
(0x0 << 16) | // 不可执行
(0x1 << 17) | // 可读
(0x1 << 18) | // 可写
(0x1 << 28); // 仅特权模式
// 使能MPU
MPU->CTRL = (0x1 << 0) | // 使能MPU
(0x1 << 2); // 在异常处理中使用默认映射
这段代码配置了三个区域。注意看区域2的配置——我加了特权模式限制。这意味着用户态代码无法直接访问外设寄存器,必须通过系统调用。这在RTOS中非常有用,可以有效防止用户任务乱搞硬件。
配置完成后,记得做一次内存屏障:
__DSB(); // 数据同步屏障
__ISB(); // 指令同步屏障
为什么要加这两条?因为MPU配置是通过总线写入的,CPU可能不会立即生效。不加屏障的话,下一条指令可能还在用旧的MPU配置,导致不可预测的行为。我曾经就因为这个bug调试了两天。
4.5 避坑指南
嗯,这里我要说几个常见的坑:
- 区域大小必须是2的幂,而且基地址必须对齐到区域大小。比如64KB的区域,基地址必须是64KB的整数倍。这个限制很多人第一次配的时候会忽略。
- 不要配重叠区域,除非你非常清楚优先级规则。重叠区域的权限取编号较小的那个,但不同芯片的实现可能有差异。
- 中断服务程序也要受MPU约束。如果ISR访问了未配置的区域,一样会触发异常。所以记得给中断向量表和ISR用到的数据区配好权限。
- 调试器可能绕过MPU。有些调试接口可以直接读写内存,不受MPU限制。这在调试时是好事,但生产环境要禁用调试接口。
重要提醒:我曾经在一个项目中,因为MPU配置错误导致看门狗喂狗失败——喂狗寄存器的地址没配进MPU区域。结果系统不断复位,产品根本跑不起来。所以配完MPU后,一定要把所有外设地址都检查一遍。
4.6 知识体系总览
下面这张图总结了MPU的核心知识结构,我建议你保存下来,配置时对照着看:
MPU这个东西,配置起来不复杂,但影响面很大。配错了系统直接挂,配对了能帮你挡住很多潜在的bug和安全漏洞。我个人建议,在产品开发的早期就把MPU加进去,而不是等到出了问题再补。因为后期加MPU,往往要改大量的代码逻辑,成本高得多。
好了,这一章的内容就到这里。记住:MPU不是锦上添花,而是雪中送炭。在安全关键系统中,它是必需品,不是可选项。
公众号:蓝海资料掘金营,微信deep3321