汇编基础(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
小技巧:X0-X7传参,超过8个参数就用栈。X30存返回地址,这点和x86的call/pop完全不同。我第一次写ARM64的shellcode时,忘了保存LR,结果ret直接飞了——嗯,血的教训。

还有个细节: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的LDR/STR支持多种寻址模式,比如前变址、后变址、偏移量。这在操作栈帧时非常灵活。

栈与调用约定

ARM64的栈是向下增长的,SP必须16字节对齐。函数调用时,栈帧布局大概是这样的:

高地址
+------------------+
| 调用者的栈帧      |
+------------------+
| 返回地址 (LR)     |  ← X29 (FP) 指向这里
| 保存的FP          |
| 局部变量          |
| 被调用者保存的寄存器 |
| 参数溢出区        |
+------------------+  ← SP 指向这里
低地址

调用约定(AAPCS64)的几个核心规则:

  1. 参数1-8放X0-X7,多余的压栈
  2. 返回值放X0(或X0+X1返回128位)
  3. X19-X29被调用者保存,用之前要push
  4. X9-X15调用者保存,被调函数可以随便改
  5. 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是成对存储/加载指令,一次操作两个寄存器。这个设计很巧妙,既减少了指令数,又保持了栈对齐。

避坑指南:我曾经在写ROP链时,忘了ARM64的RET指令是从X30取地址,而不是从栈顶弹。结果gadget链死活跑不起来。记住:ARM64的ret相当于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汇编的核心知识点串起来了:

ARM64汇编知识体系 ARM64汇编核心知识体系 寄存器体系 A64指令集 栈与调用约定 x86对比 X0-X30通用寄存器 SP/XZR特殊寄存器 W0-W30 32位视图 LDR/STR 数据传输 ADD/SUB 算术运算 B/BL/RET 分支 STP/LDP 成对操作 X0-X7 参数传递 X29/X30 帧指针+LR 固定 vs 变长指令 寄存器数量差异 返回地址处理方式 核心:理解ARM64的"寄存器传递返回地址"和"显式栈操作"设计哲学 漏洞利用(ROP/ret2libc) 逆向工程与Firmware分析

这张图把四个核心模块的关系画清楚了。你从寄存器体系出发,理解指令集怎么操作这些寄存器,再看栈和调用约定怎么组织函数调用,最后对比x86加深理解。每个模块下面都有具体的知识点,方便你查漏补缺。

实践建议:找个ARM64的二进制文件(比如手机上的.so),用objdump反汇编看看。重点关注函数序言和尾声的STP/LDP模式,以及BL/RET的配对使用。看多了自然就熟了。