1. 课程导入

各位好,我是老张。做ARM底层开发十几年了,今天咱们聊聊ARMv8架构的根儿。

ARMv8是个分水岭。在这之前,ARM处理器一直是32位的天下——ARMv4到ARMv7,大家熟知的Cortex-A8、A9、A15都是32位核心。但到了2011年,ARM推出了v8架构,第一次引入了64位指令集。说实话,我当年刚看到ARMv8手册时,第一反应是:这哪是升级,简直是重新设计了一套架构。

1.1 从AArch32到AArch64:为什么要有两套执行状态?

ARMv8最核心的设计决策,就是同时支持两套执行状态:AArch32和AArch64。

你可能会问:为什么不直接抛弃32位?

嗯,这里有个现实问题。2011年的时候,Android、iOS生态里大量应用还是32位的。如果ARM直接砍掉32位支持,那所有老应用都得重写——这不现实。所以ARM的做法是:在同一个处理器里,同时塞进两套执行环境。

对比维度 AArch32 AArch64
指令集宽度 32位(ARM/Thumb/Thumb-2) 固定32位(A64指令集)
通用寄存器 16个(r0-r15),32位宽 31个(x0-x30),64位宽
程序计数器(PC) 作为通用寄存器r15 独立寄存器,不可直接访问
栈指针(SP) 多个模式共享SP 每个异常等级有独立SP
异常返回 使用SUBS PC, LR指令 使用ERET指令
虚拟化支持 需要TrustZone扩展 原生支持EL2虚拟化

我个人习惯把AArch64看作一个"干净的重新开始"。它去掉了AArch32里很多历史包袱——比如条件执行(除了少数分支指令)、桶式移位器、LDM/STM多寄存器加载存储。这些在32位时代是特色,但在64位时代反而成了性能瓶颈。

我的经验:在移植老代码时,最容易踩坑的是PC寄存器的访问方式。AArch32里你可以直接读r15拿到当前指令地址+8,但AArch64里PC是隐式的,需要用ADR或ADRL指令来获取。我曾经有个同事没注意这个差异,调试了整整两天才发现是PC读取方式变了。

1.2 异常等级模型:ARMv8的"特权分层"设计

ARMv8引入了一个非常清晰的特权模型——异常等级(Exception Level, EL)。说白了,就是给软件分了四个等级,从EL0到EL3,数字越大权限越高。

核心概念:异常等级决定了你能访问哪些系统资源。EL0最受限,EL3拥有最高权限。

  • EL0:用户态。跑应用程序的地方。不能访问系统寄存器,不能配置MMU页表。
  • EL1:操作系统内核态。Linux内核、RTOS都跑在这里。可以配置MMU、中断控制器。
  • EL2:虚拟机监视器(Hypervisor)。KVM、Xen、商业虚拟化方案都在这个等级。
  • EL3:安全监视器(Secure Monitor)。负责安全世界和普通世界的切换,是TrustZone的根基。

为什么这么设计?我举个例子你就明白了。

假设你在手机上跑了一个Android虚拟机。这时候:

  • 虚拟机里的App → EL0(客户机用户态)
  • 虚拟机里的Linux内核 → EL1(客户机内核态)
  • KVM Hypervisor → EL2(宿主机虚拟化层)
  • 安全固件(如ATF) → EL3(安全监视器)

每个等级之间通过异常(Exception)来切换。比如App想访问硬件,会触发SVC指令,从EL0陷入EL1的内核。Hypervisor想接管中断,会配置EL2的中断路由逻辑。

下面这张图是我自己画的异常等级模型,你看一眼就明白了:

ARMv8 异常等级模型(Exception Level) EL3 - Secure Monitor(安全监视器) TrustZone 安全世界/普通世界切换 · 安全启动 · 电源管理 EL2 - Hypervisor(虚拟机监视器) KVM · Xen · 虚拟中断控制 · 两阶段地址翻译 EL1 - OS Kernel(操作系统内核) Linux · RTOS · MMU配置 · 中断处理 · 系统调用服务 EL0 - User Application(用户应用程序) App · 库函数 · 无特权访问 · 通过SVC陷入内核 异常/中断上升(Exception / Interrupt) ERET 返回(Exception Return) 数字越大,特权越高。EL0→EL1→EL2→EL3,异常等级只能升高或平级切换,不能直接降级。
避坑指南:我曾经在一个项目里,把Hypervisor的代码错误地放在了EL1而不是EL2。结果虚拟机的内存隔离完全失效——客户机内核可以直接读到宿主机内存。排查了三天才发现是异常等级配置错了。记住:EL2是虚拟化的门槛,没有EL2就没有真正的硬件辅助虚拟化。

1.3 执行状态与寄存器组:AArch64的"大换血"

从AArch32切换到AArch64,寄存器组的变化可以说是"脱胎换骨"。我刚开始接触时也花了些时间适应。

通用寄存器:从16个到31个

AArch32有16个通用寄存器(r0-r15),其中r15是PC,r13是SP,r14是LR。到了AArch64,变成了31个通用寄存器(x0-x30),全部64位宽。而且PC、SP、LR都独立出来了,不再占用通用寄存器编号。

这意味着什么?

  • 更多寄存器变量:编译器可以分配更多变量到寄存器,减少栈访问。我实测过,SPEC CPU2006在AArch64上比AArch32平均快8-12%,寄存器增多是重要原因。
  • 不再有"r15即PC"的坑:AArch32里,如果你不小心写了MOV r15, r0,程序直接跳转了。AArch64里,PC是只读的,必须用B、BL、RET等分支指令才能修改。
  • SP有独立的影子副本:每个异常等级都有自己的SP_ELx,切换异常等级时自动切换栈指针,不会互相干扰。

系统寄存器:用MSR/MRS替代CP15

AArch32时代,配置系统控制寄存器要通过协处理器CP15。我记得当年写CP15汇编代码时,经常要翻ARM手册查协处理器编号和操作码——非常容易写错。

AArch64彻底改了:所有系统寄存器都通过MSR(Move to System Register)和MRS(Move from System Register)指令访问。寄存器名字也变成了可读的字符串,比如:

/* AArch64 配置系统控制寄存器 */
MRS x0, SCTLR_EL1      /* 读取系统控制寄存器到x0 */
ORR x0, x0, #0x1       /* 使能MMU(bit 0) */
MSR SCTLR_EL1, x0      /* 写回 */

/* 对比AArch32的CP15写法 */
MRC p15, 0, r0, c1, c0, 0   /* 读取SCTLR到r0 */
ORR r0, r0, #0x1
MCR p15, 0, r0, c1, c0, 0   /* 写回 */

你看,AArch64的写法是不是直观多了?SCTLR_EL1一看就知道是"系统控制寄存器,EL1等级"。而AArch32的p15, c1, c0, 0——不查手册根本不知道在操作什么。

我的建议:如果你刚开始学ARMv8,先把AArch64的寄存器命名规则背下来。格式是:<寄存器名>_EL<等级>。比如TTBR0_EL1是EL1的页表基址寄存器,HCR_EL2是EL2的虚拟化配置寄存器。命名即文档,这是ARMv8设计最人性化的地方。

PSTATE:程序状态寄存器

AArch64里,PSTATE(Process State)取代了AArch32的CPSR。但它不是一个物理寄存器,而是一组系统寄存器中相关位的集合。常用的包括:

PSTATE位域 描述 典型用途
N, Z, C, V 条件标志位 比较指令结果,条件分支判断
EL 当前异常等级(2位) 读取当前特权级,调试时常用
SP 栈指针选择(0=SP_EL0, 1=SP_ELx) 内核中切换栈空间
DAIF 中断屏蔽位(D=调试, A=SError, I=IRQ, F=FIQ) 关中断/开中断操作
M 执行状态(0=AArch64, 1=AArch32) 判断当前是32位还是64位模式

我个人调试时最常用的是读取CurrentEL寄存器来确认当前异常等级:

/* 读取当前异常等级 */
MRS x0, CurrentEL
AND x0, x0, #0x3      /* 只保留低2位 */
/* x0 = 0 表示EL0, 1表示EL1, 2表示EL2, 3表示EL3 */

1.4 异常等级切换的典型流程

理解了异常等级模型和寄存器组,咱们来看一个实际场景:用户态App发起系统调用,进入内核态。

  1. App执行SVC #0指令(Supervisor Call),触发同步异常。
  2. 硬件自动保存状态:将PSTATE存入SPSR_EL1,返回地址存入ELR_EL1,然后切换到EL1。
  3. CPU跳转到异常向量表:根据异常类型(同步/IRQ/FIQ/SError)和当前等级,从VBAR_EL1指向的向量表中找到对应入口。
  4. 内核处理系统调用:保存现场,解析系统调用号,执行对应服务。
  5. 执行ERET指令:从ELR_EL1恢复返回地址,从SPSR_EL1恢复PSTATE,回到EL0。

整个过程,异常等级从EL0→EL1→EL0。你想想看,如果没有硬件自动保存SPSR和ELR,操作系统得手动保存这些状态——那性能得多差?

关键点:异常等级切换是硬件自动完成的。你只需要配置好异常向量表(VBAR_ELx)和栈指针(SP_ELx),剩下的交给CPU。这也是ARMv8设计的高明之处——把频繁操作硬件化,减少软件开销。

好了,第一章的内容就到这里。ARMv8的异常等级模型和寄存器组是后续所有调优和虚拟化内容的基础。你把这些搞清楚了,后面讲MMU、缓存、虚拟中断时就会轻松很多。