4. MMU工作原理
MMU 的核心,说白了就是一张“虚拟地址 → 物理地址”的映射表。但光有表不行,你得告诉硬件怎么查这张表、表有多大、每页多大、查表时用几级索引。这些配置,全压在 TCR_ELx(Translation Control Register)和 TTBR0/1(Translation Table Base Register)上。
我刚开始接触 ARMv8 的 MMU 时,觉得寄存器名字又长又多,头大。后来调过几次页表报错,才明白这几个寄存器就是 MMU 的“大脑”。配置错了,系统直接挂给你看。
4.1 TCR_ELx:转换控制寄存器
TCR_ELx 控制着整个地址翻译的行为。不同异常级有各自的 TCR:
- TCR_EL1:给 OS 内核和用户空间用(EL0/EL1)
- TCR_EL2:给虚拟化监控器用(EL2)
- TCR_EL3:给安全监控器用(EL3)
每个 TCR 的位域结构基本一致,但 EL1 的 TCR 最复杂,因为它要同时管理内核空间和用户空间的翻译。咱们重点讲 TCR_EL1。
4.1.1 IPS:物理地址空间大小
IPS(Intermediate Physical Address Size)告诉 MMU:你的物理地址总线有多宽。ARMv8 支持多种物理地址宽度:
| IPS 编码 | 物理地址宽度 | 最大物理内存 |
|---|---|---|
| 000 | 32 位 | 4 GB |
| 001 | 36 位 | 64 GB |
| 010 | 40 位 | 1 TB |
| 011 | 42 位 | 4 TB |
| 100 | 44 位 | 16 TB |
| 101 | 48 位 | 256 TB |
| 110 | 52 位 | 4 PB |
4.1.2 TG:翻译粒度
TG(Translation Granule)决定页表的最小页面大小。ARMv8 支持三种粒度:
- 4 KB:最常用,兼容性好,页表层级多(4 级)
- 16 KB:中间选项,页表层级少一级
- 64 KB:大页,页表层级最少(2 级),适合大内存系统
我个人习惯在嵌入式场景用 4 KB 粒度。为什么?因为灵活。你想想看,一个系统里既有代码段、数据段,又有外设映射,4 KB 粒度能精细控制权限。64 KB 粒度虽然页表小,但映射外设时容易浪费空间。
4.1.3 T0SZ / T1SZ:地址空间大小
T0SZ 和 T1SZ 控制虚拟地址空间的大小。T0SZ 对应 TTBR0(用户空间),T1SZ 对应 TTBR1(内核空间)。
计算公式很简单:
虚拟地址位数 = 64 - T0SZ
虚拟地址空间大小 = 2^(64 - T0SZ)
举个例子:
- T0SZ = 25 → 虚拟地址 39 位 → 512 GB 空间
- T0SZ = 16 → 虚拟地址 48 位 → 256 TB 空间
4.1.4 其他关键字段
| 字段 | 作用 | 我的建议 |
|---|---|---|
| SH | 可共享性(Inner/Outer/Non-shareable) | 多核系统用 Inner Shareable |
| IRGN/ORGN | Cache 策略(Write-Back/Write-Through) | 内存用 Write-Back,外设用 Non-cacheable |
| A1 | ASID 选择(TTBR0 还是 TTBR1) | 一般用 TTBR0 的 ASID |
| EPD0/EPD1 | 禁用 TTBR0/TTBR1 翻译 | 裸机程序可以关掉一个 |
4.2 TTBR0 / TTBR1:页表基址寄存器
TTBR0 和 TTBR1 存放页表的物理基地址。ARMv8 把虚拟地址空间分成两半:
- 低地址空间(TTBR0):用户空间,地址范围由 T0SZ 决定
- 高地址空间(TTBR1):内核空间,地址范围由 T1SZ 决定
为什么这么设计?我理解是为了隔离。用户程序只能访问低地址,内核访问高地址。切换进程时,只需要换 TTBR0,TTBR1 保持不变。这样内核页表不用每个进程都重建。
4.2.1 TTBR0 的配置
TTBR0 的格式:
位 [63:48]:ASID(地址空间标识符)
位 [47:x]:页表基地址(物理地址,按粒度对齐)
位 [x-1:0]:保留(必须为 0)
注意:基地址必须按页表粒度对齐。4 KB 粒度时,基地址低 12 位必须为 0。64 KB 粒度时,低 16 位为 0。
4.2.2 ASID:地址空间标识符
ASID 是 TTBR0 高 16 位的一个字段。它的作用是:
- 区分不同进程的 TLB 条目
- 进程切换时,TLB 不需要全部刷掉
- 只要 ASID 不同,TLB 可以同时缓存多个进程的映射
我调试过一个性能问题:进程切换频繁,TLB 每次都全刷,性能掉得厉害。加上 ASID 后,TLB 命中率从 60% 升到 95%。
4.3 配置示例:裸机 MMU 初始化
下面是一个简化版的 TCR_EL1 和 TTBR0 配置代码。假设我们使用 4 KB 粒度、39 位虚拟地址、40 位物理地址:
// 配置 TCR_EL1
// IPS = 010 (40位物理地址)
// TG0 = 00 (4KB粒度)
// T0SZ = 25 (39位虚拟地址)
// SH0 = 11 (Inner Shareable)
// ORGN0 = 01 (Write-Back, Write-Allocate)
// IRGN0 = 01 (Write-Back, Write-Allocate)
uint64_t tcr = (2 << 32) | // IPS=010
(0 << 14) | // TG0=00
(25 << 0) | // T0SZ=25
(3 << 12) | // SH0=11
(1 << 10) | // ORGN0=01
(1 << 8); // IRGN0=01
__asm__ volatile("msr tcr_el1, %0" : : "r"(tcr));
// 配置 TTBR0
// 页表基地址: 0x80000000 (物理地址)
// ASID: 0
uint64_t ttbr0 = 0x80000000; // 基地址,低12位为0
__asm__ volatile("msr ttbr0_el1, %0" : : "r"(ttbr0));
// 使能MMU
uint64_t sctlr;
__asm__ volatile("mrs %0, sctlr_el1" : "=r"(sctlr));
sctlr |= 1; // M位=1
__asm__ volatile("msr sctlr_el1, %0" : : "r"(sctlr));
4.4 知识结构图
下面这张图展示了 TCR_EL1、TTBR0/1 和页表之间的关系:
4.5 避坑指南
最后,我总结几个实际项目中容易踩的坑:
- IPS 配小了:物理内存访问不到,系统启动一半就挂。检查你的 DDR 地址范围,IPS 必须覆盖所有物理内存。
- T0SZ 和 T1SZ 冲突:两个空间不能重叠。比如 T0SZ=25(低 512 GB),T1SZ=25(高 512 GB),中间留空。如果设成 T0SZ=16(低 256 TB),T1SZ=16(高 256 TB),中间没有空隙,但地址会重叠?不会,ARM 规定 TTBR0 和 TTBR1 空间不重叠,由硬件保证。
- TTBR 基地址未对齐:MMU 直接忽略低几位,导致页表地址错位。我建议在代码里加断言检查对齐。
- 使能 MMU 前页表未初始化:MMU 一开,第一条指令就取指失败。我习惯在使能前先写一个 dummy 页表,映射当前代码段。