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)才能改变执行状态,而且必须保证目标等级的状态配置正确。
咱们用一张图来理解这两种状态的关系:
实用技巧:判断当前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有点像,但更简洁。
我画了一张异常等级的结构图,方便大家理解:
异常等级之间是怎么切换的?说白了就是通过异常和异常返回。当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。我在调试启动流程时,第一步就是检查这个寄存器,确保状态配置正确。