4. 页表创建与填充:页表描述符格式、块/页描述符、属性域(AP/XN/PXN)

页表是 MMU 实现虚拟地址到物理地址转换的核心数据结构。ARMv8-A 架构采用多级页表(通常为 3 级或 4 级),每一级页表中的条目称为页表描述符。描述符不仅包含物理地址映射信息,还携带了内存属性、访问权限和执行控制等关键字段。本章将深入解析描述符格式、块/页描述符的区别以及 AP/XN/PXN 属性域的配置方法。

核心概念: 页表描述符是 MMU 的“指令集”。驱动程序开发者必须理解描述符中每一位的含义,才能正确配置内存映射,避免缓存一致性、权限越界或安全漏洞问题。

4.1 页表描述符通用格式

ARMv8-A 的页表描述符为 64 位宽。根据描述符类型,其格式略有不同,但高位通常用于存储物理地址(或下一级页表的基地址),低位用于定义属性。下表列出了 Level 0 到 Level 3 描述符的通用字段布局。

位域 宽度 (bits) 字段名称 描述
[63:48] 16 Upper Attributes 包含 XN、PXN、Contiguous、DBM 等高级属性
[47:12] 36 Output Address [47:12] 物理地址高位(4KB 粒度时,低 12 位隐含为 0)
[11:2] 10 Lower Attributes 包含 AttrIndx、NS、AP、SH、AF、nG 等属性
[1:0] 2 Descriptor Type 00: 无效 (Fault);01: 表描述符 (Table);10: 块描述符 (Block);11: 页描述符 (Page)
关键点: 描述符类型位 [1:0] 决定了该条目是表描述符(指向下一级页表)还是块/页描述符(指向最终物理内存)。Level 0 和 Level 1 只能包含表描述符或块描述符;Level 2 可以包含表描述符或块描述符;Level 3 只能包含页描述符。

4.2 块描述符与页描述符

块描述符和页描述符是 MMU 映射的两种粒度。块描述符用于大块连续内存映射(如 2MB 或 1GB),页描述符用于标准 4KB 页面映射。选择哪种描述符直接影响 TLB 利用率和映射灵活性。

块描述符 vs 页描述符 结构对比 块描述符 (Block Descriptor) Level 1 / Level 2 使用 映射粒度: 2MB (L2) / 1GB (L1) TLB 覆盖范围: 大,条目少 适用场景: 内核线性映射、大块DMA缓冲区 优点: 页表占用小,TLB命中率高 页描述符 (Page Descriptor) Level 3 使用 映射粒度: 4KB TLB 覆盖范围: 小,条目多 适用场景: 用户空间进程页、设备寄存器页 优点: 粒度细,内存利用率高

4.3 属性域详解:AP、XN、PXN

属性域是描述符中控制访问权限和执行权限的关键字段。错误配置这些属性是导致内核崩溃(如 Unable to handle kernel paging request)或安全漏洞的常见原因。

4.3.1 AP (Access Permission) 访问权限位

AP 字段位于描述符的 [7:6] 位,控制 EL1(内核)和 EL0(用户)的读写权限。ARMv8-A 定义了四种 AP 组合:

AP[2:1] EL1 (内核) 权限 EL0 (用户) 权限 典型用途
00 读写 无访问 内核私有数据、页表自身
01 读写 读写 用户空间共享内存
10 只读 无访问 内核只读数据段 (.rodata)
11 只读 只读 用户空间只读映射 (如代码段)
常见错误: 驱动程序在映射 DMA 缓冲区时,如果错误地将 AP 设置为 01(用户可读写),会导致用户空间进程可以随意篡改内核 DMA 缓冲区,引发数据损坏或权限提升攻击。正确的做法是:对于内核专用的 DMA 缓冲区,AP 应设置为 00。

4.3.2 XN (Execute Never) 不可执行位

XN 位位于描述符的第 54 位。当 XN = 1 时,该内存区域禁止执行任何指令。这是实现 NX (No-Execute) 保护的核心机制。

  • XN = 0: 允许从该内存区域取指执行。
  • XN = 1: 禁止从该内存区域取指执行。任何尝试跳转到该区域执行指令的行为都会触发 Instruction Abort 异常。

在驱动开发中,以下场景必须设置 XN = 1:

  • 数据缓冲区(如 DMA 描述符环、数据 payload 区域)
  • 设备 MMIO 寄存器区域
  • 栈和堆区域(用户空间已默认设置)

4.3.3 PXN (Privileged Execute Never) 特权不可执行位

PXN 位位于描述符的第 53 位。PXN 是 XN 的增强版本,专门限制特权级(EL1)的执行权限。

  • PXN = 0: 允许内核(EL1)从该区域执行代码。
  • PXN = 1: 禁止内核(EL1)从该区域执行代码。用户空间(EL0)的执行权限由 XN 位控制。
安全价值: PXN 用于防止“内核态代码重用攻击”(如 ROP/JOP)。即使攻击者控制了内核的程序计数器,也无法跳转到用户空间布局的 gadget 代码执行,因为用户空间页面的 PXN 位通常被置 1。在配置内核页表时,务必为所有用户空间映射设置 PXN = 1。

4.4 页表填充流程与代码示例

以下代码展示了在 Linux 内核中,如何手动创建和填充一个 Level 2 块描述符,用于映射一个 2MB 的 DMA 缓冲区。该示例使用 pgprot_t 和 set_pte_at 等内核 API,但为了展示底层描述符操作,我们直接操作描述符值。

/*
 * 示例:手动构建一个 Level 2 块描述符
 * 映射物理地址 0xFC000000 → 虚拟地址 0xFFFF8000FC000000
 * 属性:内核读写,不可执行,非缓存(设备内存)
 */
#include <linux/mm.h>
#include <asm/pgtable.h>

#define BLOCK_DESC_TYPE      (0x1UL << 0)  /* Block descriptor type = 10b */
#define BLOCK_DESC_AF        (0x1UL << 10) /* Access Flag */
#define BLOCK_DESC_AP_RW_EL1 (0x0UL << 6)  /* AP=00: EL1 R/W, EL0 no access */
#define BLOCK_DESC_XN        (0x1UL << 54) /* Execute Never */
#define BLOCK_DESC_NG        (0x1UL << 11) /* Not Global */

static void create_block_descriptor(void)
{
    phys_addr_t phys = 0xFC000000;
    unsigned long virt = 0xFFFF8000FC000000;
    unsigned long desc;

    /* 构建描述符值 */
    desc = phys;                              /* 物理地址高位 */
    desc |= BLOCK_DESC_TYPE;                  /* 标记为块描述符 */
    desc |= BLOCK_DESC_AF;                    /* 设置 Access Flag */
    desc |= BLOCK_DESC_AP_RW_EL1;             /* 内核读写 */
    desc |= BLOCK_DESC_XN;                    /* 不可执行 */
    desc |= BLOCK_DESC_NG;                    /* 非全局(仅当前进程) */

    /* 获取页表项指针并写入 */
    pgd_t *pgd = pgd_offset(current->mm, virt);
    p4d_t *p4d = p4d_offset(pgd, virt);
    pud_t *pud = pud_offset(p4d, virt);
    pmd_t *pmd = pmd_offset(pud, virt);

    /* 写入块描述符到 PMD 条目 */
    set_pmd(pmd, __pmd(desc));

    /* 刷新 TLB */
    flush_tlb_kernel_range(virt, virt + SZ_2M);

    pr_info("Block descriptor created: PA=0x%llx VA=0x%lx desc=0x%lx\n",
            phys, virt, desc);
}

4.5 属性域配置检查清单

在配置页表描述符时,建议按照以下清单逐项检查属性域,避免遗漏:

  1. AP 权限: 确认该内存区域是否允许用户空间访问。DMA 缓冲区通常设置为 AP=00(仅内核)。
  2. XN 位: 数据区域必须设置 XN=1。代码区域根据需求设置。
  3. PXN 位: 用户空间映射必须设置 PXN=1,防止内核执行用户代码。
  4. AF (Access Flag): 必须设置为 1,否则首次访问会触发 Access Flag Fault。
  5. nG (Not Global): 进程私有映射设置 nG=1;内核全局映射(如 vmalloc)设置 nG=0。
  6. AttrIndx: 指向 MAIR 寄存器中定义的缓存策略索引(如 Normal、Device-nGnRE)。
页表描述符属性域配置决策流程 开始配置描述符 是否映射到用户空间 (EL0)? AP=01 或 11 PXN=1 AP=00 或 10 PXN=0
调试技巧: 在 Linux 内核中,可以使用 /sys/kernel/debug/page_tables/ 下的调试接口(需启用 CONFIG_PTDUMP)来 dump 当前页表,检查描述符中的 AP、XN、PXN 是否按预期配置。对于 DMA 缓冲区,重点关注 dma_alloc_coherent 返回区域的页表属性。