4. 内存保护单元(MPU):MPU工作原理、区域划分与权限控制、MPU配置实战

4.1 MPU到底在保护什么?

说实话,很多做嵌入式开发的工程师,对MPU的理解就是「一个开关,打开就完事了」。但我在项目中吃过亏——有一次产品在高温环境下频繁死机,查了三天才发现是任务栈溢出,把关键数据区给踩了。如果当时配好了MPU,这个问题半小时就能定位。

MPU的全称是Memory Protection Unit,内存保护单元。它不是一个独立芯片,而是集成在处理器内部的一个硬件模块。它的核心职责就一句话:谁可以访问哪块内存,做什么操作

你想想看,如果没有MPU,任何代码都可以随意读写任何地址。任务A的栈可以覆盖任务B的栈,用户程序可以篡改系统配置寄存器。这在安全敏感的场景下,简直是灾难。

核心要点:MPU不是用来提高性能的,它是用来保证系统健壮性和安全性的。说白了,它就是个硬件级别的「门禁系统」。

4.2 MPU的工作原理

MPU的工作机制其实不复杂。它维护一组配置寄存器,每个寄存器定义了一个内存区域的属性。每次CPU发起内存访问时,MPU会在硬件层面检查这次访问是否合法。

检查流程大致如下:

  1. CPU发出地址和访问类型(读/写/执行)
  2. MPU将地址与已配置的区域进行匹配
  3. 如果匹配到某个区域,检查权限是否允许
  4. 允许则放行,不允许则触发异常

这里有个关键点:匹配规则是优先匹配最具体的区域。如果地址落在多个区域的重叠部分,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知识体系总览 内存保护单元MPU 工作原理 地址匹配 → 权限检查 命中则放行,否则异常 优先匹配最具体区域 区域划分 代码区:R+X 数据区:R+W 外设区:特权模式 系统区:仅特权 权限控制 读/写/执行 特权级/用户级 区域使能/禁止 配置实战 RNR选择区域编号 RBAR设置基地址 RASR配置属性 避坑指南 大小必须2的幂 基地址对齐 避免区域重叠 配置后加屏障 基于ARM Cortex-M系列,适用于Cortex-R及部分RISC-V实现

MPU这个东西,配置起来不复杂,但影响面很大。配错了系统直接挂,配对了能帮你挡住很多潜在的bug和安全漏洞。我个人建议,在产品开发的早期就把MPU加进去,而不是等到出了问题再补。因为后期加MPU,往往要改大量的代码逻辑,成本高得多。

好了,这一章的内容就到这里。记住:MPU不是锦上添花,而是雪中送炭。在安全关键系统中,它是必需品,不是可选项。


公众号:蓝海资料掘金营,微信deep3321