ARMv8-A 用的是虚拟内存。CPU 里跑的地址是虚拟地址,经过 MMU 翻译成物理地址才能访问内存。这个翻译过程硬件自动完成,MMU 会去内存里读翻译表(Translation Table),这一套动作叫 Table Walk。
MMU 还有一个关键作用:让多个任务同时跑在各自的虚拟地址空间里,彼此不感知对方的物理地址位置。每个程序看到的虚拟地址空间都是连续的,底层物理内存是不是连续它不关心。
翻译表长这样:
MMU 用虚拟地址的高位去索引翻译表的条目,确定访问的是哪个内存块。它还管内存访问权限、内存布局和缓存策略。
内核和用户空间的地址分离
每个进程有自己的翻译表,内核切进程时也要切页表。但内核的映射很少变。ARMv8 用几个寄存器来处理这个。
TTBR0_EL1 和 TTBR1_EL1 是翻译表基地址寄存器:
TTBR0_EL1
-
- — 用户空间(EL0)的页表基址。用户地址高位全 0,范围
0x00000000_00000000 ~ 0x0000FFFF_FFFFFFFFTTBR1_EL1
-
- — 内核空间(EL1)的页表基址。内核地址高位全 1,范围
0xFFFF0000_00000000 ~ 0xFFFFFFFF_FFFFFFFF
虚拟地址虽然 64 位宽,但实际只用 48 位来映射物理地址。
EL2 和 EL3 只有 TTBR0,没有 TTBR1。意味着这两个异常级别只用 0x0 ~ 0x0000FFFF_FFFFFFFF 这一段。
内核空间和用户空间可以映射到同一片物理地址:
翻译控制寄存器 TCR_EL1 控制高位地址的位数。T0SZ 和 T1SZ 分别指定用户和内核空间的地址范围大小,具体值跟翻译粒度和页表起始地址有关。
TCR_EL1 还控制地址范围和翻译粒度:
IPS:最大输出物理地址大小(000=32 位,101=48 位)
TG1 / TG0:内核/用户空间的翻译粒度(00=4KB, 01=16KB, 11=64KB)
翻译表可以有三或四级。第一级从哪开始查,由翻译粒度、TCR_ELn 和 TxSZ 共同决定。
虚拟地址到物理地址
CPU 发出一个虚拟地址时,MMU 开始翻译。假设 n 位虚拟地址,高位的 VA[63:n] 必须全 0 或全 1,否则触发地址错误。低位做偏移。
举个单级查找的例子,假设 42 位虚拟地址、64KB 粒度:
- VA[63:42] = 1 用 TTBR1,= 0 用 TTBR0VA[41:29] 索引第一级翻译表(8192 个 64 位条目),MMU 读对应条目MMU 检查条目有效性和访问权限条目大小 512MB(8192 × 64),物理地址的 [47:29] 位来自条目VA[28:0] 作为偏移直接对应 PA[28:0]
单级查找分区太粗,实际用多级页表。一级描述符可以指向二级翻译表地址。下图是 stage1 + stage2 都是 64KB 粒度的两级查找:
多级流程:
- VA[63:42] 决定 TTBR1 还是 TTBR0VA[42:29] 索引第一级 8192 个条目第二级条目指向第三级翻译表地址VA[28:16] 索引第三级条目第三级条目指向 64KB 页PA[47:16] 来自第三级条目,VA[15:0] 直接对应 PA[15:0]
安全和物理地址
ARM 定义了两个物理地址空间:安全和非安全。非安全世界只能访问非安全空间。安全世界使能 MMU 后,通过翻译表可以同时访问两种空间。
这会引起缓存一致性问题。比如安全的 0x8000 和非安全的 0x8000 是不同的物理地址,但它们可以同时出现在缓存里。非安全访问安全内存会被阻止,但缓存里可能有两份数据。安全世界访问非安全内存时,需要用非安全方式访问。
多重虚拟地址空间
任何时候只有一个虚拟地址空间在使用。但三个异常级别有三组 TTBR,所以有三个并行的虚拟地址空间(EL0/1、EL2、EL3):
EL1/0 分安全和非安全。但物理寄存器只有一组 TTBR0_EL1、TTBR1_EL1、TCR_EL1,切换世界时需要安全监视器保存和恢复它们。
MMU 禁用时的行为
MMU 使能时翻译表控制内存属性。MMU 禁用时(如复位后),内存用默认属性:
指令获取默认是普通内存,可缓存属性由 SCTL_ELx.I 控制
-
- I=0:非缓存,Outer ShareableI=1:可缓存,Write-Through Read-Allocate No Write-Allocate
Cache 读写简单理解:
读 miss:Read through(直接读内存)或 Read allocate(先加载到缓存再读) 写 hit:Write through(同时写缓存和内存)或 Write back(只写缓存,再 flush) 写 miss:Write allocate(先加载到缓存再写)或 No write allocate(直接写内存)
Outer Shareable:访问的数据会缓存到所有核心的缓存中。
配置和使能 MMU
写 MMU 控制寄存器要等上下文同步事件发生后才会生效:
MSR TTBR0_EL1, X0 // 设置 TTBR0
MSR TTBR1_EL1, X1 // 设置 TTBR1
MSR TCR_EL1, X2 // 设置 TCR
ISB // 确保上述配置在 MMU 使能前已生效
MRS X0, SCTLR_EL1 // 读系统控制寄存器
ORR X0, X0, #1 // 设置 [M] 位使能 MMU
MSR SCTLR_EL1, X0 // 写回
ISB
这里要求直接映射——保证写完 SCTL_EL1.M 后下一条指令能正确执行。
翻译表格式
Armv8-A 支持三种格式:
- AArch64 长描述符格式(主要用于 64 位)Armv7-A 长描述符格式(兼容 32 位)Armv7-A 短描述符格式(只在 EL0/EL1 stage1 用)
AArch64 长描述符格式和带大物理地址扩展的 v7 长描述符类似,增加了 0 级表索引,最多支持 48 位输入输出地址。
地址的 [63:48] 要么全 0 全 1,要么高 8 位用作 VA 标记——架构不支持完整的 64 位寻址。
AArch64 支持三种翻译粒度:4KB、16KB、64KB。粒度越大,需要的翻译表级数越少。芯片支持哪些粒度可以读 ID_AA64MMFR0_EL1 寄存器。
描述符格式
所有级别的翻译表都用同一种描述符格式。0 级描述符只能指向 1 级表,3 级描述符只能输出块地址,不能再指向下级表。
描述符 [1:0] 位标识类型:
- 下一级表的地址(继续细分)内存块地址(大小可变)故障或无效
粒度影响
粒度大小决定翻译表级数和大小。如果 VA 范围限制在 39 位内,0 级表可以省略。
以 4KB 粒度为例,如果 TTBCR 配置低地址只跨越 1GB,0 级和 1 级都不需要,从 2 级开始查,到 3 级就到 4KB 页面了。
4KB 粒度
最多 4 级查找。48 位地址每级用 9 位索引(512 个条目),最后 12 位在 4KB 页内做偏移。
参考:
- https://blog.csdn.net/yhb1047818384/article/detail
189