3. 页表初始化流程:分配页表内存、清零、填充映射关系、设置属性

页表初始化是 MMU 使能前最关键的软件准备阶段。该阶段的核心目标是在物理内存中构建一套完整的、符合当前体系结构规范的页表结构,确保 MMU 开启后 CPU 能够按照预期进行地址翻译。整个流程可归纳为四个连续步骤:分配页表内存 → 清零 → 填充映射关系 → 设置属性。

性能要点:页表初始化阶段的效率直接影响系统启动时间。在内存受限的嵌入式系统中,页表内存的分配策略和清零方式往往是性能瓶颈所在。

3.1 分配页表内存

页表内存的分配需要满足两个核心约束:物理连续性和对齐要求。不同体系结构对页表基地址的对齐粒度有严格规定,例如 ARMv8 要求转换表基地址按 4KB 对齐,x86 的页目录基地址必须按 4KB 对齐。

体系结构 页表层级 对齐要求 典型内存占用(4GB 地址空间)
ARMv7-A(短描述符) 2 级 16KB 对齐(一级页表) 16KB + 二级页表按需分配
ARMv8-A(4KB 粒度) 3 级 4KB 对齐 4KB × 3 级 = 12KB(仅页表本身)
x86(32 位 PAE) 3 级 32 字节对齐(PDPT) 4KB(页目录指针表)+ 4KB(页目录)+ 4KB(页表)
RISC-V(Sv39) 3 级 4KB 对齐 4KB × 3 级 = 12KB

分配策略通常有两种:

  • 静态分配:在链接脚本中预留固定大小的页表区域,适用于内存布局固定的裸机系统。优点是零运行时开销,缺点是灵活性差。
  • 动态分配:在启动早期通过简单的物理内存分配器(如 bitmap 分配器)从预留区域中分配。适用于需要支持不同内存配置的通用 bootloader。
页表内存分配策略对比 静态分配 链接脚本 .bss 段预留 地址固定,零分配延迟 适合裸机 / RTOS 内存浪费(按最大需求预留) 典型:4KB × 3 级 = 12KB 动态分配 启动早期 bitmap 分配器 按需分配,内存利用率高 适合 bootloader / Linux 需管理分配状态 典型:4KB × 实际映射区域

3.2 清零页表内存

清零操作确保页表中所有无效条目处于已知的安全状态。未清零的页表条目可能包含随机数据,MMU 使能后会导致不可预测的翻译错误或权限异常。

安全风险:如果页表内存未清零,残留的物理地址可能指向敏感数据区域(如安全密钥存储区),攻击者可能通过构造特定的虚拟地址访问这些区域。

清零策略对比:

方法 实现方式 性能(4KB 页表) 适用场景
逐字节 memset memset(page_table, 0, size) ~2 μs(Cortex-A72 @1.5GHz) 通用场景,代码可移植
DC ZVA(Data Cache Zero by VA) ARMv8 专用指令,按 cacheline 清零 ~0.5 μs(相同条件) ARMv8 优化场景
DMA 批量清零 DMA 引擎并行执行 ~0.2 μs(不占用 CPU) 多核 / 大型页表(>64KB)
清零前 vs 清零后页表条目状态 清零前(危险状态) 条目 0: 0xDEADBEEF(随机数据) 条目 1: 0xCAFEBABE(残留地址) 条目 2: 0x00000000(偶然有效) 清零后(安全状态) 条目 0: 0x00000000(无效) 条目 1: 0x00000000(无效) 条目 2: 0x00000000(无效)

3.3 填充映射关系

填充映射关系是页表初始化的核心步骤,需要将虚拟地址空间中的各个区域映射到对应的物理地址。典型的映射需求包括:

  • 恒等映射(Identity Mapping):将虚拟地址等于物理地址的区域映射,通常用于 MMU 使能瞬间的过渡期。
  • 内核映射:将内核代码段、数据段映射到高地址虚拟空间(如 0xC0000000 以上)。
  • 外设映射:将 MMIO 地址空间映射为设备内存类型(Device-nGnRE)。
  • 页表自映射:将页表本身映射到虚拟地址空间,便于运行时修改。
典型嵌入式系统页表映射布局 虚拟地址空间 0xFFFFFFFF - 0xC0000000 内核空间(1GB) 0xBFFFFFFF - 0x80000000 页表自映射区域 0x7FFFFFFF - 0x40000000 外设 MMIO 映射 0x3FFFFFFF - 0x00100000 用户空间 / 应用程序 0x000FFFFF - 0x00000000 异常向量表 / 启动代码 物理地址空间 DDR 高地址(内核镜像) 页表物理内存 UART / GPIO / Timer DDR 低地址(应用程序) Boot ROM / SRAM

3.4 设置属性

每个页表条目除了包含物理地址外,还需要设置访问权限和内存类型属性。这些属性决定了 CPU 对该内存区域的访问行为。

关键属性字段(以 ARMv8 为例):

属性 位域 可选值 说明
访问权限(AP) [7:6] 00 = EL0/EL1 可读写
01 = EL1 只读
10 = EL0 不可访问
控制异常级别访问权限
内存类型(AttrIndx) [4:2] 000 = Normal
001 = Device-nGnRE
010 = Strongly-Ordered
决定缓存策略和访问顺序
可执行(XN) [54] 0 = 可执行
1 = 不可执行
防止代码注入攻击
脏位(AF) [10] 0 = 未访问
1 = 已访问
初始化为 1 避免缺页异常
性能优化技巧:对于大块连续内存区域(如 DDR),使用块映射(Block Mapping)代替页表映射。ARMv8 支持 2MB 和 1GB 的块映射,可显著减少页表层级遍历次数和 TLB 压力。

3.5 完整初始化流程代码示例

以下代码展示了 ARMv8-A 架构下页表初始化的核心流程,使用 4KB 粒度三级页表:

// 页表初始化流程 - ARMv8-A 示例
#define PAGE_TABLE_SIZE  0x3000    // 三级页表:L1 + L2 + L3 = 12KB
#define PAGE_TABLE_ALIGN 0x1000    // 4KB 对齐

// 步骤1:分配页表内存(静态分配)
__attribute__((aligned(PAGE_TABLE_ALIGN)))
uint64_t page_table_l1[512];   // L1 页表(1个表项覆盖 1GB)
uint64_t page_table_l2[512];   // L2 页表(1个表项覆盖 2MB)
uint64_t page_table_l3[512];   // L3 页表(1个表项覆盖 4KB)

void init_page_tables(void) {
    // 步骤2:清零所有页表
    memset(page_table_l1, 0, sizeof(page_table_l1));
    memset(page_table_l2, 0, sizeof(page_table_l2));
    memset(page_table_l3, 0, sizeof(page_table_l3));
    
    // 步骤3:填充映射关系 - 恒等映射 0x00000000 - 0x3FFFFFFF
    // L1 表项:0x00000000 - 0x3FFFFFFF(1GB 块映射)
    page_table_l1[0] = (uint64_t)&page_table_l2;
    page_table_l1[0] |= 0x3;  // 有效 + 可访问
    
    // L2 表项:0x00000000 - 0x001FFFFF(2MB 块映射)
    for (int i = 0; i < 512; i++) {
        page_table_l2[i] = (i * 0x200000);  // 物理基地址
        page_table_l2[i] |= 0x3;            // 有效 + 可访问
    }
    
    // 步骤4:设置属性
    // 设置 Normal 内存类型(MAIR 索引 0)
    // 设置可读写、可执行
    for (int i = 0; i < 512; i++) {
        page_table_l2[i] &= ~(0x3ULL << 2);  // 清除 AttrIndx
        page_table_l2[i] |= (0x0ULL << 2);  // AttrIndx = 0 (Normal)
        page_table_l2[i] |= (0x1ULL << 6);  // AP = 01 (EL1 可读写)
        page_table_l2[i] |= (0x1ULL << 10); // AF = 1 (已访问)
    }
    
    // 设置 TTBR0_EL1 指向 L1 页表基地址
    __asm__ volatile("msr ttbr0_el1, %0" : : "r" (page_table_l1));
    __asm__ volatile("isb");  // 指令同步屏障
}
关键检查点:在 MMU 使能前,务必验证以下三点:
1. 页表基地址寄存器(TTBR0/TTBR1)已正确设置。
2. MAIR(Memory Attribute Indirection Register)已配置,确保 AttrIndx 索引有效。
3. TCR(Translation Control Register)已设置,包括页面粒度、地址空间大小和缓存策略。

3.6 常见错误与调试方法

错误现象 可能原因 调试方法
MMU 使能后立即触发异常 页表内存未清零,无效条目导致翻译错误 检查页表内存是否在使能前完成 memset
访问外设时挂死 外设区域未设置为 Device 内存类型 检查 MAIR 配置和页表条目 AttrIndx 字段
性能异常低 Normal 内存区域被错误设置为 Strongly-Ordered 使用性能计数器检查缓存命中率
页表修改不生效 TLB 未失效,缓存了旧页表条目 执行 TLBI(TLB Invalidate)操作
页表初始化调试流程 MMU 使能后异常 检查异常类型 读取 ESR_EL1 定位页表 检查页表条目内容 验证物理地址正确性 检查属性字段

页表初始化是 MMU 使能前最关键的软件准备步骤。分配内存时需保证物理连续性和对齐要求;清零操作确保所有无效条目处于安全状态;填充映射关系需要覆盖所有必要的地址区域;设置属性则决定了内存的访问权限和缓存行为。这四个步骤的顺序和正确性直接影响系统的稳定性和性能。