第4章 页表描述符详解:页表项格式、属性位、访问权限、域控制
页表描述符,说白了就是 MMU 用来做地址翻译的「字典条目」。每个条目告诉硬件:这个虚拟页对应哪个物理页,谁可以访问它,以及要不要缓存。我刚开始接触 ARM MMU 时,总觉得这些位域太琐碎,后来调一个外设驱动死活跑不起来,才发现是描述符的属性位配错了。嗯,从那以后我再也不敢小看它了。
4.1 一级页表描述符格式
ARM 的一级页表描述符有 32 位,高 12 位是物理地址基址,低 20 位是属性控制。我个人习惯把描述符分成三类:
- 段描述符(Section):映射 1MB 的块,适合大块内存映射
- 粗页表描述符(Coarse):指向二级页表,二级页表管理 4KB 页
- 细页表描述符(Fine):指向二级页表,但管理 1KB 页(ARMv5 之后基本不用了)
关键位域一览:
- Bit[1:0]:描述符类型标识(00=无效,10=段/粗页表,01=细页表,11=页)
- Bit[31:20]:段描述符的物理基址(高 12 位)
- Bit[19:12]:粗页表描述符的页表基址
- Bit[11:2]:访问权限、域、缓存属性等控制位
4.2 二级页表描述符格式
二级页表描述符管理更细粒度的映射。ARM 支持三种二级页表项:
| 类型 | 粒度 | 适用场景 |
|---|---|---|
| 大页(Large Page) | 64KB | 帧缓冲、大块数据缓冲区 |
| 小页(Small Page) | 4KB | 通用内存映射,最常用 |
| 极小页(Tiny Page) | 1KB | ARMv5 之前使用,现在基本废弃 |
二级页表描述符的 Bit[1:0] 同样标识类型:00=无效,01=大页,10=小页,11=极小页。Bit[31:16] 或 Bit[31:12] 存放物理页基址,具体取决于页大小。
4.3 属性位详解
属性位是 MMU 配置中最容易出问题的地方。我遇到过好几次,明明映射对了,但程序就是跑飞,最后发现是缓存属性没配对。
4.3.1 C 和 B 位(缓存与写缓冲)
C 位控制是否启用数据缓存(Data Cache),B 位控制是否启用写缓冲(Write Buffer)。组合起来有四种模式:
| C | B | 内存类型 | 典型用途 |
|---|---|---|---|
| 0 | 0 | 无缓存、无写缓冲(Strongly Ordered) | 外设寄存器、DMA 描述符 |
| 0 | 1 | 无缓存、有写缓冲(Normal, Non-cacheable) | 共享内存、核间通信 |
| 1 | 0 | 写通(Write-Through) | 视频帧缓冲、需要一致性场景 |
| 1 | 1 | 写回(Write-Back) | 普通 RAM,性能最优 |
我曾经踩过的坑:把外设寄存器映射成 Write-Back 模式,结果读回来的值永远是缓存里的旧数据,外设状态根本更新不了。外设寄存器一定要配成 Strongly Ordered(C=0, B=0)。
4.3.2 XN 位(不可执行)
XN(eXecute Never)位是 ARMv6 之后引入的安全特性。置 1 表示该页不允许执行代码。我建议把所有数据页的 XN 位置 1,防止代码注入攻击。
4.3.3 TEX 位(类型扩展)
TEX[2:0] 三位与 C、B 位配合,定义更精细的内存类型。ARMv7 之后,TEX 位用来选择内存属性编码表。说实话,大部分场景用默认值就够了,除非你在调 GPU 或多媒体驱动。
4.4 访问权限控制
ARM 的访问权限由 AP(Access Permission)位控制,配合 S(System)和 R(ROM)位一起使用。AP 位有两位:
| AP[1:0] | 特权模式 | 用户模式 |
|---|---|---|
| 00 | 无访问 | 无访问 |
| 01 | 读写 | 无访问 |
| 10 | 读写 | 只读 |
| 11 | 读写 | 读写 |
你想想看,如果用户态程序能随便改内核数据,系统不就崩了吗?所以内核空间通常配 AP=01,用户空间配 AP=11。
我的习惯:在调试阶段先把所有页配成 AP=11,功能调通后再收紧权限。这样能快速定位是权限问题还是逻辑问题。
4.5 域控制(Domain)
域控制是 ARM MMU 的一个独特机制。每个页表描述符有一个 4 位的域编号(0~15),对应 CP15 寄存器 c3 中的域配置。域配置有三种模式:
- 00(No Access):任何访问都产生域异常
- 01(Client):按页表描述符中的 AP 位检查权限
- 11(Manager):不检查权限,直接通过
域控制的好处是:你可以把一组页表项归到同一个域,然后通过修改 CP15:c3 快速开关整个域的访问。我在做系统休眠唤醒时就用过这个技巧——把外设内存域配成 No Access,休眠前切过去,省得一个个清页表。
4.6 页表描述符结构总览
下面这张图把一级和二级页表描述符的核心结构画出来了,方便你对照着看:
4.7 实战配置示例
假设我们要映射一个 4KB 的 UART 寄存器空间,要求:特权模式可读写,用户模式不可访问,无缓存,不可执行。代码看起来像这样:
/* 二级页表描述符配置 */
#define L2_SMALL_PAGE_AP_PRIV_RW (0x1 << 10) /* AP=01 */
#define L2_SMALL_PAGE_C_B (0x0 << 2) /* C=0, B=0 */
#define L2_SMALL_PAGE_XN (0x1 << 0) /* XN=1 */
#define L2_SMALL_PAGE_TYPE (0x2) /* 小页类型=10 */
uint32_t l2_entry = (phys_addr & 0xFFFFF000) |
L2_SMALL_PAGE_AP_PRIV_RW |
L2_SMALL_PAGE_C_B |
L2_SMALL_PAGE_XN |
L2_SMALL_PAGE_TYPE;
/* 写入二级页表 */
l2_page_table[virt_idx] = l2_entry;
小技巧:写页表项时,最后再写 Bit[1:0] 的类型位。因为 MMU 看到类型位有效后,就会立即使用这个描述符。先配好其他位,最后「激活」它,能避免中间状态导致异常。
4.8 常见配置错误
- 域号越界:Domain[3:0] 只有 4 位,范围 0~15,写 16 以上会截断
- AP 位与域模式冲突:域配成 Manager 时 AP 位无效,容易误以为权限没生效
- 缓存属性不一致:同一段物理内存映射到不同虚拟地址时,缓存属性必须一致,否则数据一致性会出问题
- XN 位忘记置 1:数据区可执行,给攻击者留了后门
页表描述符的每个位都有它的用途。配错了,轻则性能下降,重则系统崩溃。我建议你每次配完页表后,用调试器读一下内存,确认描述符的值跟预期一致。别问我为什么知道要这么做——问就是吃过亏。