4、页表项格式:ARM64页表项(PTE)的位布局
ARM64 架构的页表项(PTE)是一个 64 位的数据结构,它不仅是虚拟地址到物理地址的映射桥梁,还携带了内存属性、访问权限、状态标志等关键信息。Linux 内核在遵循 ARM 架构规范的基础上,对部分软件位进行了自定义,以实现更精细的内存管理。理解 PTE 的位布局是掌握页表遍历、缺页异常处理以及内存属性控制的基础。
4.1 ARM64 PTE 总体位布局
一个标准的 ARM64 页表项(以 4KB 页面粒度为例)的 64 位被划分为多个字段。下表列出了从高位到低位的核心位域及其功能:
| 位域范围 | 名称 | 功能描述 |
|---|---|---|
| [63:52] | PBHA / 软件保留 | 页属性提示扩展(Page Based Hardware Attributes)或由软件自定义使用。 |
| [51:48] | Output Address[47:44] | 物理地址高位扩展(48位物理地址空间的高4位)。 |
| [47:16] | Output Address[47:16] | 物理页帧号(PFN)的高32位,与低位组合形成完整的物理地址。 |
| [15:12] | Output Address[15:12] | 物理页帧号(PFN)的低4位,与高位组合形成完整的物理地址。 |
| [11] | nG | 非全局位(Not Global)。若置1,该页表项仅在当前ASID的TLB中有效。 |
| [10] | AF | 访问标志位(Access Flag)。硬件在首次访问时置1,若为0则触发访问标志故障。 |
| [9] | SH[1] | 可共享性高位(Shareability)。与低位组合表示不可共享、内部共享或外部共享。 |
| [8] | SH[0] | 可共享性低位。 |
| [7:6] | AP[2:1] | 访问权限位(Access Permission)。控制读/写权限以及EL0/EL1的访问。 |
| [5] | NS | 非安全位(Non-Secure)。用于安全世界与非安全世界的内存隔离。 |
| [4] | AttrIndx[0] | 属性索引低位(Attribute Index)。指向MAIR寄存器中定义的属性。 |
| [3] | AttrIndx[1] | 属性索引高位。 |
| [2] | UXN | 用户不可执行(Unprivileged eXecute Never)。EL0无法执行该页。 |
| [1] | PXN | 特权不可执行(Privileged eXecute Never)。EL1无法执行该页。 |
| [0] | Valid | 有效位。若为0,表示该页表项无效,硬件遍历时会触发缺页异常。 |
4.2 核心属性位详解
4.2.1 访问权限位(AP[2:1])
AP 位决定了页面的读写权限以及异常等级(EL0/EL1)的访问策略。这是内核实现用户态/内核态隔离的基础。
| AP[2:1] | EL1(内核)权限 | EL0(用户)权限 | 典型用途 |
|---|---|---|---|
| 00 | 读/写 | 无访问 | 内核私有数据段 |
| 01 | 读/写 | 读/写 | 用户进程数据段 |
| 10 | 只读 | 无访问 | 内核只读数据(如常量区) |
| 11 | 只读 | 只读 | 用户进程只读数据(如代码段) |
4.2.2 脏位与访问位(Dirty / Access Flag)
ARM64 硬件并不直接提供传统的“脏位”(Dirty Bit),而是通过 访问标志位(AF) 和 脏状态管理机制(DBM, Dirty Bit Management) 来模拟。当硬件使能 DBM 时,硬件在写入操作时会自动将 PTE 标记为脏。
- AF(位[10]): 硬件在首次访问(读或写)时自动置1。若 AF 为0,硬件会触发 访问标志故障,内核在缺页处理中置位 AF 后重试指令。
- 脏状态(软件模拟): 在 Linux 内核中,当 DBM 未使能时,脏位通过 PTE 的软件位(如
PTE_DIRTY)来记录。硬件写入时,内核通过写保护(将 PTE 设为只读)捕获写操作,然后在缺页处理中置脏位并赋予写权限。
4.2.3 执行权限位(UXN / PXN)
这两个位提供了比传统 NX(No-Execute)更细粒度的控制:
- UXN(位[2]): 用户不可执行。若置1,EL0(用户态)无法从该页执行指令。
- PXN(位[1]): 特权不可执行。若置1,EL1(内核态)无法从该页执行指令。
内核通常将用户栈和数据段标记为 UXN=1,防止用户态代码执行数据段;将内核某些敏感区域(如 vmalloc 区域)标记为 PXN=1,防止内核漏洞利用。
4.3 内存属性与缓存策略
PTE 中的 AttrIndx[1:0](位[4:3])是一个 2 位索引,指向系统寄存器 MAIR_EL1(Memory Attribute Indirection Register)中定义的 8 个属性槽之一。每个槽定义了该页面的内存类型(Normal/Device)和缓存策略(Write-Back/Write-Through/Non-Cacheable)。
| AttrIndx 值 | MAIR 属性 | 典型用途 |
|---|---|---|
| 0 | Normal, Write-Back, Read-Allocate, Write-Allocate | 普通内存(默认) |
| 1 | Device-nGnRnE | 外设 MMIO 区域 |
| 2 | Normal, Write-Through, No-Allocate | 帧缓冲或特殊共享内存 |
| 3 | Normal, Non-Cacheable | DMA 一致性内存 |
4.4 软件自定义位(Linux 内核扩展)
ARM64 硬件规范保留了部分位供软件使用。Linux 内核利用这些位实现了额外的状态跟踪,这些位在硬件遍历时被忽略,但在内核缺页处理和页表操作中至关重要。
- PTE_VALID(位[0]): 硬件有效位,内核直接使用。
- PTE_DIRTY(软件位): 通常使用位[55]或[54](取决于配置),表示页面已被写入。
- PTE_YOUNG(软件位): 通常使用位[56]或[53],表示页面最近被访问过(用于页面回收的 LRU 算法)。
- PTE_SPECIAL(软件位): 用于标记零页、KSM 页面等特殊映射。
- PTE_PROT_NONE(软件位): 用于 NUMA 迁移或用户fault页面,表示页面存在但无访问权限。
CONFIG_ARM64_PA_BITS 影响高位可用空间)。在阅读内核源码时,应参考 arch/arm64/include/asm/pgtable.h 中的宏定义。
4.5 ARM64 PTE 位布局结构图
下图展示了 ARM64 4KB 页面 PTE 的完整 64 位布局,包括硬件定义字段和 Linux 内核常用的软件位区域:
4.6 硬件遍历与 PTE 解析流程
当 MMU 进行页表遍历时,它按照以下步骤解析 PTE:
- 检查 Valid 位(位[0]): 若为0,立即触发 Translation Fault(缺页异常),停止遍历。
- 检查 AF 位(位[10]): 若为0,触发 Access Flag Fault,内核在异常处理中置位 AF 并返回。
- 解析 AttrIndx(位[4:3]): 读取 MAIR_EL1 寄存器,获取内存类型和缓存策略。
- 检查 AP 位(位[7:6]): 根据当前异常等级(EL0/EL1)和访问类型(读/写),判断是否触发 Permission Fault。
- 检查 UXN/PXN(位[2:1]): 若尝试执行被禁止的页面,触发 Instruction Abort。
- 提取物理地址: 将 PTE 中的 PFN 字段与虚拟地址的页内偏移拼接,形成最终的物理地址。
do_page_fault() 中,内核通过读取 ESR_EL1(Exception Syndrome Register)获取故障类型(翻译故障、权限故障、访问标志故障等),然后根据 PTE 的软件位(如 PTE_DIRTY、PTE_YOUNG)决定是分配新页、置脏位还是进行写时复制(COW)。
4.7 典型配置示例
以下是一个用户进程匿名页的 PTE 典型值(假设硬件 DBM 使能):
// 用户态可读可写匿名页,已被访问和写入
// 物理地址 0x0000_0040_0000_0000,页内偏移忽略
// 属性:Normal Write-Back(AttrIndx=0),内部共享(SH=0b10)
// 权限:EL0 读/写(AP=0b01),UXN=1(用户不可执行),PXN=0
// AF=1,nG=1(非全局),Valid=1
PTE 二进制(高位到低位):
[63:52] 软件位: 0x000 (包含 PTE_DIRTY=1, PTE_YOUNG=1)
[51:48] PFN高: 0x0
[47:16] PFN主体: 0x0000_0040_0000
[15:12] PFN低: 0x0
[11] nG: 1
[10] AF: 1
[9:8] SH: 0b10
[7:6] AP: 0b01
[5] NS: 0
[4:3] Attr: 0b00
[2] UXN: 1
[1] PXN: 0
[0] Valid: 1
// 十六进制表示(简化):
// 0x_0004_0000_0000_0C05
// 其中位[55](PTE_DIRTY)=1,位[56](PTE_YOUNG)=1
ptdump 或 /sys/kernel/debug/page_tables 导出当前进程的页表信息,直接查看 PTE 的原始十六进制值。结合上述位布局,可以快速诊断内存映射问题。