ARM MMU 地址翻译
本文通过一次完整的 L1→L2 页表遍历实例,深入解析 ARMv7-A MMU 的地址翻译机制、TLB 缓存体系、内存属性编码及多任务上下文切换的底层细节。
1. MMU 的核心职能
Memory Management Unit(MMU)的首要职责是地址重定位:将处理器发出的虚拟地址(Virtual Address, VA)翻译为主存中的物理地址(Physical Address, PA)。ARM MMU 由硬件自动完成翻译,对应用程序完全透明。
除地址翻译外,MMU 还同时控制三项关键属性:
- 内存访问权限(Access Permissions):谁可以读/写/执行这个页面?
- 内存类型与 Cache 策略:这块内存是 Normal、Device 还是 Strongly-ordered?
- 内存序(Memory Ordering):访问顺序是否需要严格保证?
虚拟内存带来的工程收益:
- 隔离:每个任务拥有独立的虚拟地址空间,物理上共存但逻辑上互不可见
- 透明性:应用编写、编译和链接全部基于虚拟地址,无需知晓物理布局
- 连续假象:即使物理内存碎片化,对应用而言虚拟空间是连续的
- 按需映射:当 MMU 无法翻译一个地址时,会触发 abort 异常,OS 可借此实现按需页面分配(demand paging)
当 MMU 被禁用时,所有虚拟地址直接映射到对应的物理地址(flat mapping)。这意味着尽早使能 MMU 是引导代码的重要目标之一。

1.1 使能 MMU
在使能 MMU 之前,必须先完成以下准备工作:将页表写入内存,并将 TTBR(Translation Table Base Register)设置为指向页表。使能 MMU 的代码序列如下:
1 | MRC p15, 0, R1, c1, C0, 0 ; 读取 SCTLR 控制寄存器 |
这里需要特别注意:如果使能 MMU 会改变当前正在执行的代码所在区域的地址映射(例如从 flat mapping 切换到虚拟地址空间),必须在使能操作前后插入适当的 Barrier 指令保证操作顺序,具体可参考 ARM Architecture Reference Manual 中的推荐序列。
然而,每次 MMU 翻译都直接去遍历内存中的页表——那将意味着每次 LDR/STR 指令都额外触发多次外部内存读取。ARM 的解决方案是在 MMU 内部再加一层与 L1 Cache 类似的高速缓存:TLB。
2. TLB:地址翻译的硬件缓存
每次内存访问都做一次完整的页表遍历(Translation Table Walk)代价巨大——一次 L1+L2 两级遍历需要多次外部内存读取。Translation Lookaside Buffer(TLB)是 MMU 内部的页表缓存,缓存最近使用的虚拟地址到物理地址的翻译结果。
2.1 TLB 的层级结构
ARM 实现通常采用两级 TLB 架构:
1 | Micro-TLB (I-side, 8 entries) ← near I-Cache, 0-cycle hit |
Micro-TLB 的典型配置是 8 条映射(I-side 和 D-side 各 8),命中无需任何额外周期。Main TLB 容量更大(典型 64 条),但命中需要额外延迟。这与 L1/L2 Cache 的分层逻辑完全一致。
和 Cache 类似,TLB 也有自己的替换策略(replacement policy),但这对软件完全透明——只需要知道:TLB 满时,硬件会自动选择旧条目驱逐,为新条目腾出空间。
2.2 TLB 条目内容

每条 TLB 条目不仅仅存储 VA→PA 的映射关系,还包含从页表条目中提取的完整属性:
1 | +----------+------+-------------------------------+ |
例如当前运行 Process A (ASID=5),其用户空间地址 0x0000_1000 被映射到一个 4KB 页。这条 TLB 条目在硬件中的实际内容如下:
1 | VA tag = 0x00001 (VA[31:12], 4KB aligned) |
注意 TLB 中存储的 VA tag 只取 VA 的页号部分(bits[31:12]),页内偏移(bits[11:0])不在 TLB 的 tag 比较中,因为 4KB 页内的所有地址共享同一条翻译——这类似于 CPU Cache 的 tag/index/offset 划分逻辑。
2.3 TLB 一致性与维护
ARM 架构的一条核心规则:只有有效的页表条目才能被缓存到 TLB,无效条目(Fault 条目)永远不会进入 TLB。
这条规则决定了修改页表后是否需要刷 TLB。分两种情况:
情况一:修改已有的有效条目(例如改变一个已映射页面的权限或物理地址)。旧翻译已在 TLB 中,修改内存中的页表后 TLB 保留的是过期数据——必须 invalidate。
这里有一个容易被忽略的细节:造成 TLB 填充的不仅是代码中显式的 LDR/STR——speculative 指令预取和数据读取也可能触发页表遍历,把翻译装进 TLB。这意味着即使你认为某个地址”最近没有被访问过”,它的翻译仍然可能已经在 TLB 里(被推测执行悄悄填进去了)。因此修改任何有效条目后都必须 invalidate,不能依赖”最近没访问所以 TLB 里没有”的假设。这也是内核 TLB 维护路径看起来比其他架构”保守”的原因之一。
情况二:将无效条目改为有效条目(例如 OS 按需分配一个新页面——之前该 VA 对应 Fault 条目,现在填入有效映射)。因为 Fault 条目从未进入 TLB,TLB 中根本不存在这个地址的缓存,所以无需 invalidate。第一次显式访问会自然触发 TLB miss → 页表遍历 → 装载新条目。
Linux 内核中使用 flush_tlb_all() 和 flush_tlb_range() 来完成 TLB 维护,这些函数在底层使用 CP15 的 TLB 无效化操作:
1 | ; 全局无效化所有 TLB 条目 |
TLBIALL 使整个 TLB 全部失效,开销最大,通常只在上下文切换的最后手段中使用(例如 ASID 分配耗尽时不得不全局刷新)。TLBIMVA 按需精准清理——MMU 硬件在 TLB 中搜索匹配指定 VA 的条目并只移除那一项,其他进程的缓存不受影响。代价权衡:全刷简单但波及全局,按地址刷精准但每次需要额外一次 CP15 写操作。
TLB 缓存的是翻译结果,但它缓存的”粒度”取决于页表本身的配置。ARM 提供了从 4KB 到 16MB 四种页大小——每一种都直接影响 TLB 的覆盖效率。
3. 页大小的工程权衡
ARMv7-A 短描述符格式支持四种粒度:
| 粒度 | 大小 | 备注 |
|---|---|---|
| Supersection | 16 MB | 16 个连续 L1 条目,虚拟/物理地址均需 16MB 对齐 |
| Section | 1 MB | 单个 L1 条目直接映射 |
| Large Page | 64 KB | L2 条目的 16 次重复 |
| Small Page | 4 KB | L2 条目标准粒度 |
选择页大小是经典的精细度 vs TLB 覆盖率权衡:
- 小页面(4KB):精细控制权限和 Cache 属性,减少内部碎片。但 TLB 条目覆盖的内存范围小,TLB miss 更频繁。
- 大页面(1MB/16MB):单条 TLB 条目覆盖大片内存,TLB 命中率高。适合大块连续内存(如内核代码段、GPU 帧缓冲)。
此外,每个 L2 翻译表需要 1KB 内存——若使用大量 4KB 页面的精细映射,L2 表本身的物理内存开销也不可忽视。
4. 第一级地址翻译:Section 映射
ARMv7-A 使用两级页表架构,未启用 LPAE 时采用短描述符格式。该格式具有以下约束:
- 32-bit 页描述符
- 最多两级翻译表(L1 + L2)
- 支持 32-bit 物理地址空间
- 支持 16MB/1MB Section 和 64KB/4KB Page
超出 32-bit 物理地址范围的系统需要启用 LPAE 长描述符格式(不在本文范围内)。下面的讨论全部基于短描述符。
4.1 L1 翻译表结构
L1 翻译表(也称 Master Translation Table)将 32-bit 虚拟地址的 4GB 空间划分为 4096 个 1MB 的 Section,每个 Section 对应一个 32-bit(4 字节)的条目。因此 L1 表正好是 4096 × 4 = 16 KB,且必须 16 KB 对齐。
基地址存储在 CP15 的 Translation Table Base Register(TTBR)中(CP15 c2)。

4.2 定位 L1 条目
以 ARM 文档中的标准示例为例,走一遍完整的 L1 查表过程:
1 | 给定: |
4.3 L1 条目完整格式

L1 条目的最低两位 [1:0] 决定了条目类型。以下是四种类型及其判断逻辑:
| bits[1:0] | 类型 | 含义 |
|---|---|---|
00 |
Fault | 未映射,触发 abort 异常 |
01 |
L2 Page Table | 条目指向一个 L2 翻译表的物理基地址 |
10 |
Section (1MB) 或 Supersection (16MB) | 条目直接给出物理地址高 12 位 |
11 |
保留 | — |
Supersection(16MB)的判断依赖 bit[18]:若为 1 则该条目是 Supersection 的一部分,需要 16 个连续相同条目。
三类有效条目的完整 32 位布局(位宽表示从高位到低位的划分):
Fault 条目(bits[1:0] = 00):所有高位被忽略,访问即 abort。
L2 页表指针(bits[1:0] = 01):
1 | [31:10] L2 Descriptor Base Address — 指向 L2 表物理基地址(1KB 对齐) |
Section 条目(bits[1:0] = 10):
1 | [31:20] Section Base Address — 物理 Section 基地址 [31:20] |
Supersection 条目:与 Section 相同,但 bit[18]=1,且 Section Base Address 域使用 bit[31:24](而非 bit[31:20])作为物理基地址,为 16MB 对齐提供更多高位地址位。
4.4 从 Section 条目生成物理地址

Section 映射的一个核心特征是:VA 的 bits[19:0](低 20 位,1MB 范围内偏移)直接穿越到物理地址,不做翻译。这是因为物理 Section 基地址总是 1MB 对齐——物理端 bits[19:0] 永远为 0,所以无需从页表中读取,直接用 VA 的低 20 位即可。
实例:完整的 Section 翻译
1 | VA = 0x0010_0420 (1MB Section #1, 页内偏移 0x420) |
全部翻译在单次 L1 查表后即完成,无需进入 L2——这正是 Section 映射的效率所在。
5. 第二级翻译表
当 L1 条目的 bits[1:0] = 01 时,条目指向一个 L2 翻译表。
5.1 L2 表结构
- 大小:256 个 32-bit 条目 = 1 KB,必须 1KB 对齐
- 粒度:每个条目覆盖 4KB 虚拟地址范围(1MB ÷ 256 = 4KB)
- 索引位:VA bits[19:12](8 位,对应 256 个条目)
5.2 L2 条目完整格式

L2 条目也有三类,由 bits[1:0] 区分:
| bits[1:0] | 类型 | 含义 |
|---|---|---|
00 |
Fault | 未映射 |
01 |
Large Page (64KB) | 一个条目覆盖 64KB,需在表中连续重复 16 次 |
1x (10 或 11) |
Small Page (4KB) | 标准 4KB 页,bit[0] 的差异仅影响 XN 位位置 |
Small Page 条目(bits[1:0] = 1x)的完整位布局:
1 | [31:12] Small Page Base Address — 物理页帧号(4KB 对齐) |
Large Page 条目(bits[1:0] = 01)的完整位布局:
1 | [31:16] Large Page Base Address — 物理页帧号(64KB 对齐) |
5.3 完整的 L1→L2 两级遍历

以下是与上节相同虚拟地址(0x0010_0420)在 L2 映射下的完整翻译过程:
1 | VA = 0x0010_0420 |
对比两个实例可以看到关键区别:Section 映射一次 L1 查表即完成(翻译覆盖 1MB 连续区域),两级页表需要 L1→L2 两次内存读取,但能做到 4KB 粒度的独立控制。

6. 内存属性
页表条目不仅负责地址翻译——它还编码了该内存页面的访问权限、内存类型和 Cache 策略。
6.1 访问权限(AP/APX)
AP[1:0] + APX 组合定义了每个页面在特权模式(Privileged)和非特权模式(Unprivileged/User)下的访问权限:
| APX | AP | Privileged | Unprivileged | 描述 |
|---|---|---|---|---|
| 0 | 00 | No access | No access | 任何访问均触发 abort |
| 0 | 01 | Read/Write | No access | 仅内核可读写 |
| 0 | 10 | Read/Write | Read | 用户态只读(常用于 .rodata) |
| 0 | 11 | Read/Write | Read/Write | 完全访问 |
| 1 | 01 | Read | No access | 特权只读 |
| 1 | 10 | Read | Read | 全局只读 |
当访问违反权限时,数据访问产生 precise data abort,指令预取在指令实际执行前产生 prefetch abort。CP15 的 Fault Address Register 和 Fault Status Register 会记录故障地址和原因,abort 处理器据此决定:是修复页表后重试(如写时复制/COW),还是终止违规进程。
6.2 内存类型:Normal / Device / Strongly-ordered
ARMv7 定义了三种互斥的内存类型:
| 内存类型 | 可缓存 | 关键行为 |
|---|---|---|
| Strongly-ordered | 否 | 所有访问严格按程序顺序执行。所有访问假定为 Shareable |
| Device | 否 | 所有访问按程序顺序,但允许提前到/自其他 Device 区域。用于内存映射外设寄存器 |
| Normal | 可缓存 | 最灵活——可 speculative access、可乱序、可合并、可预取。普通 RAM |
Strongly-ordered 和 Device 内存的关键差异:如果不区分两者,所有外设访问都将被当作 Strongly-ordered 处理——这意味着对两个独立设备的访问也必须严格串行化,导致不必要的性能损失。Device 类型的”程序序”仅保证到同一目标的访问被串行化。
6.3 TEX/C/B 编码详解
页表条目中的 TEX[2:0]、C 和 B 位共同定义了内存类型和 Cache 策略:
| TEX | C | B | 内存类型 | 说明 |
|---|---|---|---|---|
| 000 | 0 | 0 | Strongly-ordered | — |
| 000 | 0 | 1 | Device (Shareable) | 外设寄存器 |
| 000 | 1 | 0 | Normal, Inner/Outer WT, no WA | Write-Through, 无写分配 |
| 000 | 1 | 1 | Normal, Inner/Outer WB, no WA | Write-Back, 无写分配 |
| 001 | 0 | 0 | Normal, Inner/Outer non-cacheable | 不可缓存 |
| 010 | 0 | 0 | Device (Non-shareable) | 旧式非共享设备 |
| 1bb | a | a | Normal Cached, TEX[2]=1 时 | TEX[1:0]=Outer policy, C/B=Inner policy |
最后一行(TEX[2]=1 时)支持内外层 Cache 策略的独立编码。此时 TEX[1:0]、C 和 B 四个位共同构成扩展编码,并由 ARM 定义的映射表解析为 Inner 和 Outer Cache 属性。通常可以理解为:TEX[1:0] 主要编码 Outer Cache 策略,C/B 主要编码 Inner Cache 策略,但最终属性应以 ARM ARM 的 TEX/C/B 映射表为准,而不是分别独立解释。
值得注意的是,某些 Cortex-A8/Cortex-A15 实现并未区分 L1/L2 的 Cache Policy,而是将页表中的 Inner Cacheability 同时应用于 L1 和 L2,因此 Outer 属性不会真正控制 L2 的写策略。
6.4 Execute Never (XN)
XN 位阻止从指定内存地址执行代码。一旦设置:
- 禁止 speculative 指令预取(防止预取设备寄存器引发副作用)
- 若 CPU 尝试执行该页面中的代码,触发 prefetch abort
典型用法:所有 Device 和 Strongly-Ordered 内存区域一律标记 XN=1,用户态数据页(堆、栈)设置 XN=1 防止栈溢出 ROP 攻击。
6.5 Domains(已弃用但需了解)
ARM 架构的 Domain 机制提供了 16 个 Domain ID,每个 Domain 在 DACR(Domain Access Control Register, CP15 c3)中有 2-bit 权限:
- No-access:任何对属于该 Domain 页面的访问均 abort,忽略页面自身权限
- Manager:忽略页面权限,完全访问
- Client:使用页面自身的权限设置
Domain 在 ARMv7 中已被标记为 deprecated,但为确保权限正确生效,仍需为每个 Section 分配 Domain 并将 DACR 对应字段设为 Client。典型的初始化做法:所有页面的 Domain 字段填 0,DACR 所有位置为 Client(0x55555555)。
7. 多任务与 MMU
在多任务 OS 中,每个进程拥有独立的页表。当调度器切换进程时,MMU 需要从”进程 A 的虚拟地址空间”切换到”进程 B 的虚拟地址空间”。ARM 为此提供了 ASID 和 TTBR0/TTBR1 双基址寄存器两套机制。
7.1 ASID:避免上下文切换刷 TLB
每个进程被 OS 分配一个 0-255 的 Address Space ID(ASID),存储在 CP15 c13 的 ASID 寄存器中。
当翻译一个标记为 non-global(nG=1) 的页面时,MMU 不仅需要 VA→PA 匹配,还需要当前 ASID 与 TLB 条目中存储的 ASID 一致。这带来一个深刻的好处:上下文切换时无需刷 TLB。
1 | Process A (ASID=5) ──── TLB ──── Process B (ASID=7) |
没有 ASID 的话,每次上下文切换必须刷掉整个 TLB,新进程的每个首次访问都触发一次完整的页表遍历——这对上下文切换频繁的 Linux 系统而言是灾难性的性能损失。
标记为 global(nG=0) 的页面(如内核代码段和公共库)则不受 ASID 约束——所有进程的 TLB 查询都能匹配这些全局条目,进一步减少 miss。
7.2 TTBR0 和 TTBR1
每个进程需要独立的 L1 页表(16KB),其中大部分条目(内核空间映射)在各进程间完全相同。ARM 提供了 两个 Translation Table Base Register(TTBR0 和 TTBR1)来解决这个冗余问题。
TTB Control Register(CP15 c2)中设置一个参数 N(0-7),决定 VA 如何分配到两个 TTBR:
1 | VA top N bits all zero → use TTBR0 (user space) |
实例:N=7 时,VA 的高 7 位为 0 意味着地址在低 32MB 范围内——这部分由 TTBR0 指向的进程私有页表覆盖。其余 4GB-32MB 空间由 TTBR1 指向的全局内核页表覆盖。
效果:
- 进程私有的用户空间页表(TTBR0)只需包含 32 个条目(128 字节),而非完整 4096 条
- 内核空间映射在 TTBR1 中只维护一份,所有进程共享
- 上下文切换只需修改 TTBR0 和 ASID
但这里存在一个微妙的原子性问题:TTBR0 和 ASID 的更新是两个独立的 CP15 写操作。如果 speculative 访问在新 TTBR0 + 旧 ASID 或旧 TTBR0 + 新 ASID 的不一致状态下发生,可能导致错误的 TLB 填充。ARM Architecture Reference Manual 推荐了特定的指令序列来处理这一竞态。
7.3 FCSE(已弃用)
Fast Context Switch Extension(FCSE)在 ARMv4 引入但自 ARMv6 起弃用。它通过将进程 ID 替换 VA 高 7 位来避免上下文切换时刷 Cache/TLB——这本质上是 ASID 的前身。新代码应使用 ASID + TTBR0/TTBR1 机制,FCSE 仅供维护遗留系统时参考。
FCSE 启用的系统中,ARM 文档会区分 Modified Virtual Address(MVA)和 Virtual Address(VA):MVA 是 VA 经过 FCSE 进程 ID 替换后的地址,VA 是处理器原始发出的地址。这一区分仅当 FCSE 启用时有意义,在 FCSE 禁用(现代系统)时 MVA 等同于 VA。
8. 关键要点
MMU 不只是地址翻译器——它还通过页表条目同步输出访问权限、内存类型和 Cache 策略。整个翻译过程中从 VA 到 PA 的路径完全由硬件自动完成,软件只负责维护内存中的页表数据结构。
两级页表架构是控制粒度与存储开销的平衡点:1MB Section 映射一次 L1 查表即完成,适合大块连续区域;4KB/64KB L2 页面适合精细控制的用户空间映射。短描述符格式最大支持 32-bit 物理地址,两级翻译表架构。
TLB 是两级的:Micro-TLB(各 8 条目,0 周期命中)+ Main TLB(~64 条目)。无效条目永不进入 TLB,因此只修改有效条目才需 invalidate。Speculative access 也会触发页表遍历,必须在修改有效条目后做 TLB invalidation。
TEX/C/B 三位一体编码决定了内存行为:Normal/Device/Strongly-ordered 三种互斥类型通过同一个 5-bit 组合编码。TEX[2]=1 时支持 Inner/Outer Cache 策略分离。
ASID 是多任务 MMU 的核心创新:nG=1 的页面在 TLB 中附带 ASID 存储,上下文切换时只需写 TTBR0 + ASID 寄存器,无需刷 TLB。这比没有 ASID 时每次切换都刷 TLB 节省了巨大的 cache warming 开销。
TTBR0/TTBR1 双基址寄存器分离了用户态和内核态页表:用户空间映射只需维护少量条目的瘦页表,内核映射全局共享。典型配置 N=7,32MB 用户空间 + 4GB-32MB 内核空间。
Domain 机制虽已弃用但仍需正确初始化:所有页面的 Domain 字段填 0,DACR 设为 Client(
0x55555555)。