4. MMU 控制寄存器:SCTLR_EL1/EL2/EL3 的关键位及其对启动的影响
系统控制寄存器(System Control Register, SCTLR)是 ARMv8-A 架构中控制 MMU、缓存、对齐检查等核心功能的寄存器。在启动流程中,正确配置 SCTLR 的各个位域,是 MMU 安全启用、页表正确遍历的前提。不同异常级别(EL1/EL2/EL3)拥有独立的 SCTLR 实例,但其位域定义基本一致。
4.1 SCTLR 寄存器概览
SCTLR 是一个 64 位寄存器,但启动阶段最常关注的位集中在低 32 位。下表列出了对 MMU 初始化和启动流程影响最大的关键位:
| 位域 | 名称 | 默认值(Reset) | 功能描述 | 启动影响 |
|---|---|---|---|---|
| [0] | M | 0 (EL1/EL2/EL3) | MMU 使能位。0=禁用,1=启用。 | MMU 开关,必须在页表就绪后置 1。 |
| [2] | C | 0 | 数据缓存(Data Cache)使能位。0=禁用,1=启用。 | 启动早期必须关闭,防止脏数据回写。 |
| [12] | I | 0 (EL1/EL2/EL3) | 指令缓存(Instruction Cache)使能位。0=禁用,1=启用。 | 可在页表建立前开启以加速取指。 |
| [19] | WXN | 0 | 写权限不可执行(Write permission implies eXecute Never)。0=允许写且可执行,1=写权限页面自动标记为不可执行。 | 增强安全性,防止代码被篡改后执行。 |
| [25] | EE | 由硬件复位决定 | 异常处理时的端序(Endianness)。0=小端,1=大端。 | 影响异常向量表和数据访问的字节序。 |
| [1] | A | 0 | 对齐检查使能。0=禁用,1=启用。 | 启动阶段通常关闭,避免非对齐访问触发异常。 |
| [3] | SA | 0 | 栈指针对齐检查。0=禁用,1=启用。 | SP 必须 16 字节对齐,否则触发异常。 |
启动经验: 在 MMU 启用前,务必确保 C 位和 M 位为 0。如果从 Boot ROM 跳转到裸机代码时,I 位可能已被 Boot ROM 置 1,此时无需额外关闭指令缓存。
4.2 关键位深度解析
4.2.1 M 位:MMU 总开关
M 位是 MMU 的全局使能位。当 M=0 时,处理器直接使用物理地址访问内存,所有页表遍历逻辑被旁路。当 M=1 时,MMU 开始根据页表基地址寄存器(TTBR0_EL1/TTBR1_EL1)进行地址翻译。
- 启动顺序: 必须先建立页表、配置 MAIR 和 TCR,最后才置位 M 位。
- 同步要求: 修改 M 位后,必须执行
ISB(指令同步屏障)指令,确保后续指令在 MMU 开启后的新地址空间下执行。 - EL 差异: 在 EL3 下修改 SCTLR_EL3.M 会影响整个系统;在 EL2 下修改仅影响非安全世界;在 EL1 下修改影响当前 OS。
4.2.2 C 位与 I 位:缓存控制
缓存一致性是启动阶段最容易出问题的环节。
- C 位(数据缓存): 启动时必须为 0。因为页表通常存放在内存中,如果数据缓存使能,页表修改可能只停留在缓存中,MMU 遍历时读到的是过时的内存数据。此外,外设寄存器访问必须绕过缓存。
- I 位(指令缓存): 可以在页表建立前开启。开启 I 位能显著加速从 Flash 或 ROM 中取指。但需要注意,如果代码是自修改的(如动态加载),修改后必须执行
IC IALLU使指令缓存失效。
常见陷阱: 在关闭 MMU 的情况下,如果 C 位被意外置 1,CPU 可能会从缓存中读取数据,导致外设寄存器或内存映射 IO 访问异常。务必在 MMU 初始化前执行
DC CISW 或 DC ZVA 清理数据缓存。
4.2.3 WXN 位:写权限与执行权限互斥
WXN 位提供了一种硬件级别的安全保护。当 WXN=1 时,任何具有写权限的内存区域(页表描述符中的 AP[2:1] 允许写入)都会被 MMU 自动标记为不可执行(XN=1)。这意味着即使页表描述符中 XN 位为 0,只要该区域可写,CPU 就无法从中取指。
- 启动阶段: 通常建议在 MMU 启用后立即设置 WXN=1,防止栈溢出或堆破坏后执行恶意代码。
- 与 PXN 的区别: PXN(Privileged Execute Never)限制特权级执行,而 WXN 限制可写区域的执行,两者互补。
4.2.4 EE 位:端序控制
EE 位控制异常处理时的数据访问端序。ARMv8 架构支持在 EL 级别混合使用大端和小端,但启动阶段通常保持与复位默认值一致。
- 影响范围: 异常向量表、页表遍历时的描述符读取、系统寄存器访问等。
- 启动建议: 除非有特殊的大端需求,否则保持 EE=0(小端)。修改 EE 位后,必须重新建立页表,因为页表描述符的字节序会改变。
4.3 SCTLR 配置流程与 EL 差异
不同异常级别下,SCTLR 的复位值和可访问性不同。下表总结了 EL1/EL2/EL3 在典型启动场景下的配置差异:
| 异常级别 | 典型启动场景 | SCTLR 复位值(关键位) | 启动时需修改的位 | 注意事项 |
|---|---|---|---|---|
| EL3 | Secure Monitor / ATF | M=0, C=0, I=0, EE=硬件决定 | 设置 I=1(可选),配置 EE,最后置 M=1 | EL3 页表通常使用 4KB 粒度,TCR_EL3 需单独配置 |
| EL2 | Hypervisor (KVM / Xen) | M=0, C=0, I=0, EE=0 | 设置 I=1,配置 WXN=1,置 M=1 | EL2 页表用于 stage-2 翻译,需与 EL1 页表协同 |
| EL1 | Linux Kernel / RTOS | M=0, C=0, I=0, EE=0 | 设置 I=1,C=1(MMU 开启后),WXN=1,置 M=1 | EL1 使用 TTBR0_EL1 和 TTBR1_EL1 双页表基址 |
4.4 SCTLR 配置代码示例(ARM64 汇编)
以下代码展示了在 EL1 下安全配置 SCTLR_EL1 的典型步骤:
/* 假设页表已建立,TTBR0_EL1 和 MAIR_EL1 已配置 */
/* 步骤 1: 读取当前 SCTLR_EL1 */
mrs x0, sctlr_el1
/* 步骤 2: 清除 M、C、A 位,确保 MMU 和缓存关闭 */
bic x0, x0, #(1 << 0) /* 清除 M 位 */
bic x0, x0, #(1 << 2) /* 清除 C 位 */
bic x0, x0, #(1 << 1) /* 清除 A 位 */
/* 步骤 3: 设置 I 位和 WXN 位 */
orr x0, x0, #(1 << 12) /* 设置 I 位:开启指令缓存 */
orr x0, x0, #(1 << 19) /* 设置 WXN 位:写权限不可执行 */
/* 步骤 4: 写入 SCTLR_EL1 */
msr sctlr_el1, x0
isb /* 指令同步屏障,确保配置生效 */
/* 步骤 5: 启用 MMU(置位 M 位) */
orr x0, x0, #(1 << 0) /* 设置 M 位 */
msr sctlr_el1, x0
isb /* 关键:ISB 保证后续指令在 MMU 开启后执行 */
/* 此时 MMU 已启用,PC 开始使用虚拟地址 */
关键点: 上述代码中,M 位和 C 位是分两步设置的。这是因为在 MMU 关闭时,数据缓存必须禁用;而在 MMU 开启后,可以立即启用数据缓存(C 位)。实际启动中,通常会在 MMU 开启后、进入 C 代码前,再单独设置 C 位。
4.5 SCTLR 配置流程图
下图展示了从复位到 MMU 启用过程中,SCTLR 关键位的状态变化流程:
4.6 启动阶段 SCTLR 配置检查清单
在编写启动代码时,建议按照以下清单逐一确认 SCTLR 的状态:
- 复位后立即检查: 确认 M=0, C=0, I=0(或由 Boot ROM 设置),A=0。
- 页表建立前: 执行数据缓存清理(
DC CISW),确保缓存中无脏数据。 - 页表建立后: 配置 TTBR、MAIR、TCR,但此时仍保持 M=0。
- MMU 启用前: 设置 I=1(可选),WXN=1(推荐),EE 根据平台需求设置。
- MMU 启用时: 先写 SCTLR 清除 M 位(确保状态),再写 SCTLR 置位 M 位,中间插入 ISB。
- MMU 启用后: 立即设置 C=1(数据缓存使能),并再次执行 ISB。
- 安全检查: 读取 SCTLR 回读验证 M 位和 C 位是否已置位。
调试提示: 如果 MMU 启用后发生异常(如同步外部中止),首先检查 SCTLR 的 M 位是否被意外清除,或者页表所在内存区域的缓存策略是否配置正确。使用 JTAG 读取 SCTLR 和 FAR_EL1(错误地址寄存器)是定位问题的第一步。