第1章:内存Region属性配置——地址范围、大小、访问权限、子Region与XN

各位同学,咱们今天聊点实在的。

内存Region的配置,说白了就是给芯片的各个内存区域「贴标签」。告诉它:这块地方能读、那块能写、还有一块连代码都不能跑。我刚开始做Bootloader时,觉得这玩意儿不就是配几个寄存器嘛,后来踩了坑才明白——配错了,系统直接挂给你看。

1.1 地址范围与大小:划地盘要精准

每个Region,你得告诉芯片两件事:从哪开始,到哪结束。

地址范围通常用基地址(Base Address)大小(Size)来描述。大小必须是2的幂次,而且要对齐。比如你配一个64KB的Region,基地址就得是64KB的整数倍。为什么?因为硬件就是这么设计的,你想想看,地址解码器查起来方便。

核心公式:

Region结束地址 = 基地址 + 大小 - 1

大小必须为2^n,且基地址必须对齐到大小

我在项目中遇到过一件事:有人配了一个1MB的Region,基地址写了0x08000000,大小写了0x100000(1MB)。看起来没问题对吧?但实际芯片的Flash只有512KB。结果呢?访问到后半段直接触发异常。嗯,这里要注意——Region不能超出物理内存范围,否则就是给自己挖坑。

1.2 访问权限:读、写、执行,三权分立

每个Region都有三个基本权限:

  • 读(Read):能不能从这块地址取数据
  • 写(Write):能不能往这块地址存数据
  • 执行(Execute):能不能从这块地址取指令

这三者可以任意组合。比如:

  • 代码区:读+执行,不能写(防止代码被篡改)
  • 数据区:读+写,不能执行(防止数据区被当成代码执行)
  • 外设寄存器区:读+写,不能执行(外设地址跑代码?想都别想)

我的个人习惯:

只要不是代码区,一律关掉执行权限。这是最基础的安全策略,没有之一。

我曾经调试过一个产品,莫名其妙跑飞了。查了两天,最后发现是数据区的一个数组被写入了跳转指令,然后CPU误执行了它。从那以后,我所有项目的数据区都强制加上XN(禁止执行)标记。

1.3 子Region:精细化管理

有些芯片支持把一个Region再切成8个等份的子Region。每个子Region可以独立配置权限。

举个例子:你有一个256KB的SRAM Region,想把它分成8块,每块32KB。前4块给任务A用,后4块给任务B用。任务A的代码只能读写自己的4块,不能碰任务B的。这时候子Region就派上用场了。

子Region配置要点:

  • 子Region数量固定为8个
  • 每个子Region大小相等(Region大小/8)
  • 可以单独使能/禁用某个子Region
  • 禁用后,访问该子Region会触发异常

我建议你在设计内存布局时,提前规划好子Region的用途。别等到代码写完了再回头改,那叫一个痛苦。

1.4 禁止执行(XN):防代码注入的护城河

XN,全称是eXecute Never。这个位一旦置1,CPU就不能从这块Region取指令执行。

为什么要搞这个?你想想看,如果攻击者往你的数据缓冲区里塞了一段恶意代码,然后想办法让PC指针跳过去……没有XN保护的话,你的系统就沦陷了。

警告:

以下区域必须设置XN:

  • 所有RAM数据区(堆、栈、全局变量)
  • 外设寄存器地址空间
  • 非易失性存储器的数据分区(如EEPROM模拟区)
  • 任何不需要执行代码的内存区域

我记得有一次帮客户做安全审计,发现他们的Bootloader居然把整个Flash都配成了可执行。我说这不等于把家门钥匙挂门口吗?改完之后,XN位一设,安全等级直接拉满。

1.5 实战配置示例

咱们拿ARM Cortex-M系列的MPU来举个例。假设系统有512KB Flash和128KB SRAM:

/* Region 0: Flash代码区 - 只读+可执行 */
MPU->RBAR = (0x08000000) | (0 << 4);  // 基地址
MPU->RASR = (0x1 << 0)   // 使能
          | (0x3 << 1)   // 全访问权限
          | (0x1 << 28)  // XN=0,允许执行
          | (0x12 << 1); // 大小=512KB

/* Region 1: SRAM数据区 - 读写+禁止执行 */
MPU->RBAR = (0x20000000) | (1 << 4);  // 基地址
MPU->RASR = (0x1 << 0)   // 使能
          | (0x3 << 1)   // 全访问权限
          | (0x0 << 28)  // XN=1,禁止执行
          | (0x10 << 1); // 大小=128KB

这段代码里,Flash区可以读、可以执行,但不能写(防止运行时篡改代码)。SRAM区可以读写,但不能执行。嗯,这就是最基础的内存保护配置。

1.6 避坑指南

最后,我把自己这些年踩过的坑总结一下:

  • 大小对齐问题:Region大小必须是2的幂,基地址必须对齐到大小。我曾经配了个3MB的Region,结果硬件直接忽略,因为3不是2的幂。
  • 子Region重叠:两个Region如果有重叠,优先级高的Region会覆盖优先级低的。我建议你画个内存布局图,一目了然。
  • XN位误清:有些芯片复位后XN位默认是0(允许执行)。如果你忘了设置,数据区就是可执行的。我习惯在初始化代码里显式设置所有Region的XN位,哪怕它本来就是0。
  • 调试时别太激进:刚开始调试时,权限可以放宽一点。等系统稳定了,再收紧权限。否则你连printf都跑不了,排查问题会非常痛苦。

我的小技巧:

在Bootloader里加一个内存Region打印函数,把当前所有Region的配置dump出来。调试时看一眼,就知道哪里配错了。这个习惯帮我省了至少几十个小时的调试时间。

好了,这一章的内容就到这儿。Region配置是内存保护的基础,你把它搞明白了,后面的章节学起来会轻松很多。下一章咱们聊聊「特权级与用户级访问控制」,到时候见。