1. ARM64 体系结构概览:AArch64 执行状态、寄存器模型、异常等级与安全模型

各位好,我是老周。今天咱们开始第一讲,ARM64 体系结构概览。说实话,我做了十几年底层系统,ARM 架构从 v7 到 v8 再到 v9,一路跟下来,感触最深的就是:理解体系结构是性能调优的根基。你连 CPU 有多少个寄存器、异常怎么切换都不知道,优化代码就是瞎蒙。

这一章,咱们把 AArch64 执行状态、寄存器模型、异常等级和安全模型这四个核心概念讲透。嗯,内容不少,但都是硬货。

1.1 AArch64 执行状态:告别 32 位,拥抱 64 位

ARMv8 架构引入了两种执行状态:AArch64 和 AArch32。咱们课程主要关注 AArch64,也就是 64 位执行状态。

说白了,AArch64 就是 ARM 的 64 位指令集。它跟之前的 32 位 ARM 指令集相比,变化非常大。我刚开始从 ARMv7 切到 ARMv8 时,最不习惯的就是指令编码完全变了——不再是条件执行遍地走,而是更接近 RISC 的简洁风格。

核心要点:AArch64 使用 64 位虚拟地址,提供 31 个通用寄存器(X0-X30),以及专用的 SP、PC、XZR 寄存器。指令长度固定为 32 位,寻址空间最大支持 48 位(未来可扩展到 52 位)。

AArch64 与 AArch32 的关键区别,我列个表方便对比:

特性 AArch64 AArch32
通用寄存器 31 个(X0-X30),64 位宽 15 个(R0-R14),32 位宽
指令长度 固定 32 位 32 位(ARM)/ 16 位(Thumb)
虚拟地址宽度 48 位(默认),可扩展 32 位
条件执行 仅少数指令支持 几乎所有指令支持
PC 访问 不能直接写 PC 可以直接写 PC
加载/存储对 LDP/STP(双字) LDM/STM(多字)

你想想看,为什么 ARM 要抛弃 Thumb 和条件执行?我个人理解,是为了简化硬件设计,提高流水线效率。固定 32 位指令让解码器更简单,去掉大部分条件执行减少了分支预测的压力。这些都是为高性能铺路。

1.2 寄存器模型:31 个通用寄存器,个个有讲究

AArch64 的寄存器模型,我建议你把它记在脑子里。调优的时候,寄存器分配直接影响性能。

先看通用寄存器:

  • X0-X7:参数传递寄存器。函数调用时,前 8 个参数放这里。返回值也走 X0。
  • X8:间接结果寄存器。比如函数返回结构体时,X8 指向返回地址。
  • X9-X15:临时寄存器。调用者保存,子程序可以随便用。
  • X16-X17:IP0/IP1,过程内调用临时寄存器。常用于 PLT 跳转。
  • X18:平台寄存器。在 Linux 内核中用作 per-CPU 指针,应用层别碰。
  • X19-X28:被调用者保存寄存器。子程序必须恢复原值。
  • X29:帧指针(FP)。
  • X30:链接寄存器(LR)。保存函数返回地址。

还有几个特殊寄存器:

  • SP:栈指针。注意,AArch64 有多个 SP,对应不同异常等级。
  • PC:程序计数器。不能直接写,只能通过分支指令修改。
  • XZR:零寄存器。读它返回 0,写它被忽略。

我的经验:做性能调优时,优先用 X0-X15 做临时变量,避免压栈。X19-X28 虽然安全,但保存恢复有开销。我曾经优化过一个加密算法,把 6 个循环变量从 X19-X24 挪到 X9-X14,性能提升了 12%。寄存器压力大的时候,这种细节很关键。

除了通用寄存器,还有系统寄存器。这些寄存器用 MSR/MRS 指令访问,控制着 MMU、异常、调试、性能监控等核心功能。比如:

// 读取当前异常等级
MRS X0, CurrentEL

// 读取帧指针
MOV X29, SP

// 写系统控制寄存器
MSR SCTLR_EL1, X0

系统寄存器名字的格式是 XXX_ELn,其中 n 表示可访问的最低异常等级。比如 SCTLR_EL1 只能在 EL1 及以上访问,EL0 碰不了。这个设计是为了安全隔离。

1.3 异常等级:CPU 的四个特权层级

ARM64 定义了 4 个异常等级(Exception Level),从 EL0 到 EL3。数字越大,特权越高。

我画了一张图,帮你理解这四个层级的关系:

ARM64 异常等级模型 EL0 — 用户态(Application) 运行普通应用程序,权限最低,不能直接访问硬件 EL1 — 内核态(OS Kernel) 运行操作系统内核,管理 MMU、中断、进程调度 EL2 — 虚拟机监视器(Hypervisor) 管理多个虚拟机,提供虚拟化支持 EL3 — 安全监视器(Secure Monitor) 最高权限,负责安全世界与普通世界切换 特权 递增 ↑

这张图很直观。EL0 在最底层,跑你的 App;EL1 是内核;EL2 是虚拟机监视器(比如 KVM);EL3 是安全监视器,负责 TrustZone 的切换。

异常等级切换通过异常(Exception)和返回(ERET)实现。比如用户程序执行 SVC 指令触发系统调用,CPU 从 EL0 切换到 EL1。内核处理完后再用 ERET 返回 EL0。

注意:异常等级只能升高,不能直接降低。降低必须通过 ERET 指令。我曾经见过一个新手写的内核模块,试图用 MSR 直接写 CurrentEL 来降级,结果直接触发 Undefined Instruction 异常。记住,ERET 是唯一的降级途径。

1.4 安全模型:TrustZone 与安全世界

ARM64 的安全模型基于 TrustZone 技术。它将物理硬件划分为两个世界:安全世界(Secure World)和普通世界(Normal World)。

每个世界都有自己的异常等级。也就是说,安全世界有 Secure EL0/EL1/EL3,普通世界有 Non-Secure EL0/EL1/EL2。两个世界通过 Monitor 模式(EL3)切换。

我整理了一个对比表:

特性 普通世界 安全世界
运行内容 Android/Linux 等通用 OS 可信 OS(如 OP-TEE)、安全应用
内存访问 只能访问非安全内存 可访问安全和非安全内存
外设访问 受限制 可访问安全外设(如指纹传感器)
调试 非安全调试 安全调试需认证
切换方式 SMC 指令进入 Monitor Monitor 返回普通世界

为什么需要 TrustZone?说白了,就是为了隔离敏感数据。比如手机上的指纹信息、支付密钥,这些不能放在普通世界,否则恶意 App 可能通过内核漏洞窃取。安全世界相当于一个独立的“安全岛”,有自己独立的内存、中断和外设。

我记得在做一个 TEE(可信执行环境)驱动时,踩过一个坑:安全世界的中断配置必须在 EL3 完成,普通世界的 GIC 驱动不能碰安全中断。当时调试了三天,最后发现是中断路由配错了,安全中断被路由到了普通世界。嗯,这种问题很难查,因为普通世界根本看不到安全中断的状态。

避坑指南:如果你在开发安全相关的驱动,记住三点:1)安全世界和非安全世界共享内存时,必须使用 Buffer 共享协议,不能直接访问对方地址;2)SMC 调用是同步的,调用期间普通世界会被阻塞;3)安全世界的栈空间通常很小(几 KB),别搞递归。

1.5 小结:这些概念怎么用?

好了,这一章的内容就这些。你可能觉得都是理论,跟写代码没关系。其实不然——

  • 理解寄存器模型,你才能写出高效的汇编代码,合理分配寄存器,减少栈操作。
  • 理解异常等级,你才能正确配置系统寄存器,知道哪些操作只能在 EL1 做。
  • 理解安全模型,你才能设计安全的系统架构,把敏感操作放到安全世界。