2. MPU硬件原理:寄存器配置、Region定义与访问权限控制
好,咱们接着聊MPU的硬件原理。说实话,这部分内容我第一次接触时也觉得有点绕,但搞明白之后你会发现——其实就那么回事。
MPU的核心工作,说白了就是三件事:定义区域、设置权限、检查访问。咱们一个一个拆开讲。
2.1 MPU的寄存器配置
MPU的配置,本质上就是往几个关键寄存器里写值。我习惯把寄存器分成两类:控制类和区域类。
2.1.1 控制寄存器
以ARM Cortex-M系列为例,最核心的控制寄存器是MPU_CTRL。它的位定义大致如下:
| 位域 | 名称 | 功能描述 |
|---|---|---|
| [0] | ENABLE | 全局使能位。1使能,0禁用 |
| [1] | HFNMIENA | 硬fault和NMI下是否启用MPU。我建议默认置1 |
| [2] | PRIVDEFENA | 特权模式下是否使用默认内存映射。嗯,这里要注意 |
我在项目中遇到过一个问题:调试阶段把PRIVDEFENA置0了,结果系统一进中断就崩。后来才发现,中断服务函数默认是特权模式,但MPU没给这个模式配任何区域——直接触发异常。
MPU_CTRL的这三个位,能置1的全置1。
2.1.2 区域寄存器
每个Region由两个寄存器描述:MPU_RBAR(基址寄存器)和MPU_RASR(属性寄存器)。
MPU_RBAR的结构:
位[31:5] :基地址(必须32字节对齐)
位[4] :VALID位,写1表示当前操作立即生效
位[3:0] :REGION编号(0~15)
MPU_RASR的结构就复杂一些:
位[31:29] :属性(XN、AP等)
位[28:24] :TEX、S、C、B(缓存策略)
位[23:22] :SRD(子区域禁用)
位[21:19] :AP(访问权限)
位[18:17] :保留
位[16] :指令访问使能
位[15:8] :SIZE(区域大小)
位[7:0] :ENABLE(区域使能)
你想想看,这么多位,其实真正需要你关心的就几个:基地址、大小、访问权限、使能位。其他的,大部分场景用默认值就行。
2.2 Region的定义
Region就是一块连续的内存区域。每个Region必须满足:
- 大小是2的幂次方:比如256字节、1KB、4MB
- 基地址必须对齐到大小:比如4KB的Region,基地址必须是0x1000、0x2000这样的
- 最多支持16个Region:编号0~15
为什么必须是2的幂次方?我打个比方你就明白了。MPU内部是用一个掩码比较器来检查地址的。如果Region大小是2的幂,掩码就是连续的1,硬件实现起来特别简单。如果不是2的幂,那比较逻辑就复杂了,芯片面积和功耗都会增加。
Region的大小编码方式是这样的:
| SIZE编码 | 实际大小 | 对齐要求 |
|---|---|---|
| 8 | 256字节 | 0x100对齐 |
| 9 | 512字节 | 0x200对齐 |
| 10 | 1KB | 0x400对齐 |
| 11 | 2KB | 0x800对齐 |
| 12 | 4KB | 0x1000对齐 |
| ... | ... | ... |
| 31 | 4GB | 0x80000000对齐 |
公式很简单:实际大小 = 2^(SIZE编码)。比如SIZE=12,就是2^12=4096字节=4KB。
2.3 访问权限控制
访问权限由MPU_RASR中的AP位控制。AP位有3个比特,定义了8种权限组合:
| AP[2:0] | 特权模式 | 用户模式 | 典型用途 |
|---|---|---|---|
| 000 | 无访问 | 无访问 | 保留区域,谁都不让碰 |
| 001 | 读写 | 无访问 | 内核私有数据 |
| 010 | 读写 | 只读 | 系统配置参数 |
| 011 | 读写 | 读写 | 共享数据区 |
| 100 | 只读 | 无访问 | 内核代码区 |
| 101 | 只读 | 只读 | 只读数据(如查表) |
| 110 | 只读 | 无访问 | 同100,保留 |
| 111 | 只读 | 只读 | 同101,保留 |
这里有个细节要注意:AP=000 表示完全禁止访问。我在项目中就用这个来保护未使用的内存区域,防止野指针乱窜。
另外,MPU_RASR里还有个XN位(Execute Never),用来控制能否从该区域取指令执行。对于数据区,一定要把XN位置1,否则攻击者可能通过缓冲区溢出注入代码执行。
- 检查是实时的,没有额外开销
- 检查是强制的,无法绕过
- 检查是精确的,会触发精确异常
2.4 配置示例
说了这么多理论,咱们看个实际配置。假设我们要保护一段4KB的SRAM区域,基地址0x20000000,只允许特权模式读写:
// 配置Region 0
// 基地址:0x20000000,大小:4KB,特权读写,用户无访问
MPU->RNR = 0; // 选择Region 0
MPU->RBAR = 0x20000000; // 基地址
MPU->RASR = (0x01 << 19) | // AP=001:特权读写
(0x00 << 16) | // XN=0:允许执行(如果是数据区,建议置1)
(12 << 8) | // SIZE=12:4KB
(1 << 0); // ENABLE=1:使能该Region
// 全局使能MPU
MPU->CTRL = (1 << 0) | // ENABLE
(1 << 1) | // HFNMIENA
(1 << 2); // PRIVDEFENA
配置完成后,任何用户模式下的代码试图访问0x20000000~0x20000FFF这个区域,都会触发MemManage Fault。这就是MPU的保护能力。
好了,MPU的硬件原理就讲到这里。下一节咱们聊聊怎么在实际项目中把这些Region组织起来,形成完整的内存保护方案。