📍 【基础概念 1-5】 | 逆向工程系列课程
x86/x86-64是PC和服务器的统治性架构,绝大多数桌面软件、服务器程序和恶意软件都以x86二进制形式发布。理解x86汇编是逆向工程的基本功——你无法读懂你不懂的语言。本课将从寄存器、寻址方式、常用指令到函数调用约定,系统建立x86汇编的知识体系。
| 架构 | 位数 | 通用寄存器 | 常见场景 |
|---|---|---|---|
| 8086 | 16位 | AX/BX/CX/DX/SI/DI/BP/SP | 历史遗留、BIOS |
| i386 | 32位 | EAX/EBX/ECX/EDX/ESI/EDI/EBP/ESP | 老旧软件、驱动、恶意软件 |
| x86-64 | 64位 | RAX~R15 + RSI/RDI/RBP/RSP | 现代系统主流 |
┌─────────────────────────────────────────────────────┐
│ 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, %rax | mov rax, 42 | 直接使用常量值 |
| 寄存器 | mov %rax, %rbx | mov rbx, rax | 寄存器间传送数据 |
| 直接内存 | mov 0x601000, %rax | mov rax, [0x601000] | 访问固定内存地址 |
| 基址+偏移 | mov 8(%rbp), %rax | mov rax, [rbp+8] | 访问栈帧中的变量 |
| 基址+索引*比例 | mov (%rbx,%rcx,4), %rax | mov rax, [rbx+rcx*4] | 数组元素访问 |
| 完整寻址 | mov 8(%rbx,%rcx,4), %rax | mov rax, [rbx+rcx*4+8] | 结构体数组访问 |
| RIP相对 | mov (%rip), %rax | mov rax, [rip] | 位置无关代码(PIC) |
# 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
$ 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返回
# 对比-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条指令
$ 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条件分支
$ 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循环结构
$ 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 (Linux默认) | Intel (Windows默认) |
|---|---|---|
| 操作数顺序 | 源 → 目的 mov $5, %eax | 目的 ← 源 mov eax, 5 |
| 寄存器前缀 | %eax | eax |
| 立即数前缀 | $42 | 42 |
| 内存操作数 | 0x8(%rbp,%rcx,4) | [rbp+rcx*4+0x8] |
| 指令后缀 | movl, movq, addw | mov dword, mov qword |
| 注释 | # 注释 | ; 注释 |
| HEX前缀 | 0x前缀 | 0x或h后缀 |
set disassembly-flavor intel 切换到Intel语法。int factorial(int n)(递归版和迭代版),用objdump查看汇编差异gcc -S生成switch-case的汇编,分析跳转表(jump table)的实现*p = *q的代码,分析寻址方式的变化gdb -tui ./program,观察寄存器变化你已经掌握了x86-64汇编的基础知识!能读懂基本的函数序言、参数传递、条件分支和循环结构,这是逆向工程最核心的能力。
掌握x86-64寄存器 理解7种寻址方式 能读懂基本指令序列 分析函数调用约定 O0 vs O2对比分析