汇编基础(ARM64):ARMv8寄存器、A64指令集、栈与调用约定
好,我们开始第三讲。如果你之前只接触过x86,第一次看ARM64的汇编可能会有点懵。别急,我当年从x86切到ARM64时也花了点时间适应。说白了,这两套架构虽然都是干活的,但设计哲学差别挺大。
这一章我们聚焦四个核心:寄存器怎么用、指令怎么写、栈怎么玩、函数怎么调。搞懂这些,你就能看懂大部分ARM64的汇编代码了。
ARMv8寄存器体系
ARMv8有31个通用寄存器,从X0到X30,每个都是64位。另外还有个特殊的XZR(零寄存器)和SP(栈指针)。
我个人习惯把寄存器分成几类来记:
| 类别 | 寄存器 | 用途 |
|---|---|---|
| 参数寄存器 | X0-X7 | 传参,X0也存返回值 |
| 临时寄存器 | X9-X15 | 调用者保存,随便用 |
| 被调用者保存 | X19-X29 | 被调函数必须恢复原值 |
| 特殊寄存器 | X16-X18 | 平台相关,别乱动 |
| 帧指针 | X29 | FP,指向栈帧底部 |
| 链接寄存器 | X30 | LR,存返回地址 |
| 栈指针 | SP | 指向栈顶 |
| 零寄存器 | XZR | 读出来永远是0 |
还有个细节:ARM64支持32位和64位两种操作模式。用W0-W30访问低32位,用X0-X30访问全64位。比如:
MOV X0, #0x1000 // 64位操作
MOV W0, #0x1000 // 32位操作,高32位清零
A64指令集速览
A64是ARMv8的64位指令集,固定4字节长度。这点和x86的变长指令完全不同。你想想看,每条指令都是4字节,反汇编时对齐就简单多了。
常用的指令类别:
- 数据传输:LDR(加载)、STR(存储)、MOV(移动)
- 算术运算:ADD、SUB、MUL、UDIV/SDIV
- 逻辑运算:AND、ORR、EOR、BIC
- 移位操作:LSL、LSR、ASR、ROR
- 分支指令:B(无条件跳转)、BL(带链接跳转)、RET(返回)
- 比较指令:CMP、CMN、TST
举个例子,一个简单的加法函数:
// int add(int a, int b) { return a + b; }
add:
ADD W0, W0, W1 // W0 = W0 + W1
RET // 返回,结果在W0中
注意这里没有x86那种push/pop。ARM64的栈操作全靠LDR/STR显式完成。我刚开始写ARM64的exploit时,总想找push指令——嗯,没有。
栈与调用约定
ARM64的栈是向下增长的,SP必须16字节对齐。函数调用时,栈帧布局大概是这样的:
高地址
+------------------+
| 调用者的栈帧 |
+------------------+
| 返回地址 (LR) | ← X29 (FP) 指向这里
| 保存的FP |
| 局部变量 |
| 被调用者保存的寄存器 |
| 参数溢出区 |
+------------------+ ← SP 指向这里
低地址
调用约定(AAPCS64)的几个核心规则:
- 参数1-8放X0-X7,多余的压栈
- 返回值放X0(或X0+X1返回128位)
- X19-X29被调用者保存,用之前要push
- X9-X15调用者保存,被调函数可以随便改
- SP必须16字节对齐
一个典型的函数序言和尾声:
func:
// 序言
STP X29, X30, [SP, #-16]! // 保存FP和LR,SP减16
MOV X29, SP // 设置FP
// 函数体
// ... 干活 ...
// 尾声
LDP X29, X30, [SP], #16 // 恢复FP和LR,SP加16
RET // 返回
STP/LDP是成对存储/加载指令,一次操作两个寄存器。这个设计很巧妙,既减少了指令数,又保持了栈对齐。
BR X30,不是x86的pop rip。
与x86的对比
我把几个关键差异整理成表格,方便你对照:
| 特性 | x86-64 | ARM64 |
|---|---|---|
| 指令长度 | 变长(1-15字节) | 固定4字节 |
| 通用寄存器 | 16个(RAX-R15) | 31个(X0-X30) |
| 返回地址 | 栈顶(call压栈) | X30(LR寄存器) |
| 栈操作 | push/pop指令 | LDR/STR显式操作 |
| 条件码 | RFLAGS寄存器 | NZCV条件标志 |
| 寻址模式 | 复杂(ModRM+SIB) | 简洁(基址+偏移) |
| 参数传递 | RCX/RDX/R8/R9 + 栈 | X0-X7 + 栈 |
我个人觉得,ARM64的设计更规整。固定指令长度让反汇编和ROP gadget搜索都简单不少。但x86的变长指令也有优势——指令密度高,同样的代码占更少内存。
举个例子,同样的函数调用:
// x86-64
call func // 5字节,返回地址压栈
add rax, rbx // 3字节
// ARM64
BL func // 4字节,返回地址存X30
ADD X0, X0, X1 // 4字节
你看,ARM64每条指令都是4字节,对齐整齐。x86则长短不一,反汇编时容易错位——这也是为什么x86的混淆更难搞。
知识结构图
下面这张图把ARM64汇编的核心知识点串起来了:
这张图把四个核心模块的关系画清楚了。你从寄存器体系出发,理解指令集怎么操作这些寄存器,再看栈和调用约定怎么组织函数调用,最后对比x86加深理解。每个模块下面都有具体的知识点,方便你查漏补缺。