1. 课程导入

说实话,我刚入行那会儿,ARM 还是个「低功耗、性能一般」的代名词。谁能想到十几年后,它从手机杀进了服务器,甚至 Mac 都换上了自研 ARM 芯片。这背后,ARMv8 架构的推出是关键转折点。

这一章,咱们先把地基打牢。ARM64 到底怎么来的?AArch64 和 AArch32 有什么区别?异常等级又是什么鬼?搞懂这些,后面反汇编调试才不会一头雾水。

1.1 ARM64 发展史:一段逆袭之路

ARM 的故事得从 1985 年说起。当时 Acorn 公司想做一款简单高效的处理器,于是 ARM1 诞生了。但真正让 ARM 起飞的是 1990 年代——低功耗、高性价比,完美契合嵌入式市场。

我印象最深的是 2011 年,ARM 发布了 ARMv8 架构。这是 ARM 历史上第一次引入 64 位指令集。为什么这么晚?因为 32 位够用啊!但移动设备的内存越来越大,4GB 上限卡脖子了。服务器市场更是需要大地址空间。

ARMv8 不是简单的 32 位升级版,而是重新设计了 64 位执行状态——AArch64。同时保留了 32 位兼容模式,让老代码能平滑过渡。这个设计思路,我个人觉得非常务实。

关键里程碑:

  • 1985 — ARM1 诞生,26 位地址总线
  • 1990s — ARM7/ARM9 系列,嵌入式霸主
  • 2005 — ARMv7-A,Cortex-A 系列,智能手机爆发
  • 2011 — ARMv8-A,首次引入 64 位(AArch64)
  • 2017 — ARMv8.2-A,加入更多安全与虚拟化特性
  • 2021 — ARMv9,进一步强化安全与 AI

现在你看到的 Apple M 系列芯片、华为鲲鹏、AWS Graviton,都是 ARMv8 及后续架构的产物。ARM64 不再是「低功耗」的代名词,它已经能跟 x86 正面硬刚了。

1.2 AArch64 vs AArch32:到底差在哪?

ARMv8 架构最核心的设计就是两种执行状态:AArch64(64 位)和 AArch32(32 位)。它们不是简单的「寄存器变宽」,而是完全不同的指令集和编程模型。

我刚开始做逆向时,经常搞混这两个状态。有一次调试一个混合架构的固件,32 位和 64 位代码来回切换,差点把我整崩溃。后来我总结了一张对比表,每次遇到都拿出来对照:

特性 AArch64 AArch32
寄存器宽度 31 个 64 位通用寄存器(X0-X30) 15 个 32 位通用寄存器(R0-R14)
指令长度 固定 4 字节 4 字节(Thumb 为 2 字节)
寻址空间 最大 2^64 字节 最大 2^32 字节(4GB)
条件执行 大部分指令不支持 大部分指令支持条件执行
异常模型 4 个异常等级(EL0-EL3) 7 种处理器模式
SIMD/FP 32 个 128 位寄存器(V0-V31) 32 个 64 位寄存器(D0-D31)
兼容性 不兼容 AArch32 兼容 ARMv7 及之前

你看,AArch64 的寄存器数量翻了一倍,而且每个都是 64 位宽。这意味着函数传参可以多用寄存器,少压栈。性能自然就上去了。

避坑指南: 我曾经在逆向一个 iOS 应用时,发现函数开头有 STP X29, X30, [SP, #-16]! 这样的指令。这是 AArch64 典型的栈帧建立方式——用 X29(FP)和 X30(LR)保存返回地址。如果你用 AArch32 的思路去理解,会完全对不上。

1.3 ARMv8-A 架构概述:核心组件一览

ARMv8-A 不是单打独斗,它是一整套体系。我习惯把它拆成几个核心模块来看:

  • 处理单元(PE) — 就是 CPU 核心,负责执行指令
  • 内存管理单元(MMU) — 负责虚拟地址到物理地址的转换
  • 中断控制器(GIC) — 管理各种中断的优先级和分发
  • 调试与跟踪单元 — 包括硬件断点、观察点、ETM 等
  • 安全扩展(TrustZone) — 将系统分为安全世界和普通世界

这里面,MMU 和异常等级是逆向分析最常打交道的部分。你想想看,一个程序崩溃了,你看到的地址是虚拟地址还是物理地址?肯定是虚拟地址。所以理解 MMU 的页表结构,才能定位到真正的内存问题。

下面这张图是我自己整理的 ARMv8-A 核心组件关系,帮你快速建立整体认知:

ARMv8-A 核心组件架构图 处理单元 (PE) 指令执行 / 流水线 MMU 虚拟地址 → 物理地址 GIC 中断管理 / 优先级 调试与跟踪 断点 / 观察点 / ETM TrustZone 安全世界 / 普通世界 异常等级 EL0 / EL1 / EL2 / EL3 说明 • PE 是执行核心,MMU 负责地址转换 • GIC 管理中断,调试单元辅助故障定位 • TrustZone 提供硬件级安全隔离 • 异常等级控制特权级别,EL3 最高

1.4 异常等级与运行模式:谁在管谁?

ARMv8 的异常模型是我觉得最巧妙的设计之一。它用 4 个异常等级(Exception Level)来划分特权级别:

  • EL0 — 用户态,权限最低。应用程序跑在这里
  • EL1 — 操作系统内核。Linux、Windows 的 kernel 都在这里
  • EL2 — 虚拟机监视器(Hypervisor)。跑虚拟化场景
  • EL3 — 安全监视器(Secure Monitor)。TrustZone 的核心

数字越大,权限越高。EL3 可以访问所有资源,EL0 只能碰自己的那一亩三分地。

为什么会这样设计?说白了,就是为了隔离。你想想看,一个 App 崩溃了,不能把整个系统带崩吧?异常等级就是干这个的——低等级的错误,高等级可以拦截并处理。

注意: 我在调试一个 Android 内核崩溃时,发现异常等级搞错了。当时以为是在 EL1 跑的代码,结果实际是 EL2 的 Hypervisor 在接管。浪费了我整整两天。所以,拿到一个 crash dump,第一件事就是看 CurrentEL 寄存器的值,确认当前异常等级。

异常等级之间通过「异常」来切换。比如用户程序(EL0)触发系统调用,就会陷入 EL1 的内核。这个过程叫「异常陷入」。反过来,内核处理完返回用户态,叫「异常返回」。

每个异常等级都有自己的栈指针(SP_ELx)、保存状态寄存器(SPSR_ELx)和返回地址寄存器(ELR_ELx)。这些在逆向分析时特别重要——你看到 ERET 指令,就知道是在做异常返回。

下面这个表格总结了各异常等级的典型用途:

异常等级 典型用途 权限 常见软件
EL0 用户应用程序 最低 App、用户态驱动
EL1 操作系统内核 中 Linux Kernel、Windows Kernel
EL2 虚拟机监视器 高 KVM、Xen、Hyper-V
EL3 安全监视器 最高 TrustZone Firmware、ATF

嗯,这里要注意:EL0 和 EL1 是必须实现的,EL2 和 EL3 是可选的。很多嵌入式设备没有虚拟化需求,就不实现 EL2。但安全场景一般都会带 EL3。

我个人习惯在逆向一个固件时,先通过异常向量表的位置判断它跑在哪个异常等级。向量表在 EL3 和 EL1 的地址不同,这个技巧在实战中非常有用。

实战技巧: 用 GDB 调试 ARM64 程序时,可以执行 info registers 查看当前异常等级。重点关注 CurrentEL 和 SPSR_EL1(或对应等级)。如果 CurrentEL 是 0b01,表示当前在 EL1。

好了,ARM64 架构的基础就聊到这儿。记住:AArch64 是 64 位执行状态,异常等级是权限划分的核心,MMU 和 TrustZone 是安全与隔离的基石。这些概念会贯穿整个课程,后面每一章都会用到。