4. MMU页表配置实战

任务说明

在ARMv8 AArch64架构下,为系统配置MMU(Memory Management Unit),建立4KB粒度的页表映射,实现虚拟地址到物理地址的转换。本案例以树莓派4B(BCM2711,Cortex-A72)为硬件参考,实现一个最小可运行的页表配置:将内核镜像所在的虚拟地址区间映射到对应的物理地址区间,并启用MMU完成地址转换。

核心原理

ARMv8的MMU依赖翻译表(Translation Table)完成地址转换。4KB粒度对应三级页表结构:

  • TTBR0_EL1:指向一级页表基地址(Level 0)
  • Level 0:覆盖整个48位地址空间,每个表项管理1GB区域
  • Level 1:每个表项管理2MB区域
  • Level 2:每个表项管理4KB页面(最终映射粒度)

本案例采用恒等映射(虚拟地址=物理地址)方式,便于在启用MMU前后保持PC连续性,避免跳转问题。

完整代码实现

// mmu_setup.c - ARMv8 4KB页表配置最小实现
// 编译: aarch64-linux-gnu-gcc -c mmu_setup.c -o mmu_setup.o

#include <stdint.h>

// 页表描述符类型定义
#define PD_TABLE      0x3ULL          // 指向下一级表的描述符
#define PD_PAGE       0x3ULL          // 4KB页描述符
#define PD_AF         0x1ULL << 10    // Access Flag
#define PD_SH_INNER   0x3ULL << 8     // Inner Shareable
#define PD_AP_RW      0x0ULL << 6     // EL1读写
#define PD_MEM_NORMAL 0x4ULL << 2     // Normal内存属性索引(MAIR[1])

// 内存属性寄存器(MAIR)配置
#define MAIR_ATTR0    0x00ULL         // Device-nGnRnE
#define MAIR_ATTR1    0xFFULL         // Normal Memory, Inner/Outer Write-Back

// 页表存储(需按4096字节对齐)
__attribute__((aligned(4096)))
uint64_t tt_l0[512];    // Level 0 表
__attribute__((aligned(4096)))
uint64_t tt_l1[512];    // Level 1 表
__attribute__((aligned(4096)))
uint64_t tt_l2[512];    // Level 2 表

// TCR_EL1 配置值
#define TCR_T0SZ     16          // 2^(64-16) = 2^48 地址空间
#define TCR_IRGN0_WB  (0x1ULL << 8)   // Inner Write-Back
#define TCR_ORGN0_WB  (0x1ULL << 10)  // Outer Write-Back
#define TCR_SH0_INNER (0x3ULL << 12)  // Inner Shareable
#define TCR_TG0_4KB   (0x0ULL << 14)  // 4KB粒度

// 初始化页表
void init_page_tables(void) {
    // 清零所有页表
    for (int i = 0; i < 512; i++) {
        tt_l0[i] = 0;
        tt_l1[i] = 0;
        tt_l2[i] = 0;
    }

    // 建立恒等映射:虚拟地址0x00000000_00000000 ~ 0x00000000_00200000 (2MB)
    // 覆盖范围:0x00000000_00000000 - 0x00000000_00100000 (1MB)
    // 映射前1MB物理内存,包含内核启动代码

    // Level 0: 虚拟地址[47:39] = 0,指向Level 1表
    tt_l0[0] = ((uint64_t)tt_l1 >> 12) << 12 | PD_TABLE;

    // Level 1: 虚拟地址[38:30] = 0,指向Level 2表
    tt_l1[0] = ((uint64_t)tt_l2 >> 12) << 12 | PD_TABLE;

    // Level 2: 映射前256个4KB页(共1MB)
    for (int i = 0; i < 256; i++) {
        uint64_t phys = (uint64_t)i << 12;  // 物理地址 = i * 4KB
        tt_l2[i] = phys | PD_PAGE | PD_AF | PD_SH_INNER | PD_AP_RW | PD_MEM_NORMAL;
    }
}

// 启用MMU
void enable_mmu(void) {
    uint64_t tcr, mair, ttbr0, sctlr;

    // 1. 设置MAIR_EL1
    mair = (MAIR_ATTR0 << 0) | (MAIR_ATTR1 << 8);
    __asm__ volatile("msr mair_el1, %0" : : "r" (mair));

    // 2. 设置TCR_EL1
    tcr = TCR_T0SZ | TCR_IRGN0_WB | TCR_ORGN0_WB | TCR_SH0_INNER | TCR_TG0_4KB;
    __asm__ volatile("msr tcr_el1, %0" : : "r" (tcr));

    // 3. 设置TTBR0_EL1
    ttbr0 = (uint64_t)tt_l0;
    __asm__ volatile("msr ttbr0_el1, %0" : : "r" (ttbr0));

    // 4. 设置SCTLR_EL1,启用MMU
    __asm__ volatile("mrs %0, sctlr_el1" : "=r" (sctlr));
    sctlr |= 0x1;  // M位 = 1,启用MMU
    __asm__ volatile("msr sctlr_el1, %0" : : "r" (sctlr));

    // 5. 指令屏障,确保配置生效
    __asm__ volatile("isb");
}

// 测试函数:读取当前PC并验证映射
void mmu_test(void) {
    uint64_t pc;
    __asm__ volatile("adr %0, ." : "=r" (pc));
    // 若MMU工作正常,PC值应等于当前物理地址(恒等映射)
    // 可通过串口打印pc值验证
}

启动与链接配置

// link.ld - 链接脚本(关键片段)
SECTIONS
{
    . = 0x0000000000008000;  /* 内核加载地址 */
    .text : { *(.text*) }
    .rodata : { *(.rodata*) }
    .data : { *(.data*) }
    .bss : { *(.bss*) }
}

运行与验证

编译步骤:

aarch64-linux-gnu-gcc -c mmu_setup.c -o mmu_setup.o
aarch64-linux-gnu-ld -T link.ld mmu_setup.o -o kernel.elf
aarch64-linux-gnu-objcopy -O binary kernel.elf kernel.img

验证方法:

  1. 将kernel.img写入SD卡,树莓派4B上电启动
  2. 在mmu_test()中通过UART输出PC值(地址0x8000附近)
  3. 若串口输出地址与链接地址一致(0x8000区域),说明MMU映射正确
  4. 通过条件:MMU启用后,代码能继续执行且访问地址正确,无异常触发

预期输出:串口打印PC值约为0x0000000000008000(恒等映射下虚拟地址等于物理地址)。

关键说明

  • 本实现仅映射1MB内存,覆盖内核启动所需区域;实际系统需按需扩展页表
  • 页表必须按4KB对齐,否则MMU配置无效
  • 启用MMU前需确保页表所在内存已被映射或位于当前可访问区域
  • 若需映射超过1MB,需在Level 1/2表中增加相应表项