第02课:x86汇编基础

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

🎯 为什么学习x86汇编

x86/x86-64是PC和服务器的统治性架构,绝大多数桌面软件、服务器程序和恶意软件都以x86二进制形式发布。理解x86汇编是逆向工程的基本功——你无法读懂你不懂的语言。本课将从寄存器、寻址方式、常用指令到函数调用约定,系统建立x86汇编的知识体系。

x86架构家族

架构位数通用寄存器常见场景
808616位AX/BX/CX/DX/SI/DI/BP/SP历史遗留、BIOS
i38632位EAX/EBX/ECX/EDX/ESI/EDI/EBP/ESP老旧软件、驱动、恶意软件
x86-6464位RAX~R15 + RSI/RDI/RBP/RSP现代系统主流

📋 通用寄存器详解

x86-64寄存器一览

┌─────────────────────────────────────────────────────┐
│  64位        32位        16位    8位(高)  8位(低)    │
├─────────────────────────────────────────────────────┤
│  RAX         EAX         AX      AH       AL         │  累加器(函数返回值)
│  RBX         EBX         BX      BH       BL         │  基址寄存器
│  RCX         ECX         CX      CH       CL         │  计数器(循环/移位)
│  RDX         EDX         DX      DH       DL         │  数据寄存器(I/O)
│  RSI         ESI         SI      -        SIL        │  源索引(字符串操作)
│  RDI         EDI         DI      -        DIL        │  目标索引(字符串操作)
│  RBP         EBP         BP      -        BPL        │  栈帧指针
│  RSP         ESP         SP      -        SPL        │  栈指针
│  R8-R15      R8D-R15D    R8W+   -        R8B+       │  扩展寄存器
└─────────────────────────────────────────────────────┘

System V AMD64 ABI 调用约定(Linux/macOS使用):

🔢 寻址方式

寻址方式AT&T语法Intel语法说明
立即数mov $42, %raxmov rax, 42直接使用常量值
寄存器mov %rax, %rbxmov rbx, rax寄存器间传送数据
直接内存mov 0x601000, %raxmov rax, [0x601000]访问固定内存地址
基址+偏移mov 8(%rbp), %raxmov rax, [rbp+8]访问栈帧中的变量
基址+索引*比例mov (%rbx,%rcx,4), %raxmov rax, [rbx+rcx*4]数组元素访问
完整寻址mov 8(%rbx,%rcx,4), %raxmov rax, [rbx+rcx*4+8]结构体数组访问
RIP相对mov (%rip), %raxmov rax, [rip]位置无关代码(PIC)
Linux默认使用AT&T语法(objdump、GDB),Windows逆向通常使用Intel语法(IDA Pro、x64dbg)。两种语法必须都熟悉!

📝 常用指令详解

数据传送指令

# MOV:数据传送(最常用指令)
mov    $0x10, %rax      # rax = 0x10 (立即数→寄存器)
mov    %rax, %rbx       # rbx = rax (寄存器→寄存器)
mov    (%rbp), %rax     # rax = *rbp (内存→寄存器)
mov    %rax, (%rbp)     # *rbp = rax (寄存器→内存)

# MOVZX / MOVSX:零扩展/符号扩展传送
movzwl %ax, %eax        # 零扩展16位→32位 (无符号)
movslq %eax, %rax       # 符号扩展32位→64位 (有符号)

# LEA:加载有效地址(关键指令!不访问内存)
lea    0x10(%rbx,%rcx,4), %rax  # rax = rbx + rcx*4 + 0x10
# 常用于:1)计算地址 2)快速算术 3)不触发内存访问
# 逆向时遇到lea,先判断是算术还是真正取地址

算术与逻辑指令

# 算术运算
add    $5, %rax         # rax += 5
sub    $3, %rax         # rax -= 3
imul   %rbx, %rax       # rax *= rbx (有符号乘法)
div    %rbx             # rax = rdx:rax / rbx, rdx = 余数
inc    %rax             # rax++ (自增)
dec    %rax             # rax-- (自减)
neg    %rax             # rax = -rax (取反)

# 逻辑运算
and    $0xFF, %rax      # rax &= 0xFF (掩码操作)
or     $0x80, %al       # al |= 0x80 (设置特定位)
xor    %rax, %rax       # rax = 0 (清零,比mov $0更高效)
not    %rax             # rax = ~rax (按位取反)
shl    $4, %rax         # rax <<= 4 (逻辑左移)
shr    $2, %rax         # rax >>= 2 (逻辑右移,高位补0)
sar    $2, %rax         # rax >>= 2 (算术右移,保留符号位)

比较与控制流指令

# 比较(设置标志位但不修改操作数)
cmp    $10, %rax        # 计算 rax - 10,设置CF/ZF/SF/OF
test   %rax, %rax       # 计算 rax & rax,判断是否为零(常用!)

# 条件跳转(基于标志位)
je     label            # jump if equal (ZF=1)
jne    label            # jump if not equal (ZF=0)
jg     label            # jump if greater (signed: ZF=0 & SF=OF)
jge    label            # jump if greater or equal
jl     label            # jump if less (signed: SF!=OF)
jle    label            # jump if less or equal
ja     label            # jump if above (unsigned: CF=0 & ZF=0)
jb     label            # jump if below (unsigned: CF=1)

# 无条件跳转
jmp    label            # 直接跳转
jmp    *%rax            # 间接跳转(跳到rax指向的地址)

# 条件设置(不跳转,设置字节)
sete   %al              # if ZF=1 then al=1 else al=0
setl   %al              # if less then al=1 else al=0

栈操作与函数调用

# 栈操作(栈向低地址增长)
push   %rax             # rsp -= 8; *rsp = rax
pop    %rax             # rax = *rsp; rsp += 8

# 函数调用
call   func             # push %rip(下一条指令); jmp func
ret                     # pop %rip; jmp (返回)

# 典型函数序言(prologue)和结语(epilogue)
func:
    push   %rbp            # 保存调用者的栈帧指针
    mov    %rsp, %rbp      # 建立新的栈帧
    sub    $0x20, %rsp     # 分配局部变量空间(32字节)
    # ... 函数体 ...
    mov    %rbp, %rsp      # 释放局部变量空间(等价于add $0x20,%rsp)
    pop    %rbp            # 恢复调用者的栈帧指针
    ret                    # 返回到调用者

# 叶子函数(不调用其他函数)可以省略栈帧
leaf_func:
    # 直接使用rsp访问参数和局部变量
    ret

💻 实操:编写并分析x86汇编程序

1. 创建示例C程序并查看汇编

$ cat > /tmp/test_x86.c <<'CEOF'
#include <stdio.h>

int add_numbers(int a, int b) {
    int result = a + b;
    return result;
}

int main() {
    int sum = add_numbers(10, 20);
    printf("Sum: %d\n", sum);
    return 0;
}
CEOF

$ gcc -O0 -no-pie -o /tmp/test_x86 /tmp/test_x86.c

# 反汇编add_numbers函数
$ objdump -d /tmp/test_x86 | grep -A20 '<add_numbers>'
0000000000401136 <add_numbers>:
  401136:   55                      push   %rbp
  401137:   48 89 e5                mov    %rsp,%rbp
  40113a:   89 7d ec                mov    %edi,-0x14(%rbp)    # 保存参数a
  40113d:   89 75 e8                mov    %esi,-0x18(%rbp)    # 保存参数b
  401140:   8b 55 ec                mov    -0x14(%rbp),%edx    # edx = a
  401143:   8b 45 e8                mov    -0x18(%rbp),%eax    # eax = b
  401145:   01 d0                   add    %edx,%eax           # eax = a + b
  401147:   89 45 fc                mov    %eax,-0x4(%rbp)     # result = eax
  40114a:   8b 45 fc                mov    -0x4(%rbp),%eax     # eax = result
  40114d:   5d                      pop    %rbp
  40114e:   c3                      ret
✅ 验证通过:参数通过edi/esi传入,结果通过eax返回

2. 分析优化版本的差异

# 对比-O2优化
$ gcc -O2 -no-pie -o /tmp/test_x86_o2 /tmp/test_x86.c
$ objdump -d /tmp/test_x86_o2 | grep -A10 '<add_numbers>'
0000000000401050 <add_numbers>:
  401050:   8d 04 37                lea    (%rdi,%rsi,1),%eax  # eax = rdi + rsi
  401053:   c3                      ret
✅ 验证通过:O2优化后函数被精简为lea+ret,仅2条指令
O0 vs O2 深度对比
O0版本:参数先存栈(rbp-0x14, rbp-0x18),再从栈读取到寄存器相加,结果存栈(rbp-0x4)再读出到eax——共9条指令,5次内存访问
O2版本:直接用lea计算rdi+rsi存入eax——仅2条指令,0次内存访问。优化器消除了所有冗余的栈操作,将加法直接映射到lea指令。
逆向实战中,O2代码更紧凑但更难理解,因为变量分配在寄存器而非栈上。

3. 条件分支分析

$ cat > /tmp/test_branch.c <<'CEOF'
int max(int a, int b) {
    if (a > b) return a;
    else return b;
}
CEOF

$ gcc -O0 -no-pie -S -o /tmp/test_branch.s /tmp/test_branch.c
$ grep -A20 'max:' /tmp/test_branch.s
max:
  pushq  %rbp
  movq   %rsp, %rbp
  movl   %edi, -4(%rbp)       # 保存a
  movl   %esi, -8(%rbp)       # 保存b
  movl   -4(%rbp), %eax       # eax = a
  cmpl   -8(%rbp), %eax       # a - b,设置标志位
  jle    .L2                   # if a <= b 跳到else
  movl   -4(%rbp), %eax       # return a
  jmp    .L3                   # 跳到函数结尾
.L2:
  movl   -8(%rbp), %eax       # return b
.L3:
  popq   %rbp
  ret
✅ 验证通过:cmp+jle+jmp实现if-else条件分支

4. 循环结构分析

$ cat > /tmp/test_loop.c <<'CEOF'
int sum_n(int n) {
    int s = 0;
    for (int i = 0; i < n; i++)
        s += i;
    return s;
}
CEOF

$ gcc -O1 -no-pie -S -o /tmp/test_loop.s /tmp/test_loop.c
$ grep -A20 'sum_n:' /tmp/test_loop.s
sum_n:
  pushq  %rbp
  movq   %rsp, %rbp
  movl   %edi, -20(%rbp)      # 保存n
  movl   $0, -4(%rbp)         # s = 0
  movl   $0, -8(%rbp)         # i = 0
  jmp    .L2                   # 跳到条件检查
.L3:
  addl   -8(%rbp), -4(%rbp)   # s += i
  addl   $1, -8(%rbp)         # i++
.L2:
  movl   -8(%rbp), %eax       # eax = i
  cmpl   -20(%rbp), %eax      # i - n
  jl     .L3                   # if i < n 继续循环
  movl   -4(%rbp), %eax       # return s
  popq   %rbp
  ret
✅ 验证通过:jmp+cmpl+jl构成for循环结构

5. 数组与指针操作

$ cat > /tmp/test_array.c <<'CEOF'
int sum_array(int *arr, int n) {
    int s = 0;
    for (int i = 0; i < n; i++)
        s += arr[i];
    return s;
}
CEOF

$ gcc -O2 -no-pie -S -o /tmp/test_array.s /tmp/test_array.c
$ grep -A15 'sum_array:' /tmp/test_array.s
sum_array:
  testl   %esi, %esi           # 测试n是否为0
  jle     .L4                   # n <= 0 则返回0
  leal    -1(%rsi), %eax       # eax = n-1
  leaq    4(%rdi,%rax,4), %rcx # rcx = arr + (n-1)*4 + 4 = 结束地址
  xorl    %eax, %eax           # s = 0
.L3:
  addl    (%rdi), %eax         # s += *arr
  addq    $4, %rdi             # arr++ (移动4字节/int)
  cmpq    %rcx, %rdi           # 比较当前指针和结束地址
  jne     .L3                   # 继续循环
.L4:
  ret
✅ 验证通过:O2优化后用指针遍历替代数组索引

📊 AT&T vs Intel语法速查

特征AT&T (Linux默认)Intel (Windows默认)
操作数顺序源 → 目的 mov $5, %eax目的 ← 源 mov eax, 5
寄存器前缀%eaxeax
立即数前缀$4242
内存操作数0x8(%rbp,%rcx,4)[rbp+rcx*4+0x8]
指令后缀movl, movq, addwmov dword, mov qword
注释# 注释; 注释
HEX前缀0x前缀0x或h后缀
在GDB中可以切换语法:set disassembly-flavor intel 切换到Intel语法。

🏋️ 课后练习

  1. 编写C函数int factorial(int n)(递归版和迭代版),用objdump查看汇编差异
  2. gcc -S生成switch-case的汇编,分析跳转表(jump table)的实现
  3. 编写含指针操作*p = *q的代码,分析寻址方式的变化
  4. 用GDB单步执行一个简单程序:gdb -tui ./program,观察寄存器变化
  5. 尝试手动将一段简单汇编翻译回C伪代码(这是逆向的核心技能)

🏆 成就解锁:汇编初学者

你已经掌握了x86-64汇编的基础知识!能读懂基本的函数序言、参数传递、条件分支和循环结构,这是逆向工程最核心的能力。

掌握x86-64寄存器 理解7种寻址方式 能读懂基本指令序列 分析函数调用约定 O0 vs O2对比分析