第03课:ARM汇编基础

📍 【基础概念 1-5】 | 逆向工程系列课程

🎯 ARM架构的重要性

ARM架构是移动设备和嵌入式系统的绝对主流——全球超过95%的智能手机运行ARM处理器。从Android逆向到iOS越狱分析,从IoT固件提取到汽车电子破解,ARM汇编都是不可或缺的技能。ARM处理器以低功耗、高性能著称,广泛应用于智能手机、平板电脑、物联网设备、汽车电子和服务器领域。

ARM架构家族

架构位数模式典型设备
ARMv7-A32位ARM/Thumb老旧Android、iPhone 4
ARMv8-A64位(AArch64)AArch64/AArch32现代手机、Apple Silicon
ARMv8-M32位Thumb-2微控制器、IoT设备
ARMv9-A64位AArch64最新旗舰手机、服务器

📋 AArch64寄存器体系

通用寄存器

┌──────────────────────────────────────────────────────┐
│  64位通用寄存器 X0-X30 (64位) / W0-W30 (低32位)      │
├──────────────────────────────────────────────────────┤
│  X0-X7    : 函数参数 / 返回值 (X0/W0)                 │
│  X8       : 间接结果位置 / 系统调用号                  │
│  X9-X15   : 临时寄存器(caller-saved)                  │
│  X16-X17  : IP0/IP1,链接器内部使用                   │
│  X18      : 平台寄存器(OS特定用途)                    │
│  X19-X28  : 被调用者保存寄存器(callee-saved)           │
│  X29/FP   : 帧指针(Frame Pointer)                    │
│  X30/LR   : 链接寄存器(Link Register/返回地址)        │
│  SP       : 栈指针(Stack Pointer)                    │
│  PC       : 程序计数器(Program Counter)               │
├──────────────────────────────────────────────────────┤
│  特殊寄存器                                           │
│  NZCV     : 条件标志(Negative/Zero/Carry/oVerflow)    │
│  FPCR     : 浮点控制寄存器                            │
│  FPSR     : 浮点状态寄存器                            │
└──────────────────────────────────────────────────────┘

AArch64调用约定(AAPCS64):前8个整数参数通过X0-X7传递,返回值在X0中。这与x86-64的6参数限制不同,ARM可以传递更多寄存器参数。

📝 ARM64指令集详解

数据传送与算术

# MOV——数据传送
MOV     X0, #42            # X0 = 42 (16位立即数)
MOV     X1, X0             # X1 = X0 (寄存器间传送)
MOVZ    X0, #0x1234, LSL#16 # X0 = 0x12340000 (零扩展移位)
MOVK    X0, #0x5678, LSL#32 # X0[32:47] = 0x5678 (保留其他位)

# ADD/SUB——加减法
ADD     X0, X1, X2         # X0 = X1 + X2
ADD     X0, X1, #10        # X0 = X1 + 10 (12位立即数)
SUB     X0, X1, #8         # X0 = X1 - 8
SUB     X0, X1, X2, LSL#2  # X0 = X1 - (X2 << 2) 移位加算术

# MUL/DIV——乘除法
MUL     X0, X1, X2         # X0 = X1 * X2 (低64位)
SMADDL  X0, W1, W2, X3     # X0 = X3 + (signed)W1*W2 乘加
SDIV    X0, X1, X2         # X0 = X1 / X2 (有符号除法)
UDIV    X0, X1, X2         # X0 = X1 / X2 (无符号除法)

# 位操作
AND     X0, X1, X2         # X0 = X1 & X2 (按位与)
ORR     X0, X1, X2         # X0 = X1 | X2 (按位或)
EOR     X0, X1, X2         # X0 = X1 ^ X2 (按位异或)
MVN     X0, X1             # X0 = ~X1 (按位取反)

访存指令(Load/Store)

# 基本加载/存储
LDR     X0, [X1]           # X0 = *X1 (加载64位)
STR     X0, [X1]           # *X1 = X0 (存储64位)
LDR     W0, [X1]           # W0 = *(uint32*)X1 (加载32位)
LDRB    W0, [X1]           # W0 = *(uint8*)X1 (加载8位)
LDRH    W0, [X1]           # W0 = *(uint16*)X1 (加载16位)

# 带偏移的加载/存储
LDR     X0, [X1, #16]      # X0 = *(X1+16) 常量偏移
LDR     X0, [X1, X2]       # X0 = *(X1+X2) 寄存器偏移
LDR     X0, [X1, X2, LSL#3] # X0 = *(X1 + X2*8) 数组访问

# 前索引/后索引(自动更新基地址)
LDR     X0, [X1, #8]!      # X1+=8; X0=*X1 (前索引,先更新后访问)
LDR     X0, [X1], #8       # X0=*X1; X1+=8 (后索引,先访问后更新)

# 成对操作(常用在函数序言/结语)
STP     X29, X30, [SP, #-16]!  # SP-=16; 存FP和LR (等效push)
LDP     X29, X30, [SP], #16    # 加载FP和LR; SP+=16 (等效pop)

控制流指令

# 条件比较与零比较
CMP     X0, #10            # 设置标志: X0 - 10
CBZ     X0, label           # if X0==0 then jump (零则跳转)
CBNZ    X0, label           # if X0!=0 then jump (非零则跳转)
TBZ     X0, #3, label       # if X0的第3位为0则跳转 (测试位)
TBNZ    X0, #3, label       # if X0的第3位为1则跳转

# 条件分支(基于NZCV标志)
B.EQ    label               # 相等则跳转 (Z==1)
B.NE    label               # 不等则跳转 (Z==0)
B.GT    label               # 大于则跳转 (signed)
B.LT    label               # 小于则跳转 (signed)
B.GE    label               # 大于等于 (signed)
B.HI    label               # 高于则跳转 (unsigned)
B.LS    label               # 低于等于 (unsigned)

# 函数调用与返回
BL      func                # X30=PC+4; PC=func (带链接的分支)
BLR     Xn                  # X30=PC+4; PC=Xn (寄存器间接调用)
RET                         # PC=X30 (返回,等价于BR X30)

# 条件选择(ARM独有!x86没有等效指令)
CSEL    X0, X1, X2, EQ      # if EQ then X0=X1 else X0=X2
CSINC   X0, X1, X2, NE      # if NE then X0=X1 else X0=X2+1
CSINV   X0, X1, X2, EQ      # if EQ then X0=X1 else X0=~X2

💻 实操:交叉编译与ARM反汇编

1. 安装ARM交叉编译工具链

# 安装AArch64和ARMv7交叉编译器
$ apt-get install -y gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf
✅ 验证通过:安装AArch64(64位)和ARMv7(32位)交叉编译器

$ aarch64-linux-gnu-gcc --version | head -1
aarch64-linux-gnu-gcc (Ubuntu 11.4.0) 11.4.0

$ arm-linux-gnueabihf-gcc --version | head -1
arm-linux-gnueabihf-gcc (Ubuntu 11.4.0) 11.4.0

2. 编译并分析ARM64程序

$ cat > /tmp/test_arm64.c <<'CEOF'
int add(int a, int b) { return a + b; }
int max(int a, int b) { return a > b ? a : b; }
int sum(int *arr, int n) {
    int s = 0;
    for (int i = 0; i < n; i++) s += arr[i];
    return s;
}
CEOF

$ aarch64-linux-gnu-gcc -O0 -o /tmp/test_arm64 /tmp/test_arm64.c
$ aarch64-linux-gnu-objdump -d /tmp/test_arm64 | grep -A15 '<add>'
0000000000400654 <add>:
  400654:   a9bf7bfd    stp x29, x30, [sp, #-16]!
  400658:   910003fd    mov x29, sp
  40065c:   b9000fe0    str w0, [sp, #12]
  400660:   b9000be1    str w1, [sp, #8]
  400664:   b9400fe1    ldr w1, [sp, #12]
  400668:   b9400be0    ldr w0, [sp, #8]
  40066c:   0b000020    add w0, w1, w0
  400670:   a8c17bfd    ldp x29, x30, [sp], #16
  400674:   d65f03c0    ret
✅ 验证通过:ARM64使用stp/ldp保存/恢复FP和LR寄存器

3. 分析优化版本

$ aarch64-linux-gnu-gcc -O2 -o /tmp/test_arm64_o2 /tmp/test_arm64.c
$ aarch64-linux-gnu-objdump -d /tmp/test_arm64_o2 | grep -A5 '<add>'
0000000000400584 <add>:
  400584:   0b010000    add w0, w0, w1
  400588:   d65f03c0    ret
✅ 验证通过:O2优化后ARM64的add仅2条指令

$ aarch64-linux-gnu-objdump -d /tmp/test_arm64_o2 | grep -A5 '<max>'
000000000040058c <max>:
  40058c:   6b01001f    cmp w0, w1
  400590:   1a81b000    csel w0, w0, w1, gt
  400594:   d65f03c0    ret
✅ 验证通过:ARM64使用csel(条件选择)指令,一条完成if-else
CSEL指令是ARM64的独特优势:根据条件标志从两个寄存器中选择一个值,无需分支跳转。x86需要cmp+jcc+mov多条指令,而ARM64只需cmp+csel两条。这减少了分支预测失败的开销,是ARM64比x86更高效的原因之一。

4. ARM32 Thumb模式分析

$ arm-linux-gnueabihf-gcc -O2 -mthumb -o /tmp/test_thumb /tmp/test_arm64.c
$ arm-linux-gnueabihf-objdump -d /tmp/test_thumb | grep -A5 '<add>'
00010554 <add>:
   10554:   4408        add r0, r1
   10556:   4770        bx  lr
✅ 验证通过:Thumb指令仅2字节(16位),代码密度更高

🔄 ARM vs x86关键差异对比

架构对比

特征x86-64AArch64
指令长度变长(1-15字节)固定4字节
参数传递RDI/RSI/RDX/RCX/R8/R9(6个)X0-X7(8个)
返回值RAXX0/W0
返回地址栈上(call自动push)LR寄存器(X30)
帧指针RBPX29(FP)
条件执行条件跳转(jcc)条件分支(B.cc)+CSEL
调用方式call/retBL/RET
函数序言push rbp; mov rsp,rbpstp x29,x30,[sp,#-16]!
寻址模式复杂(基址+索引*比例+偏移)简化(基址+偏移/索引*比例)

ARM64函数调用流程详解

# ARM64函数调用完整流程
caller:
    # 1. 准备参数
    MOV     X0, #10            # 第1个参数
    MOV     X1, #20            # 第2个参数
    # 2. 保存caller-saved寄存器(如果需要)
    # 3. 调用函数
    BL      callee             # X30 = 返回地址; 跳转
    # 4. 使用返回值(在X0中)

callee:
    # 函数序言(prologue)
    STP     X29, X30, [SP, #-32]!  # 保存FP和LR,分配栈空间
    MOV     X29, SP                # 设置帧指针
    STP     X19, X20, [SP, #16]    # 保存callee-saved寄存器
    
    # 函数体...
    
    # 函数结语(epilogue)
    LDP     X19, X20, [SP, #16]    # 恢复callee-saved寄存器
    LDP     X29, X30, [SP], #32    # 恢复FP和LR,释放栈空间
    RET                            # 返回(PC = X30)

ARM64系统调用

# Linux AArch64系统调用约定
# 系统调用号放入X8
# 参数依次X0-X5
# 使用SVC #0触发

# write(1, "Hello", 5)
MOV     X0, #1             # fd = stdout
ADR     X1, msg            # buf = "Hello"
MOV     X2, #5             # count = 5
MOV     X8, #64            # sys_write = 64
SVC     #0                 # 触发系统调用

# exit(0)
MOV     X0, #0             # status = 0
MOV     X8, #93            # sys_exit = 93
SVC     #0

🏋️ 课后练习

  1. 编写ARM64汇编的斐波那契函数,用交叉编译器验证
  2. 对比ARM64和x86-64对同一C函数的汇编输出,列出5个主要差异
  3. 分析Thumb和ARM模式对同一函数的代码大小差异,解释为什么Thumb更小
  4. 使用QEMU user模式运行ARM64二进制:qemu-aarch64 -L /usr/aarch64-linux-gnu/ ./program
  5. 理解CSEL、CSINC、CSINV等条件选择指令的用途,编写C代码验证编译器生成这些指令

🏆 成就解锁:ARM汇编入门

掌握AArch64寄存器 理解LDR/STR访存 学会交叉编译 ARM与x86对比 CSEL条件选择