4. 页表详解:一级页表与二级页表
页表这东西,说白了就是 MMU 手里的地图。CPU 发出来的虚拟地址怎么找到物理地址?全靠页表来翻译。ARM 架构里页表分两级,一级页表管大块区域,二级页表管小块区域。我刚开始调 MMU 时,总觉得页表描述符格式特别绕,后来踩了几个坑才真正搞明白。
4.1 一级页表:Section 与 Page 的入口
一级页表,也叫 L1 页表。它是个 16KB 的表格,里面放了 4096 个描述符。每个描述符对应 1MB 的虚拟地址空间。你想想看,4GB 地址空间除以 4096,正好是 1MB。
一级页表描述符有几种类型:
- Fault(无效):访问这个地址直接触发异常
- Section(段):直接映射 1MB 的物理内存
- Page Table(页表):指向二级页表,做更细粒度的映射
- Supersection(超级段):映射 16MB,用于大块连续内存
一级页表描述符格式(32位 ARM)
位 [31:20]:Section 基地址(物理地址高 12 位)
位 [19:12]:保留 / 扩展属性
位 [11:9]:AP(访问权限)
位 [8:5]:Domain(域编号)
位 [4]:XN(不可执行)
位 [3:2]:C/B(缓存策略)
位 [1:0]:描述符类型(10 表示 Section)
我记得第一次配 Section 映射时,忘了设置 Domain 位,结果访问时一直报 permission fault。查了半天才发现,Domain 默认是 0,但对应的域访问控制寄存器没配好。嗯,这个坑我印象很深。
4.2 二级页表:Large Page 与 Small Page
一级页表粒度太粗,1MB 一块。很多时候我们需要更细的控制。这时候就用二级页表。
二级页表也叫 L2 页表,它本身也是个表格,里面放的是二级描述符。二级描述符支持四种粒度:
| 类型 | 大小 | 描述 |
|---|---|---|
| Large Page | 64KB | 大页,适合帧缓冲、大块数据 |
| Small Page | 4KB | 标准页,最常用 |
| 极小页 | 1KB | ARMv4/5 支持,新架构已废弃 |
| Fault | - | 无效条目 |
我个人的习惯是,能用 Small Page 就用 Small Page。4KB 粒度最灵活,也最容易调试。Large Page 虽然能减少 TLB miss,但分配起来容易产生内部碎片。
避坑指南:我曾经在一个视频项目里用了 Large Page 映射帧缓冲,结果发现缓存策略配错了,导致显示画面撕裂。后来换成 Small Page 逐页配置,问题才解决。所以大页虽好,但缓存属性一定要仔细配。
4.3 二级页表描述符格式
二级描述符的格式跟一级不太一样。以 Small Page 为例:
位 [31:12]:物理页帧号(PFN)
位 [11:6]:扩展属性(XN、APX、S、nG)
位 [5:4]:AP(访问权限)
位 [3:2]:C/B(缓存策略)
位 [1:0]:描述符类型(10 表示 Small Page)
这里有个关键点:二级描述符的物理地址是 4KB 对齐的。也就是说,位 [11:0] 不用来存地址,而是放属性。我见过有人直接把物理地址填进去,忘了做对齐,结果 MMU 翻译出来的地址全错了。
4.4 页表遍历流程
MMU 翻译地址时,会一级一级往下查。流程是这样的:
- 从 TTBR(页表基址寄存器)拿到一级页表的物理地址
- 用虚拟地址的 [31:20] 位做索引,找到一级描述符
- 如果一级描述符是 Section,直接得到物理地址
- 如果一级描述符指向二级页表,继续往下查
- 用虚拟地址的 [19:12] 位做索引,找到二级描述符
- 二级描述符给出物理页帧号,加上页内偏移得到最终地址
你想想看,一次地址翻译最多查两次表。如果 TLB 命中,连查表都省了。这就是为什么 TLB 对性能影响这么大。
注意:页表遍历过程中,如果遇到 Fault 描述符,MMU 会立即触发 Data Abort 或 Prefetch Abort。我在调试时经常用这个特性来检测非法地址访问。
4.5 页表配置实战要点
配页表时,有几个地方容易出错:
- 描述符类型位:位 [1:0] 必须正确。00 是 Fault,01 是 Page Table,10 是 Section/Small Page,11 是 Supersection/Large Page
- Domain 一致性:同一域内的页表,Domain 编号必须一致
- 缓存策略:C/B 位要跟内存类型匹配。普通内存用 Write-Back,设备内存用 Strongly-Ordered
- 权限控制:AP 位决定用户态和内核态的访问权限。我习惯把内核空间设成只读,用户空间设成读写
我曾经在一个项目里,把设备内存的 C/B 位配成了 Write-Back,结果外设寄存器读写总是不对。后来改成 Strongly-Ordered 才正常。说白了,页表不只是地址翻译,它还管着内存的访问属性和行为。
4.6 页表结构总览
下面这张图把一级页表和二级页表的关系画清楚了。你可以看到虚拟地址怎么一步步变成物理地址。
4.7 页表调试技巧
调试页表问题,我一般用这几招:
- 打印页表内容:在初始化完成后,把一级页表的前几个描述符打印出来,检查类型位和地址是否正确
- 验证地址翻译:手动算一个虚拟地址对应的物理地址,然后跟 MMU 翻译结果对比
- 检查对齐:页表基地址必须 16KB 对齐,描述符中的物理地址必须按粒度对齐
- 用 Fault 描述符做保护:在未使用的地址空间填 Fault 描述符,这样非法访问会立刻触发异常
个人经验:我习惯在页表初始化代码里加一段自检逻辑。遍历所有一级描述符,检查有没有无效的 Domain 编号或非对齐的地址。这招帮我抓出过好几个隐蔽的 bug。
页表配置是 MMU 调试的基础。搞清楚了描述符格式和遍历流程,后面再遇到地址翻译问题,你就能快速定位了。