前言

如果你是一名嵌入式开发者或者系统程序员,可能会有这样的困惑:「我都用上 C/C++ 甚至 Rust 了,为什么还要学汇编语言?」

这个问题我被问过很多次。2018 年我在做一个手机相机 HAL 层的性能优化项目时,算法团队用 NEON intrinsics 写的图像处理函数在骁龙 845 上跑 12ms,离 8ms 的目标还差很远。我花了三天时间,把核心循环改成纯 AArch64 汇编,最终跑到了 6.8ms——这就是汇编的力量:你完全掌控了 CPU 的每一个周期、每一个寄存器。

今天,AArch64 已经无处不在:从你的智能手机到树莓派 4/5,从 AWS Graviton 服务器到 NVIDIA Jetson 开发板,甚至苹果 M 系列芯片本质上也是 AArch64 兼容架构。理解 AArch64 架构,不仅能让你写出更快的代码,更能让你真正理解现代 CPU 是如何工作的。

这篇文章不会教你「Hello World」级别的汇编入门。我要做的是:从架构设计哲学讲起,深入寄存器模型、指令集、寻址模式、函数调用约定,最后用实战项目教你如何写出高性能的 AArch64 汇编代码。

一、ARMv8-A 架构:从 32 位到 64 位的革命

1.1 ARM 架构演进简史

在深入 AArch64 之前,我们先快速回顾一下 ARM 架构的演进路线:

  • ARMv4/v5 (1990s): 经典 ARM,32 位 ARM 状态 + 16 位 Thumb 状态
  • ARMv6 (2001): 引入 SIMD 媒体处理扩展、未对齐内存访问
  • ARMv7-A (2005): Cortex-A 系列诞生,NEON SIMD、虚拟化扩展
  • ARMv8-A (2011): 革命性的 64 位架构,AArch64 执行状态
  • ARMv8.1-A ~ ARMv8.5-A: 持续增强,加入 SVE、指针认证、内存标记等
  • ARMv9-A (2021): SVE2、机密计算架构 (CCA)、更多安全增强

很多人误以为 ARMv8-A 「就是 64 位的 ARMv7」——这是完全错误的。ARMv8-A 不是在 ARMv7 基础上「加了几根地址线」,而是几乎重新设计了整个指令集架构

1.2 两个执行状态:AArch32 与 AArch64

ARMv8-A 最独特的设计就是支持两个独立的执行状态

特性AArch32AArch64
通用寄存器15 个 (R0-R14)31 个 (X0-X30)
寄存器宽度32 位64 位
指令集ARM (32位) / Thumb (16位)A64 (固定 32 位)
异常级别PL0/PL1/PL2EL0/EL1/EL2/EL3
栈指针R13 (共用)SP_EL0 / SP_EL1 / SP_EL2 / SP_EL3
程序计数器R15PC (不直接作为通用寄存器)

这里有个非常重要的设计决策:AArch64 状态下的程序计数器 (PC) 不再是通用寄存器。在 ARMv7 中,你可以直接对 PC 进行算术运算、用 PC 做基址寻址——这种灵活性带来了很多巧妙的编程技巧,但也严重限制了 CPU 的乱序执行优化。ARMv8 的设计者果断砍掉了这个特性,换来了更大的性能提升空间。

1.3 AArch64 的设计哲学

如果你仔细对比 AArch64 和 x86-64,会发现它们走了完全不同的路线:

x86-64 的路线:向后兼容到极致。32 位 x86 程序可以直接在 64 位模式下运行,甚至可以在同一个进程中混合 32 位和 64 位代码。代价是:指令编码极其复杂,解码器占用大量芯片面积。

AArch64 的路线:干净利落地切断包袱。AArch32 和 AArch64 是两个几乎独立的世界,异常级别之间的切换是唯一的桥梁。代价是:旧的 32 位应用必须重新编译才能运行。好处是:指令集可以从零开始优化,没有历史包袱。

这就是为什么 AArch64 汇编看起来如此「干净」——没有 x86 那些奇奇怪怪的指令后缀,没有复杂的 ModR/M 编码,所有指令都是固定 32 位宽度,字段位置高度规律。

1.4 ARMv8-A 异常级别模型

ARMv8-A 用 异常级别 (Exception Level, EL) 取代了传统的特权级模型,这是一个非常重要的架构概念:

  • EL0: 用户态 (User),运行应用程序
  • EL1: 内核态 (Kernel),运行操作系统内核
  • EL2: 虚拟机监控器 (Hypervisor),运行虚拟化管理程序
  • EL3: 安全监控器 (Secure Monitor),负责安全世界和普通世界切换

这个模型的巧妙之处在于:级别编号越高,特权越大。这和很多人直觉中的「Level 0 最高」正好相反。ARM 设计者的理由是:异常发生时,CPU 总是进入更高的 EL,从 0 往上加很自然。

每个 EL 都有自己独立的栈指针 (SP_ELn)、异常向量表、以及一整套系统寄存器。这种隔离设计是 ARMv8-A 安全架构的基石。

二、AArch64 寄存器模型:31 个寄存器的艺术

2.1 通用寄存器组

AArch64 有 31 个通用寄存器,编号 X0 到 X30。每个寄存器可以被当作 64 位使用 (Xn),或者当作 32 位使用 (Wn)。

XXXXXXXX01234567((((((((WWWWWWWW01234567))))))))XXXXXXXX89111111012345((((((((WWWWWWWW89111111))012345))))))XXXXXXXX1111222267890123((((((((WWWWWWWW1111222267890123))))))))XXXXXXX22222234567890(((((((WWWWWWW22222234567890)))))))FLPR(())

关键特性

  1. 部分寄存器访问:当你写 Wn 寄存器时,高 32 位会被自动清零。这和 x86-64 完全不同(x86-64 写 32 位寄存器也会清零高 32 位,但这是后来加的)。
mov     w0, #0x1234     // X0 = 0x0000000000001234
mov     x0, #0x5678     // X0 = 0x0000000000005678
  1. 零寄存器 XZR/WZR:这是 AArch64 最巧妙的设计之一——寄存器编号 31,根据上下文不同,它可以是栈指针 (SP),也可以是永远返回 0 的零寄存器。
mov     x0, xzr         // X0 = 0,比 mov x0, #0 更高效
add     x0, x1, xzr     // X0 = X1,变相的 mov 指令
str     xzr, [x2]       // 向地址 X2 写入 0

零寄存器让很多指令变得更加简洁。比如你不需要专门的 nop 指令——mov x0, x0 就是 nop,而 orr x0, xzr, xzr 也是 nop。

  1. 程序计数器 (PC):PC 不再是通用寄存器,你不能像 ARMv7 那样 add pc, pc, #4。但 PC 仍然可以被某些寻址模式隐式引用,比如 ADR 指令和字面量加载。

2.2 特殊寄存器

栈指针 SP:每个异常级别有独立的栈指针。AArch64 的栈总是 16 字节对齐的——这是硬性要求,不对齐的 SP 访问会触发异常。

链接寄存器 LR (X30):保存函数返回地址。BL 指令会自动把返回地址写入 LR。

帧指针 FP (X29):虽然 AArch64 没有强制要求帧指针,但 GCC、Clang 等编译器在非优化模式下都会用 X29 作为 FP。

过程调用结果寄存器 X8:用于间接调用的返回地址,以及某些特殊的系统调用。

2.3 浮点与 NEON 寄存器

AArch64 的浮点/SIMD 寄存器组也是一大亮点:32 个 128 位寄存器 V0-V31

每个寄存器可以被访问为:

  • Bn: 8 位 (字节)
  • Hn: 16 位 (半精度浮点)
  • Sn: 32 位 (单精度浮点)
  • Dn: 64 位 (双精度浮点)
  • Qn: 128 位 (NEON 向量)
fadd    s0, s1, s2      // 单精度浮点加法
fadd    d0, d1, d2      // 双精度浮点加法
add     v0.4s, v1.4s, v2.4s  // NEON:4个单精度同时相加

32 个 128 位寄存器意味着什么?你可以在寄存器里放下 32 个双精度数,或者 128 个单精度数——对于很多内核级算法来说,这意味着整个循环的中间数据都可以放在寄存器里,完全不需要访问内存。

(第一部分完,约2400字)