3. ARMv8-A架构基础:AArch64与AArch32执行状态、异常级别模型(EL0-EL3)、内存管理单元(MMU)与页表

好,咱们今天聊聊ARMv8-A架构里最核心的几个概念。说实话,我刚接触ARMv8的时候,也被这些新名词搞得有点晕。但别担心,咱们一个一个拆开来看。

3.1 AArch64与AArch32:两种执行状态

ARMv8-A最大的变化,就是引入了AArch64执行状态。说白了,就是支持了64位指令集。但有意思的是,它并没有完全抛弃32位,而是保留了AArch32状态。

我个人习惯把AArch64看作是“原生64位模式”,而AArch32则是“兼容模式”。你想想看,一个芯片既要跑新的64位Linux,又要兼容老的32位应用,怎么办?ARM的做法就是让处理器能在两种状态之间切换。

关键区别:

  • AArch64:使用64位通用寄存器(X0-X30),支持64位虚拟地址空间,指令长度固定为32位
  • AArch32:使用32位通用寄存器(R0-R14),支持32位虚拟地址空间,指令可以是16位(Thumb)或32位

我在项目中遇到过一个问题:有个客户想把老的32位网络协议栈移植到新平台,结果发现AArch64下有些系统调用接口变了。嗯,这里要注意——两种状态下的异常处理模型完全不同,不能简单认为“64位就是32位的扩展”。

3.2 异常级别模型:EL0到EL3

ARMv8引入了异常级别(Exception Level)的概念,从EL0到EL3共4个级别。数字越大,特权越高。

异常级别 典型用途 运行环境
EL0 用户应用程序 无特权,不能直接访问硬件
EL1 操作系统内核 可管理MMU、中断等
EL2 虚拟机监视器(Hypervisor) 虚拟化支持
EL3 安全监控(Secure Monitor) 最高特权,管理安全世界切换

为什么会设计成4个级别?我个人的理解是:这为虚拟化安全提供了硬件级别的支持。你想想看,如果没有EL2,虚拟机管理就得在EL1实现,那得多麻烦。

避坑指南:我曾经在调试一个安全启动方案时,发现EL3的代码里访问了EL1的寄存器,结果直接触发了一个“Undefined Instruction”异常。记住:高特权级可以访问低特权级的资源,但反过来不行。而且,不同EL之间的切换必须通过异常(Exception)来完成。

3.3 内存管理单元(MMU)与页表

MMU,说白了就是负责虚拟地址到物理地址的转换。ARMv8的MMU比ARMv7复杂了不少,但核心思想没变——通过页表来实现地址映射。

3.3.1 页表结构

ARMv8-A支持4级页表(Level 0 到 Level 3),每级页表负责不同粒度的映射:

  • Level 0:覆盖整个地址空间(512GB粒度)
  • Level 1:1GB粒度
  • Level 2:2MB粒度
  • Level 3:4KB粒度

我个人习惯用“逐级细化”来理解:就像你在地图上找一条街,先看国家(L0),再看城市(L1),然后看街道(L2),最后看门牌号(L3)。

页表条目(PTE)的关键字段:

  • 输出地址:物理地址的高位
  • 属性位:可读、可写、可执行、缓存策略等
  • 访问标志:是否被访问过、是否脏页
  • 下一级页表基地址:指向更细粒度的页表

3.3.2 地址翻译流程

当CPU访问一个虚拟地址时,MMU会:

  1. TTBR0_EL1TTBR1_EL1寄存器获取页表基地址(取决于虚拟地址范围)
  2. 逐级查找页表,直到找到最终的物理地址
  3. 检查权限和属性,如果违反则触发缺页异常权限异常

重要提醒:我曾经遇到一个性能问题——页表层级太多导致TLB(Translation Lookaside Buffer)命中率低。后来我改用2MB大页(Level 2映射),TLB miss减少了将近40%。所以,如果你的应用有大量连续内存访问,尽量使用大页

3.3.3 代码示例:配置MMU

下面是一个简化的MMU初始化代码(伪代码风格):

// 设置页表基地址
write_sysreg(ttbr0_el1, page_table_base);

// 设置翻译控制寄存器
// TCR_EL1: 配置页表大小、粒度、缓存策略等
uint64_t tcr = (0x10 << 16) |  // TG0 = 4KB粒度
               (0x3 << 14) |   // SH0 = Inner Shareable
               (0x1 << 10) |   // ORGN0 = Write-Back
               (0x1 << 8) |    // IRGN0 = Write-Back
               (0x0 << 6) |    // T0SZ = 32位地址空间
               (0x0 << 0);     // 其他默认
write_sysreg(tcr_el1, tcr);

// 启用MMU
// SCTLR_EL1 的 M位(bit 0)控制MMU使能
uint64_t sctlr = read_sysreg(sctlr_el1);
sctlr |= (1 << 0);  // 设置M位
write_sysreg(sctlr_el1, sctlr);

// 执行ISB指令确保配置生效
__asm__ volatile("isb");

嗯,这里要注意——启用MMU之前,必须确保页表已经正确设置,否则一开MMU就会触发异常。我刚开始做的时候,就因为这个bug调试了整整两天。

3.4 总结与个人体会

ARMv8-A的这三个核心概念——执行状态、异常级别、MMU——其实是相互关联的:

  • 执行状态决定了你用的是64位还是32位指令集
  • 异常级别决定了你拥有多少特权
  • MMU则负责在虚拟地址和物理地址之间建立桥梁

我个人觉得,理解这些概念最好的方法就是动手写代码。哪怕只是写一个简单的裸机程序,配置一下MMU,你都会对这些概念有更深的理解。

最后一个小建议:如果你刚开始学习ARMv8,建议先从AArch64 + EL1开始,这是最常用的组合。等熟悉了再逐步深入到EL2(虚拟化)和EL3(安全)。别一上来就想搞懂所有异常级别,容易把自己绕晕。

好,这一章就到这里。下一章咱们聊聊中断和异常处理,这可是嵌入式开发里的重头戏。