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的数据。那段时间排查得我头大。
正确的做法是:
- 保存当前进程的TTBR0值到进程控制块
- 加载新进程的TTBR0值
- 执行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地址空间划分与切换流程
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配置不当导致的诡异问题——系统跑着跑着就挂了,或者某个进程访问内存时莫名其妙地崩溃。嗯,这些坑,希望你能避开。