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
验证方法:
- 将
kernel.img写入SD卡,树莓派4B上电启动 - 在
mmu_test()中通过UART输出PC值(地址0x8000附近) - 若串口输出地址与链接地址一致(0x8000区域),说明MMU映射正确
- 通过条件:MMU启用后,代码能继续执行且访问地址正确,无异常触发
预期输出:串口打印PC值约为0x0000000000008000(恒等映射下虚拟地址等于物理地址)。
关键说明
- 本实现仅映射1MB内存,覆盖内核启动所需区域;实际系统需按需扩展页表
- 页表必须按4KB对齐,否则MMU配置无效
- 启用MMU前需确保页表所在内存已被映射或位于当前可访问区域
- 若需映射超过1MB,需在Level 1/2表中增加相应表项