第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 页表描述符结构总览

下面这张图把一级和二级页表描述符的核心结构画出来了,方便你对照着看:

ARM 页表描述符结构总览 一级段描述符(Section,1MB) Bit[31:20] 物理基址 | Bit[19:12] 域+AP+属性 | Bit[11:2] 缓存+权限 | Bit[1:0] = 10 一级粗页表描述符(Coarse Page Table) Bit[31:10] 二级页表基址 | Bit[9:2] 域+属性 | Bit[1:0] = 11 二级小页描述符(Small Page,4KB) Bit[31:12] 物理页基址 | Bit[11:6] AP+属性 | Bit[5:2] 缓存+XN | Bit[1:0] = 10 属性位展开(以段描述符为例) Bit[11:10] AP | Bit[9] TEX[0] | Bit[8] S | Bit[7] nG | Bit[6:5] Domain[3:2] | Bit[4] Domain[1] | Bit[3:2] C+B | Bit[1:0] 类型 域控制(Domain) CP15:c3 寄存器配置 16 个域:00=No Access | 01=Client(按 AP 检查)| 11=Manager(直接通过) 每个页表描述符的 Domain[3:0] 选择对应域

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:数据区可执行,给攻击者留了后门

页表描述符的每个位都有它的用途。配错了,轻则性能下降,重则系统崩溃。我建议你每次配完页表后,用调试器读一下内存,确认描述符的值跟预期一致。别问我为什么知道要这么做——问就是吃过亏。