3. ARMv8-A 页表描述符:页表项(PTE)格式详解、属性位(Access Flag、Dirty Bit、Execute Never)、页表缓存(TLB)机制
好,咱们进入正题。ARMv8-A 的页表,说实话我第一次看的时候也有点头大。64位地址空间,四级页表,各种属性位……但别怕,拆开来看其实就那么几样东西。
我个人习惯,研究页表先从PTE(Page Table Entry)下手。PTE就是页表里的每一个条目,它告诉MMU:这块虚拟内存对应哪块物理内存,权限是什么,能不能缓存,等等。
3.1 页表项(PTE)格式详解
ARMv8-A 的PTE是64位的。嗯,这里要注意,不同级别的页表,PTE的格式略有不同。但核心字段是通用的。
我直接给个典型格式,以4KB页、48位地址空间为例:
位域 宽度 描述
[63:48] 16 Upper attributes(高位属性)
[47:12] 36 Output address(物理地址高位,4KB页对齐)
[11:2] 10 Lower attributes(低位属性)
[1:0] 2 Descriptor type(描述符类型:00无效,01页表,11页)
你看,核心就是物理地址加上一堆属性。Descriptor type 决定了这个PTE指向什么:
- 00:无效条目,访问会触发缺页异常
- 01:指向下一级页表(块描述符)
- 11:指向一个页(页描述符)
我在项目中遇到过一个问题:有人把页表项的类型位写错了,结果MMU死活不认。查了半天,发现是位域操作搞反了。所以啊,位操作一定要仔细。
3.2 属性位详解
属性位才是PTE的灵魂。咱们挑三个最重要的说:Access Flag、Dirty Bit、Execute Never。
3.2.1 Access Flag(AF)
Access Flag,说白了就是“这个页被访问过吗?”
AF位在PTE的[10]位。硬件在第一次访问这个页时,如果AF为0,会触发一个异常(Access Flag fault)。操作系统在异常处理里把AF置1,然后继续执行。
为什么要这么设计?你想想看,操作系统可以借此知道哪些页被真正用过。这在页面置换算法里很有用——没被访问过的页,优先换出去。
我个人的经验:在嵌入式系统里,如果内存紧张,我通常会利用AF位做冷热页统计。把长时间没被访问的页换到压缩内存或闪存里,效果不错。
3.2.2 Dirty Bit(DB)
Dirty Bit,脏位。它表示这个页的内容是否被修改过。
DB位在PTE的[53]位(注意,这是ARMv8.1之后的标准位置,早期版本在[51]位)。硬件在第一次写这个页时,如果DB为0,会触发一个异常。操作系统在异常处理里把DB置1。
为什么需要脏位?说白了,就是给页面换出用的。一个页如果没被写过(干净页),换出时直接丢弃就行,不用写回磁盘。如果被写过(脏页),换出前必须写回。
避坑指南:我曾经在一个项目里忘了处理脏位异常,结果一写内存就死机。后来发现是内核的异常处理没注册。记住:AF和DB的异常处理一定要配齐,缺一个都不行。
3.2.3 Execute Never(XN)
Execute Never,禁止执行。这个位在PTE的[54]位。
XN位为1时,这个页里的数据不能被当作指令执行。这是实现NX(No-Execute)保护的关键。
你想想看,如果没有XN位,攻击者把恶意代码写到数据段,然后跳转过去执行,系统就沦陷了。有了XN位,数据页不可执行,攻击者就没辙了。
ARMv8-A 还支持PXN(Privileged Execute Never),在[53]位。PXN禁止特权级(EL1/EL2/EL3)执行该页的代码。这是内核防护用的——即使内核被攻破,也不能执行用户空间的代码。
注意:XN和PXN不是互斥的。我建议你两个都配好。用户空间页设置XN=1、PXN=0;内核空间页设置XN=0、PXN=1。这样用户不能执行内核代码,内核也不能执行用户代码,双向隔离。
3.3 页表缓存(TLB)机制
TLB,Translation Lookaside Buffer,页表缓存。说白了,就是MMU内部的一个小缓存,存最近用过的虚拟地址到物理地址的映射。
为什么需要TLB?因为页表在内存里,每次地址翻译都要查内存,太慢了。TLB是SRAM,访问只要一个时钟周期,比查内存快两个数量级。
ARMv8-A 的TLB有几个特点:
- 多级TLB:L1 TLB(指令和数据分开)和L2 TLB(统一)。L1快但小,L2慢但大。
- 全相联或组相联:具体实现由芯片厂商决定。Cortex-A系列通常用全相联。
- ASID支持:Address Space ID,区分不同进程的TLB条目。切换进程时不用刷TLB,只要切换ASID就行。
TLB的维护是个技术活。ARMv8-A 提供了几条指令:
TLBI VMALLE1 // 无效化所有EL1的TLB条目
TLBI VAAE1, x0 // 按ASID无效化
TLBI VAE1, x0 // 按虚拟地址无效化
DSB SY // 确保TLB操作完成
ISB // 刷新指令流水线
我建议你记住一个原则:修改页表后,一定要执行TLB无效化操作。否则MMU可能还在用旧的缓存条目,导致访问错误。
我踩过的坑:有一次我动态映射了一段内存,忘了刷TLB。结果在另一个核上访问时,MMU用了旧的TLB条目,直接映射到了错误的物理地址。数据写飞了,查了两天才找到原因。从那以后,我每次修改页表都严格执行:修改页表 → DSB → TLBI → DSB → ISB。
TLB的命中率直接影响系统性能。我个人的经验是:
- 尽量使用大页(2MB或1GB),减少TLB条目数
- 避免频繁切换进程上下文,减少TLB刷新
- 利用ASID,让TLB条目跨进程复用
嗯,关于ARMv8-A的页表描述符和TLB,核心内容就这些。记住:PTE的格式要记牢,AF/DB/XN三个属性位要理解透彻,TLB的维护操作不能忘。下次咱们聊RISC-V的页表,你会发现很多相似之处,但也有不少差异。