4. 页表创建与填充:页表描述符格式、块/页描述符、属性域(AP/XN/PXN)
页表是 MMU 实现虚拟地址到物理地址转换的核心数据结构。ARMv8-A 架构采用多级页表(通常为 3 级或 4 级),每一级页表中的条目称为页表描述符。描述符不仅包含物理地址映射信息,还携带了内存属性、访问权限和执行控制等关键字段。本章将深入解析描述符格式、块/页描述符的区别以及 AP/XN/PXN 属性域的配置方法。
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) |
4.2 块描述符与页描述符
块描述符和页描述符是 MMU 映射的两种粒度。块描述符用于大块连续内存映射(如 2MB 或 1GB),页描述符用于标准 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 | 只读 | 只读 | 用户空间只读映射 (如代码段) |
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 位控制。
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 属性域配置检查清单
在配置页表描述符时,建议按照以下清单逐项检查属性域,避免遗漏:
- AP 权限: 确认该内存区域是否允许用户空间访问。DMA 缓冲区通常设置为 AP=00(仅内核)。
- XN 位: 数据区域必须设置 XN=1。代码区域根据需求设置。
- PXN 位: 用户空间映射必须设置 PXN=1,防止内核执行用户代码。
- AF (Access Flag): 必须设置为 1,否则首次访问会触发 Access Flag Fault。
- nG (Not Global): 进程私有映射设置 nG=1;内核全局映射(如 vmalloc)设置 nG=0。
- AttrIndx: 指向 MAIR 寄存器中定义的缓存策略索引(如 Normal、Device-nGnRE)。
/sys/kernel/debug/page_tables/ 下的调试接口(需启用 CONFIG_PTDUMP)来 dump 当前页表,检查描述符中的 AP、XN、PXN 是否按预期配置。对于 DMA 缓冲区,重点关注 dma_alloc_coherent 返回区域的页表属性。