1. ARMv8架构概述:从AArch32到AArch64的演进
各位好,我是老张。做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位时代反而成了性能瓶颈。
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的中断路由逻辑。
下面这张图是我自己画的异常等级模型,你看一眼就明白了:
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——不查手册根本不知道在操作什么。
<寄存器名>_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发起系统调用,进入内核态。
- App执行SVC #0指令(Supervisor Call),触发同步异常。
- 硬件自动保存状态:将PSTATE存入SPSR_EL1,返回地址存入ELR_EL1,然后切换到EL1。
- CPU跳转到异常向量表:根据异常类型(同步/IRQ/FIQ/SError)和当前等级,从VBAR_EL1指向的向量表中找到对应入口。
- 内核处理系统调用:保存现场,解析系统调用号,执行对应服务。
- 执行ERET指令:从ELR_EL1恢复返回地址,从SPSR_EL1恢复PSTATE,回到EL0。
整个过程,异常等级从EL0→EL1→EL0。你想想看,如果没有硬件自动保存SPSR和ELR,操作系统得手动保存这些状态——那性能得多差?
关键点:异常等级切换是硬件自动完成的。你只需要配置好异常向量表(VBAR_ELx)和栈指针(SP_ELx),剩下的交给CPU。这也是ARMv8设计的高明之处——把频繁操作硬件化,减少软件开销。
好了,第一章的内容就到这里。ARMv8的异常等级模型和寄存器组是后续所有调优和虚拟化内容的基础。你把这些搞清楚了,后面讲MMU、缓存、虚拟中断时就会轻松很多。