2. ARM64体系结构基础
寄存器,说白了就是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字节,一压就崩。
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) 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是数据并行的,一个指令干四个活。
2.5 寄存器体系总览
下面这张图把ARM64的寄存器体系串起来了。我建议你把它存下来,调试时对照着看。
这张图把ARM64的寄存器分成了四大块。通用寄存器是日常主力,特殊寄存器管关键控制,系统寄存器配整个芯片,NEON寄存器管数据并行。调试时遇到不认识的寄存器,先看它属于哪一类,再查具体功能。