第 38 章 汇编语言精讲(读懂工具输出的底层)
反编译器输出再漂亮,关键时候还是要回汇编确认。本章系统讲透 x86-64 汇编的核心知识:寻址、数据传送、算术、控制流、函数调用、调用约定、栈布局。这是逆向的"语言课"。
📍 知识点地图 | 主题:汇编语言精讲 | 前置:第3章 | 后续:第18章 | 核心概念:寄存器、寻址、调用约定、栈帧
38.1 x86-64 寄存器模型
| 寄存器 | 用途 | 逆向中常见含义 |
|---|---|---|
| RAX | 累加器/返回值 | 函数返回值、运算结果 |
| RBX | 通用(callee-saved) | 跨调用保存的值 |
| RCX/RDX | 参数/计数/数据 | Windows 第 1/2 参数 |
| RSI/RDI | 源/目标 | strcmp/memcpy 参数、Linux 参数 |
| RBP | 帧指针 | 栈帧基址(优化代码可能不用) |
| RSP | 栈指针 | 栈顶 |
| R8-R11 | 通用 | 第 3/4 参数(64 位约定) |
| R12-R15 | 通用(callee-saved) | 循环变量/基址 |
| RIP | 指令指针 | 当前指令(相对寻址) |
| RFLAGS | 标志位 | ZF/CF/SF/OF → 比较结果 |
32 位: EAX/EBX/...(RAX 的低 32 位)
16 位: AX/BX/...
8 位: AL(AX 低 8 位)、AH(AX 高 8 位)
规则: 写 EAX 会清零 RAX 高 32 位;写 AL 只动低 8 位
38.2 寻址模式(一切操作的基础)
; 立即数(常量)
mov eax, 0x42
; 寄存器
mov eax, ebx
; 内存寻址: [base + index*scale + disp]
mov eax, [rbx] ; 基址
mov eax, [rbx + 0x10] ; 基址 + 偏移(结构体字段访问!)
mov eax, [rbx + rcx*4] ; 数组元素(index*scale)
mov eax, [rbx + rcx*8 + 0x10] ; 结构体数组
lea rax, [rbx + 0x20] ; 计算地址(不访问内存!常用于指针运算)
; RIP 相对寻址(64 位程序最常见)
lea rax, [rip + 0x1234] ; 访问全局变量/字符串(位置无关代码)
逆向速读:[reg + 0x10] 一眼看出"结构体第 0x10 偏移的字段"。
38.3 常用指令族
数据传送
mov dst, src ; 拷贝
movzx / movsx ; 零扩展/符号扩展(8/16 → 32/64)
movsxd ; 32 → 64 符号扩展(关键坑!)
lea ; 算地址不访存
xchg ; 交换
push / pop ; 压栈/弹栈(栈操作)
算术逻辑
add / sub / inc / dec ; 加减
imul / mul ; 乘(imul 有符号)
idiv / div ; 除(商在 RAX,余数在 RDX)
and / or / xor / not ; 逻辑运算
shl / shr / sar / rol / ror ; 移位/循环移位
neg ; 取反
识别加密特征:大量 xor/shl/add 组合 = DIY 加密;mul+add 连用 = 线性变换。
比较与跳转(控制流核心)
cmp a, b ; 计算 a-b,设置标志位(不保存结果)
test a, a ; 计算 a&a,设置标志(等价 cmp a, 0)
; 条件跳转(记住"J after CMP")
je/jz 相等则跳 jne/jnz 不等则跳
jg/jge 大于(有符号) jl/jle 小于(有符号)
ja/jae 高于(无符号) jb/jbe 低于(无符号)
js/jns 符号位 jc/jnc 进位
jmp 无条件跳转
call / ret 调用/返回
逆向速读:
cmp eax, 0x1337
jne fail → "如果 eax != 0x1337 就失败" → 成功条件是 eax == 0x1337
38.4 函数调用与栈帧(最重要的章节)
调用约定(64 位)
| 平台 | 参数传递 | 返回值 | 说明 |
|---|---|---|---|
| System V(Linux/macOS) | RDI, RSI, RDX, RCX, R8, R9 | RAX | 前 6 个整数参数 |
| Windows x64 | RCX, RDX, R8, R9 | RAX | 前 4 个;还有 shadow space |
| 浮点 | XMM0-XMM7 | XMM0 | 两种约定一致 |
标准函数序言/尾声
; 序言(prologue)
push rbp ; 保存调用者帧指针
mov rbp, rsp ; 建立新帧
sub rsp, 0x40 ; 分配局部变量空间
; 函数体...
; 尾声(epilogue)
leave ; = mov rsp, rbp; pop rbp
ret ; 返回(弹出返回地址到 RIP)
栈帧布局图(必须画过的图)
高地址
┌──────────────┐
│ 调用者局部变量 │
├──────────────┤
│ 返回地址 │ ← call 压入
├──────────────┤
│ 保存的 RBP │ ← push rbp
├──────────────┤ ← RBP 指向这里
│ 局部变量区 │
│ (sub rsp 分配)│
├──────────────┤
│ 参数区/临时区 │
└──────────────┘ ← RSP
低地址
常见漏洞在栈帧中的表现
sub rsp, 0x40 + 读入 0x200 字节 → 栈溢出(缓冲区超过分配空间)
canary(金丝雀): [rbp-0x8] 处有随机值 → 溢出检测
泄露地址 → 绕过 ASLR/PIE 的基础
38.5 常见代码模式的汇编识别(翻译训练)
| C 代码 | 汇编模式 |
|---|---|
if (a == b) |
cmp a,b; jne skip |
switch(x) |
跳转表 jmp [table + x*8] |
for(i=0;i<n;i++) |
xor eax,eax; loop: cmp eax,n; jge done; ... inc eax; jmp loop |
strcpy(dst, src) |
mov rdi,dst; mov rsi,src; call strcpy |
p = obj->field |
mov rax, [obj + offset] |
arr[i] |
mov rax, [arr_base + i*elemsize] |
| 三元/布尔 | setne al; movzx eax, al(setcc 模式) |
| 字符串比较 | call strcmp; test eax,eax; jne fail |
38.6 ARM64 快速对照(移动端/嵌入式必懂)
寄存器: X0-X30(64 位),W0-W30(低 32 位)
X0-X7 = 参数/返回值(AAPCS)
X29 = 帧指针(FP),X30 = 链接寄存器(LR,返回地址)
XSP = 栈指针
核心指令:
LDR/STR 加载/存储(str/ldr 是"存储/加载",与 x86 相反!)
ADRP + ADD 加载全局地址(相当于 lea)
B / BL 分支/带链接分支(BL = call)
BR x30 ret(返回)
CBZ/CBNZ 为零跳/非零跳
CMP/B.EQ 比较+条件分支
与 x86 的核心差异:
x86: mov 指令多(mov dest, src)
ARM: 操作数寄存器化(add x0, x1, x2 三操作数)
条件执行: csel/ccmp(无分支条件逻辑)
38.7 汇编阅读训练法(如何练成条件反射)
1. 大量阅读: 每分析一个函数,先读汇编再对反编译(两种视角交替)
2. 模式卡片: 把 38.5 的表做成记忆卡,看到即认
3. 自我测试: 给一段汇编写 C 等价物,再给 C 写汇编要点
4. 关注差异: 编译器优化(-O2)会让源码模式变形,学会认"优化特征"
5. 不追求每行都懂: 先抓控制流骨架(函数边界/分支/循环),再补细节
动手练习
- 写一个含 if/switch/for/结构体访问的 C 程序,编译后对照汇编逐行翻译。
- 用 GDB 观察一个函数的 prologue/epilogue,画出它的栈帧(对照 38.4 图)。
- 找 5 段陌生汇编,先自己翻译成 C,再用反编译器验证。
- 编译同一程序到 x86-64 和 ARM64(交叉编译),对照两种架构的实现差异。
深入阅读
- 仓库:
skills/reverse-engineering/SKILL.md(快速工具参考) - 仓库:
skills/reverse-engineering/elf-analysis.md(ARM64 寄存器/指令模式) - 仓库:
skills/reverse-engineering/platforms-hardware.md(ARM64 gadget/ROP) - 仓库:
skills/reverse-engineering/kernel-driver-reverse.md(C/C++ 模式识别表) - 仓库:
skills/reverse-engineering/languages-compiled.md(C++ vtable/STL 汇编特征)