TTBR寄存器详解:TTBR0与TTBR1

MMU的核心,说白了就是查表。而查表的起点,就是TTBR——Translation Table Base Register。ARMv7里有两个:TTBR0和TTBR1。很多人刚接触时觉得这俩差不多,其实分工完全不同。

我刚开始做MMU配置时,就踩过TTBR0/TTBR1搞混的坑。那次系统跑起来后,用户态进程一访问内存就崩,查了两天才发现是TTBR0指向的页表被内核写乱了。嗯,从那以后我对这两个寄存器的分工就记得特别牢。

TTBR0与TTBR1的分工逻辑

ARMv7把虚拟地址空间切成两半:低地址段归TTBR0管,高地址段归TTBR1管。具体切在哪儿?由TTBCR寄存器决定。

寄存器 管辖范围 典型用途
TTBR0 低地址空间(0x00000000 ~ 分界点) 用户态进程页表
TTBR1 高地址空间(分界点 ~ 0xFFFFFFFF) 内核态页表

分界点由TTBCR的N字段控制。N=0时,TTBR0管整个4GB空间,TTBR1不用。N=7时,TTBR0只管最低的1MB,其余全归TTBR1。我常用的配置是N=1,这样TTBR0管低2GB,TTBR1管高2GB,刚好把用户空间和内核空间分开。

关键点:TTBR0和TTBR1的切换是硬件自动完成的。CPU根据当前访问的虚拟地址落在哪个区间,自动选择对应的TTBR去查表。不需要软件干预。

地址空间划分的实战考量

为什么要把地址空间分成两段?你想想看,每个进程都有自己的用户空间页表,但内核空间页表是所有进程共享的。如果只用TTBR0,每次进程切换都得把内核页表重新加载一遍,那效率就太低了。

ARMv7的设计思路是:

  • TTBR0:每个进程独享,存用户态页表基地址。进程切换时更新。
  • TTBR1:所有进程共享,存内核态页表基地址。进程切换时不动。

这样切换进程时,只需要改TTBR0,TTBR1保持不变。内核页表永远在线,系统调用、中断处理都不用重新加载页表。

我的习惯:内核页表放在高地址的固定区域,用TTBR1指向它。用户进程的页表放在低地址,每个进程一份,通过TTBR0切换。这样既保证了隔离性,又减少了TLB刷新开销。

上下文切换时TTBR的更新策略

进程切换时,TTBR0必须更新为新进程的页表基地址。但这里有个坑:TLB里还缓存着旧进程的地址映射。如果不做处理,新进程访问同一个虚拟地址时,可能命中旧进程的TLB条目,读到错误的数据。

我曾经在一个实时系统里遇到过这个问题。进程A和进程B的虚拟地址0x1000映射到不同的物理地址,切换后没刷TLB,结果进程B读到的还是进程A的数据。那段时间排查得我头大。

正确的做法是:

  1. 保存当前进程的TTBR0值到进程控制块
  2. 加载新进程的TTBR0值
  3. 执行TLB刷新操作(ISB + DSB + TLBIALL)
; 上下文切换中的TTBR更新示例
; R0 = 新进程的页表基地址
MCR p15, 0, R0, c2, c0, 0   ; 写TTBR0
ISB                          ; 指令同步屏障
DSB                          ; 数据同步屏障
MOV R1, #0
MCR p15, 0, R1, c8, c3, 0   ; TLBIALL,刷新整个TLB
DSB                          ; 确保TLB刷新完成
ISB                          ; 确保后续指令使用新页表

注意:TLB刷新操作有性能开销。如果进程切换频繁,可以考虑使用ASID(Address Space ID)来减少TLB刷新。每个进程分配一个ASID,TLB条目带上ASID标记,切换时只刷新当前ASID的条目,而不是整个TLB。

TTBR寄存器格式

TTBR0和TTBR1的寄存器格式完全一样。我直接列出来:

位域 名称 说明
[31:14] 基地址 页表基地址的高18位,低14位为0(16字节对齐)
[13:6] RGN 页表本身的内存属性(缓存策略)
[5:3] IRGN 页表本身的指令缓存策略
[2:1] S 页表本身的共享属性
[0] C 页表本身的缓存使能

这里有个容易被忽略的点:TTBR里存的页表基地址,必须是16字节对齐的。低4位硬件直接忽略。我见过有人把页表放在非对齐地址上,结果MMU查表时基地址被截断,映射全乱套了。

避坑指南:我曾经在移植Linux内核时,发现某个平台启动后MMU异常。查到最后,是Bootloader把页表放在了非对齐地址。TTBR的低4位被截断后,页表基地址偏移了16字节,第一级页表项全读错了。从那以后,我每次配置TTBR都会检查地址对齐。

SVG:TTBR0/TTBR1地址空间划分与切换流程

TTBR0/TTBR1地址空间划分与切换流程 虚拟地址空间 0x00000000 ~ 0xFFFFFFFF 分界点(由TTBCR.N决定) 低地址段 → TTBR0 高地址段 → TTBR1 进程A用户页表 进程B用户页表 内核页表(共享) 上下文切换流程 保存旧TTBR0 加载新TTBR0 TLB刷新 说明: 1. TTBR0在进程切换时更新,TTBR1保持不变 2. TLB刷新可用TLBIALL(全刷)或ASID方式(部分刷) 3. 内核页表常驻TTBR1,系统调用和中断无需切换页表

TTBR配置的注意事项

配置TTBR时,有几个点我特别提醒一下:

  • 页表本身的内存属性:TTBR里的RGN、IRGN、S、C位控制的是页表本身在内存中的缓存策略。如果页表被缓存在L1/L2 cache里,查表速度会快很多。但要注意,如果页表被修改了,必须保证cache一致性。
  • TTBR0和TTBR1的切换条件:硬件根据虚拟地址的高位判断用哪个TTBR。如果TTBCR.N=0,TTBR0管全部,TTBR1被忽略。如果N>0,地址的最高N位为0时用TTBR0,非0时用TTBR1。
  • 多核系统的TTBR:每个核有自己的TTBR0和TTBR1。进程切换时,每个核都要更新自己的TTBR0。如果使用ASID,还要同步ASID分配。

我的建议:在初始化阶段,先把TTBR1配好,指向内核页表。然后配TTBCR,设置好地址分界点。最后配TTBR0,指向初始进程的页表。顺序不要搞反,否则中间状态可能让MMU查不到页表。

TTBR的配置看似简单,但涉及地址空间划分、进程切换、TLB管理等多个方面。理解清楚TTBR0和TTBR1的分工,是掌握ARMv7 MMU的关键一步。实际项目中,我见过太多因为TTBR配置不当导致的诡异问题——系统跑着跑着就挂了,或者某个进程访问内存时莫名其妙地崩溃。嗯,这些坑,希望你能避开。