第二章 ARM64寄存器体系:从通用寄存器到NEON

寄存器,说白了就是CPU内部最快的那一小块存储。你写汇编也好,做逆向也罢,大部分时间都在跟寄存器打交道。我刚开始接触ARM64时,第一反应是——寄存器怎么这么多?但用久了你会发现,每个寄存器都有它存在的道理。

这一章,我们就把ARM64的寄存器体系捋一遍。通用寄存器、特殊寄存器、系统寄存器、浮点与NEON寄存器,一个一个来。

2.1 通用寄存器:X0~X30

ARM64有31个通用寄存器,编号从X0到X30。每个都是64位宽。你可以把它们看成CPU的31个临时工作台。

寄存器 别名/用途 调用约定
X0~X7 参数寄存器 传递函数参数,X0也用于返回值
X8 间接结果寄存器 返回大结构体时使用
X9~X15 临时寄存器 调用者保存,函数内随便用
X16~X17 IP0、IP1 过程内调用临时寄存器,PLT跳板常用
X18 平台寄存器 Windows用,Linux一般不动它
X19~X28 被调用者保存 函数必须恢复原值
X29 帧指针FP 指向当前栈帧底部
X30 链接寄存器LR 保存函数返回地址

这里有个细节——X0到X30是64位视图,但你也可以用W0到W30访问它们的低32位。比如:

MOV X0, #0x123456789ABCDEF   ; 64位操作
MOV W1, #0xDEADBEEF          ; 32位操作,高32位自动清零

我在逆向一个Android native库时,遇到过一段代码反复用W寄存器做循环计数。当时我盯着X寄存器看半天没看懂,后来才意识到——人家用的是32位视图。嗯,这个坑我踩过。

2.2 特殊寄存器:XZR、SP、PC、ELR、SPSR

特殊寄存器不多,但个个都是狠角色。

2.2.1 XZR:零寄存器

XZR是个只读寄存器,永远返回0。你写进去的任何值都会被忽略。有什么用?

ADD X0, X1, XZR   ; 等价于 MOV X0, X1
CMP X0, XZR       ; 比较X0是否为0

我个人习惯用XZR做清零操作,比用MOV快一丁点,而且代码更紧凑。

2.2.2 SP:栈指针

SP指向当前栈顶。ARM64里SP必须16字节对齐,否则访问会触发异常。我调试过一个crash,就是SP没对齐导致的——函数入口处压栈时SP是奇数倍16字节,一压就崩。

注意:ARM64没有PUSH/POP指令。压栈出栈用STP/LDP:
STP X29, X30, [SP, #-16]!   ; 压栈
LDP X29, X30, [SP], #16     ; 出栈
感叹号表示先减地址再操作,这叫前索引寻址。

2.2.3 PC:程序计数器

PC指向当前正在执行的指令地址。ARM64里你不能直接写PC,但可以通过分支指令间接修改它。有意思的是,PC可以参与地址计算:

ADR X0, label    ; 将label的PC相对地址加载到X0
LDR X1, [PC, #8] ; 从PC+8处加载数据

逆向时,我经常靠PC相对寻址来定位全局变量和字符串。你想想看,如果代码是PIE的,只有PC相对地址才是可靠的。

2.2.4 ELR与SPSR:异常现场保存

ELR(Exception Link Register)保存异常返回地址。SPSR(Saved Program Status Register)保存异常发生时的PSTATE。这两个寄存器在异常处理中成对出现:

// 异常发生时,硬件自动完成:
ELR_EL1 = PC + 4    // 返回地址
SPSR_EL1 = PSTATE   // 当前状态

// 异常返回时:
ERET                // PC = ELR_EL1, PSTATE = SPSR_EL1

我做过一个内核驱动的故障定位,系统随机crash,查了半天发现是SPSR里的DAIF掩码被意外修改了。中断被屏蔽,系统直接挂死。这种问题,光看代码很难发现。

2.3 系统寄存器

系统寄存器是ARM64的配置中心。它们控制MMU、Cache、异常等级、调试功能等。访问系统寄存器只能用MSR和MRS指令:

MRS X0, SCTLR_EL1   ; 读取系统控制寄存器
MSR SCTLR_EL1, X0   ; 写入系统控制寄存器

常用的系统寄存器包括:

寄存器 全称 用途
SCTLR_EL1 System Control Register MMU开关、对齐检查、Cache控制
TCR_EL1 Translation Control Register 页表配置、地址空间大小
TTBR0_EL1/TTBR1_EL1 Translation Table Base Register 页表基地址
VBAR_EL1 Vector Base Address Register 异常向量表基地址
ID_AA64PFR0_EL1 Processor Feature Register CPU特性查询
调试技巧:在GDB里查看系统寄存器:
(gdb) p/x $SCTLR_EL1
(gdb) p/x $VBAR_EL1
如果GDB不认识,可以用:
(gdb) msr SCTLR_EL1

系统寄存器是异常等级敏感的。EL0(用户态)不能访问大部分系统寄存器,访问会触发异常。逆向时如果看到MSR/MRS指令,基本可以断定这段代码运行在EL1或更高等级。

2.4 NEON与浮点寄存器

ARM64的浮点和SIMD共用一组寄存器:V0~V31。每个寄存器128位宽,但可以按不同视图访问:

视图 宽度 用途
B0~B31 8位 字节操作
H0~H31 16位 半字/半精度浮点
S0~S31 32位 单精度浮点
D0~D31 64位 双精度浮点
Q0~Q31 128位 四字/NEON向量

举个例子,一个NEON向量加法:

LD1 {V0.4S}, [X0]   ; 从X0加载4个32位浮点到V0
LD1 {V1.4S}, [X1]   ; 从X1加载4个32位浮点到V1
FADD V2.4S, V0.4S, V1.4S  ; 向量加法
ST1 {V2.4S}, [X2]   ; 存回内存

我在逆向一个图像处理算法时,看到满屏的NEON指令,一开始头都大了。后来发现,只要把V寄存器拆成S/D视图,逐元素分析,反而比普通指令更容易理解——因为NEON是数据并行的,一个指令干四个活。

逆向要点:看到FADD、FMUL、FMLA这些指令,基本可以确定代码在做浮点运算。看到LD1/ST1、UZP、TRN这些,就是NEON向量化代码。如果你在逆向一个音视频编解码库,NEON指令会占很大比例。

2.5 寄存器体系总览

下面这张图把ARM64的寄存器体系串起来了。我建议你把它存下来,调试时对照着看。

ARM64 寄存器体系总览 通用寄存器 (X0~X30) X0~X7 : 参数/返回值 X8 : 间接结果 X9~X15: 临时寄存器 (调用者保存) X16~X17: IP0/IP1 (PLT跳板) X18 : 平台寄存器 X19~X28: 被调用者保存 X29(FP), X30(LR) 特殊寄存器 XZR : 零寄存器 (只读) SP : 栈指针 (16字节对齐) PC : 程序计数器 ELR : 异常返回地址 SPSR: 保存的程序状态 系统寄存器 (MSR/MRS访问) SCTLR, TCR, TTBR0/1 VBAR, ID_AA64PFR0, 等 NEON & 浮点寄存器 (V0~V31) B/H/S/D/Q 多视图访问 SIMD向量 + 标量浮点 说明 W0~W30 是 X0~X30 的 32 位视图,写 W 寄存器会自动清零高 32 位 系统寄存器访问权限由异常等级 (EL0~EL3) 控制

这张图把ARM64的寄存器分成了四大块。通用寄存器是日常主力,特殊寄存器管关键控制,系统寄存器配整个芯片,NEON寄存器管数据并行。调试时遇到不认识的寄存器,先看它属于哪一类,再查具体功能。

我的习惯:逆向时在GDB里用 info registers 看通用寄存器,用 info all-registers 看所有寄存器(包括NEON)。如果怀疑代码用了浮点,直接看S0~S31的值,往往能发现线索。