1. 课程导入与学习地图

各位同学,咱们今天正式开讲ARMv8。说实话,我第一次接触ARMv8是在一个移动设备项目上,当时老板扔过来一块新板子,说“这个跑64位,你搞搞看”。我翻开手册一看,好家伙,跟ARMv7完全不是一个思路。从那天起我就意识到,学ARMv8不能光看寄存器名字,得先搞懂它的设计哲学。

这一章,咱们就聊聊ARMv8到底改了啥。我会从三个角度切入:演进路线、两种执行状态、以及异常等级模型。这三个东西搞明白了,后面讲性能调优你才能听懂我在说什么。

1.1 从ARMv7到ARMv8:不只是32位到64位

很多人觉得ARMv8就是ARMv7的64位升级版。嗯,这么说也不算错,但太表面了。我个人的理解是,ARMv8是一次架构层面的重构,而不仅仅是位数扩展。

咱们先看一张对比表,把关键差异列出来:

特性 ARMv7 (AArch32) ARMv8 (AArch64)
通用寄存器 16个 (r0-r15) 31个 (x0-x30)
程序计数器 通用寄存器之一 (r15) 独立寄存器 (PC)
指令集 ARM/Thumb/Thumb-2 A64 (固定32位)
异常模型 处理器模式 (7种) 异常等级 (EL0-EL3)
虚拟化支持 可选扩展 原生支持 (EL2)
寻址空间 32位 (4GB) 48位 (256TB)

你看,寄存器数量翻了一倍,PC不再是通用寄存器了——这个改动我特别喜欢。以前在ARMv7上调试时,一不小心把r15改了,整个程序就飞了。ARMv8把PC独立出来,安全性好了很多。

还有一个细节:ARMv8的指令集是固定32位长度的。这意味着什么?每条指令都占4字节,没有Thumb模式那种16位指令。好处是解码简单、流水线效率高;坏处是代码密度下降了。我在做嵌入式存储受限的项目时,就因为这个吃过亏——同样的C代码,编译成A64比编译成Thumb-2大了将近30%。

我的经验:如果你在做存储敏感型产品(比如Flash只有256KB的IoT设备),ARMv8的A64指令集可能不是最优选择。这时候可以考虑用AArch32兼容模式,或者干脆继续用ARMv7。别为了追新而追新。

1.2 AArch64与AArch32:两种执行状态

ARMv8最巧妙的设计之一,就是同时支持两种执行状态:AArch64和AArch32。说白了,一个芯片可以跑64位操作系统,同时兼容32位的老应用。

我当年在一个项目里就遇到过这种情况:系统跑的是64位Linux,但有个第三方库只提供了32位版本。当时我差点准备重写整个库,后来发现ARMv8的AArch32兼容模式直接就能跑——省了我两周时间。

两种状态的核心区别:

  • AArch64:使用A64指令集,31个64位通用寄存器(x0-x30),独立的PC和SP,异常等级模型
  • AArch32:兼容ARMv7,使用A32/T32指令集,16个32位寄存器(r0-r15),沿用处理器模式模型

这里有个关键点:两种状态不能混用。一个异常等级要么是AArch64,要么是AArch32,不能同时存在。比如EL1跑64位内核,EL0可以跑32位应用——但EL1和EL0必须都是AArch64,或者都是AArch32。

注意:我曾经见过一个团队,在移植系统时把EL1设成AArch64,EL0设成AArch32,结果系统启动到一半就挂了。原因是异常等级之间的状态切换有严格限制——只有通过异常返回(ERET)才能改变执行状态,而且必须保证目标等级的状态配置正确。

咱们用一张图来理解这两种状态的关系:

AArch64与AArch32执行状态关系图 AArch64与AArch32执行状态关系 AArch64 执行状态 A64 指令集 (固定32位) 31个64位通用寄存器 (x0-x30) 独立PC、SP、异常等级模型 寻址空间: 48位 (256TB) AArch32 执行状态 A32/T32 指令集 (16/32位混合) 16个32位通用寄存器 (r0-r15) 处理器模式模型 (7种模式) 寻址空间: 32位 (4GB) 兼容 一个异常等级只能选择一种执行状态,不能混用

实用技巧:判断当前CPU处于哪种状态,可以读CurrentEL寄存器。如果返回0x4,说明在EL2且是AArch64;如果返回0x2,说明在EL1且是AArch32。我在调试启动代码时经常用这个来确认状态切换是否成功。

1.3 异常等级模型:EL0到EL3

ARMv8的异常模型是我认为最值得讲的部分。ARMv7有7种处理器模式(User、FIQ、IRQ、Supervisor、Abort、Undefined、System),说实话,有点乱。我当年背这些模式背得头大,而且不同模式之间的权限关系也不够清晰。

ARMv8直接简化成了4个异常等级:

  • EL0:用户态,权限最低,运行应用程序
  • EL1:操作系统内核态,运行OS内核
  • EL2:虚拟机监视器(Hypervisor),运行VMM
  • EL3:安全监视器(Secure Monitor),运行安全固件

数字越大,权限越高。EL3最高,EL0最低。这个设计思路跟x86的Ring 0-3有点像,但更简洁。

我画了一张异常等级的结构图,方便大家理解:

ARMv8异常等级模型 ARMv8 异常等级模型 (EL0 - EL3) EL3 - 安全监视器 (Secure Monitor) 权限最高 | 安全固件、ATF、电源管理 EL2 - 虚拟机监视器 (Hypervisor) 虚拟化管理 | 支持多个OS同时运行 EL1 - 操作系统内核 (OS Kernel) Linux/RTOS内核 | 内存管理、中断处理 EL0 - 用户态 (User Application) 权限最低 | 运行应用程序 权限递减 异常上升 异常发生时,处理器从低等级升到高等级 异常返回时,从高等级降回低等级

异常等级之间是怎么切换的?说白了就是通过异常和异常返回。当EL0的应用触发了一个系统调用(SVC指令),处理器会升到EL1,让内核来处理。当内核处理完,执行ERET指令,又降回EL0。

这里有个我踩过的坑:异常等级之间的寄存器组是独立的。每个等级都有自己的SP_ELx、SPSR_ELx、ELR_ELx。如果你在EL1写了一个值到SP_EL0,然后切到EL0,那个值并不会自动生效——你得手动切换。我曾经在写一个上下文切换代码时忘了这个,结果切回用户态后栈指针指向了错误地址,系统直接panic。

关键概念:异常等级模型的核心价值在于隔离。EL0的应用崩溃了,不会影响EL1的内核;EL1的内核崩溃了,不会影响EL2的Hypervisor。这种分层隔离机制,是ARMv8安全性和稳定性的基石。

1.4 异常等级与执行状态的组合

最后咱们聊聊这两个概念的组合。一个ARMv8系统,每个异常等级可以独立选择AArch64或AArch32。常见的组合方式:

场景 EL3 EL2 EL1 EL0
纯64位系统 AArch64 AArch64 AArch64 AArch64
64位内核+32位应用 AArch64 AArch64 AArch64 AArch32
32位兼容系统 AArch32 AArch32 AArch32 AArch32
虚拟化场景 AArch64 AArch64 AArch64/AArch32 AArch64/AArch32

我个人建议,新项目尽量用纯64位。虽然32位兼容很诱人,但混合模式会带来额外的性能开销——每次异常等级切换时,处理器都要做状态转换,这个转换是有代价的。我在一个网络设备项目上做过测试,纯64位比混合模式在中断响应上快了大约15%。

调试小技巧:用SCR_EL3.RW位可以控制EL3以下所有等级的执行状态。如果这个位是1,EL2和EL1都是AArch64;如果是0,都是AArch32。我在调试启动流程时,第一步就是检查这个寄存器,确保状态配置正确。