摘要:上一篇讲完 EL0--EL3 异常模型、VBAR、ESR、GIC 和 ERET,这一篇进入让"虚拟地址"真正落地的硬件------AArch64 MMU 。核心就四件事:
TTBR0/1_EL1选地址空间、TCR_EL1定义切分与粒度、MAIR_EL1定义内存属性、多级页表把 VA 翻译成 PA。再叠加 TLB 维护、Break-Before-Make、Access Flag 惰性提交、KPTI、大页与 Contiguous Hint,就是 Linux/裸机 boot 里最容易踩坑的一整块。
一、TTBR0 / TTBR1:天然的内核/用户地址空间切分
AArch64 用最高位决定走哪张根表:
- VA63:56 == 0x00 → 用 TTBR0_EL1(用户空间,低地址)
- VA63:56 == 0xFF → 用 TTBR1_EL1(内核空间,高地址)
- 两者都不匹配 → Address Size Fault
好处:
- 用户/内核页表物理分离,上下文切换只换
TTBR0_EL1,TTBR1_EL1不动 - AP 位直接表达权限:用户页
AP=01/11(EL1+EL0 可访问),内核页AP=00(仅 EL1) - Spectre/Meltdown 后 Linux 加 KPTI:EL0 时把 TTBR1 换成只映射异常向量的 trampoline 表,进 EL1 再切回全内核表
二、TCR_EL1:切分点与粒度
- T0SZ :TTBR0 空间大小 = 2^(64−T0SZ) 字节。Linux 常用
T0SZ=25→ 512 GB 用户空间(0x0000...0000 ~ 0x0000_007F_FFFF_FFFF) - T1SZ:TTBR1 空间同理,从最高位往下长
- TG0 / TG1 :粒度
00=4KB、01=64KB、10=16KB(4KB 最通用,64KB 降 walk 深度适合 HPC/NUMA) - IPS:物理地址位宽
- SH0/SH1、ORGN0/IRGN0:缓存共享与内外属性默认策略
4KB 粒度 + T0SZ=25 时,L0 只用少数几项(因为高 17 位被 T0SZ 砍掉),L0→L1→L2→L3 四级 walk,每级 512 项 × 8 字节 = 4KB 一页。
三、VA→PA 手算示例(4KB 粒度,T0SZ=25)
翻译 0x0000_0040_1234_5678:
- Top-bit:VA63:56=0x00 → 走 TTBR0
- L0 索引 VA47:39 = 0b000000010 = 2 → 读 L02
- L1 索引 VA38:30 = 0 → 若 L10 是 1GB Block,直接 PA=Block_base|VA29:0;否则 Table→L2
- L2 索引 VA29:21 = 0b010010001 = 145 → 2MB Block 则 PA=Block_base|VA20:0
- L3 索引 VA20:12 = 0b001000101 = 69 → 4KB Page,PA=Page_base|VA11:0(offset 0x678)
最坏 4 次串行内存读 ≈ 40 周期;命中 TLB 后接近单周期。
四、MAIR_EL1:8 个属性槽,描述符用 AttrIndx2:0 选
每个 8 位编码,典型内核配置:
| Index | 编码含义 | 用途 |
|---|---|---|
| 0 | Device-nGnRnE | MMIO 寄存器,严格顺序、不可重排 |
| 1 | Normal, Inner/Outer Write-Back, RA/WA | 普通缓存 RAM |
| 2 | Normal, Non-Cacheable | DMA buffer、一致性旁路 |
| 3~7 | 留作 Device-nGRE / GRE / Normal NC 等变体 |
- Device 内存:访问按程序序,对同一外设不重排;不同外设间允许重排(nGnRnE 最严,GRE 最松)
- Normal 内存 :允许乱序+推测执行,跨核/跨外设必须自己加
DMB/DSB/ISB
页表描述符里 AttrIndx 3 位指向 MAIR 槽位,配合 SH(shareability)和 AP(权限)一起决定最终访问语义。
五、描述符格式(4KB 粒度,8 字节/项)
- bits1:0:
00/10= Invalid11= Table(指向下一级表基址)01在 L3 = Page;在 L1/L2 = Block(1GB / 2MB)
- AP7:6 :
00=EL1 RW/EL0 无、01=EL1+EL0 RW、10=EL1 RO、11=EL1+EL0 RO - UXN54/PXN53:用户/特权不可执行
- AF10:Access Flag,首次访问触发 AF fault,软件置 1(惰性提交,省 TLB 预热)
- SH9:8:0=None, 1=Inner, 2=Outer, 3=Inner Share
- Contiguous52:16 个连续 4KB L3 页可合并成 64KB TLB 项
- nG11:非全局(按 ASID 隔离)
Block 映射能砍掉一级 walk:L1 放 1GB Block、L2 放 2MB Block,Linux 内核镜像和大匿名映射大量用 2MB 大页。
六、启动身份映射(Identity Map)最小骨架
MMU 开启瞬间 PC 还在物理地址跑,所以先建 VA==PA 的恒等映射:
- 一张 L0 表(4KB,512 项)
- L00 → L1 表
- L1 里放 4 个 1GB Block 覆盖 0x0 ~ 0xFFFF_FFFF:
- 0x0000_0000--0x3FFF_FFFF → Device-nGnRnE(AttrIndx=0)
- 0x4000_0000--0x7FFF_FFFF → Normal RW Cache(AttrIndx=1, AP=00)
- 0x8000_0000--0xBFFF_FFFF → Normal RO+PXN(AttrIndx=1, AP=10, PXN=1)
- 0xC000_0000--0xFFFF_FFFF → Normal RW Non-Cacheable(AttrIndx=2)
然后:
MSR MAIR_EL1, X0
MSR TCR_EL1, X1
MSR TTBR0_EL1, X2
DSB ISH
MSR SCTLR_EL1, X3 ; 置 M=1 (bit0), C=1, I=1
ISB
顺序铁律:先刷 TLBI、DSB 同步,再开 M;开 M 后立刻 ISB,否则后面指令还按旧翻译取指。
七、TLB 维护与 Break-Before-Make(BBM)
改活页表项(改 PA、改权限、改属性)必须按序:
- 写 invalid 描述符到该表项
DSB ISHTLBI VAE1IS, Xt(Xt = ASID<<48 | (VA>>12))DSB ISH- 写新 valid 描述符
DSB ISH+ISB
跳过任何一步 → 别的核 TLB 里可能同时命中新旧映射,行为未定义。只读收紧(RW→RO)理论上可放宽,但工程默认全走 BBM 最稳。
常用 TLBI:
TLBI VMALLE1IS:本 ASID 全失效TLBI VAE1IS, Xt:单页失效TLBI ASIDE1IS, Xt:按 ASID 全失效TLBI IPAS2E1IS:Stage-2(Hypervisor 用)
八、Access Flag 惰性提交
描述符 AF=0 时,第一次访问触发 Data Abort,ESR_EL1 EC=0b100100(0x24),DFSC=0b001001。Handler 做:
- 读
FAR_EL1拿坏 VA - 从 TTBR0 走页表找到 L3 项
- 原子
ORR置 bit10(AF=1) TLBI VAE1IS+DSB+ISBERET重执行原指令
线性扫 64KB(16 个 4KB 页)正好 16 次 AF fault------这就是 Linux 懒分配页表的来源:建表时全 AF=0,用到才付 TLB 预热成本。
九、Stage-2 与 Hypervisor 一句话
EL2 开 Stage-2 后,VA→IPA(Stage-1,TTBR_EL1)→PA(Stage-2,VTTBR_EL2)。KVM 里每个 VM 一套 Stage-2,设备直通改 Stage-2 权限就能做内存隔离,不用动 guest 自己的页表。
十、本文核心复盘
- TTBR0(低 VA,用户)/ TTBR1(高 VA,内核)按 VA63:56 自动选,切换只换 TTBR0
- TCR_EL1:T0SZ/T1SZ 定空间大小,TG0/TG1 定 4K/16K/64K 粒度
- MAIR_EL1 八槽属性,描述符 AttrIndx 选 Device / Normal WB / Normal NC
- 4KB 粒度四级 walk,L1/L2 Block 做大页,Contiguous52 合并 16×4KB→64KB TLB 项
- AP/UXN/PXN/AF/SH/nG 组合出权限与缓存语义
- 开 MMU 顺序:建表→MAIR/TCR/TTBR→DSB→SCTLR.M=1→ISB
- 改活映射必须 Break-Before-Make:invalid→DSB→TLBI→DSB→new→DSB+ISB
- AF=0 触发 EC=0x24/DFSC=0x9,handler 置 AF 位再 TLBI 重执行
- KPTI 用 trampoline TTBR1 挡住 EL0 对内核空间的推测读