第3章:汇编语言基础(x86/x64)——逆向工程的“母语”

说实话,很多刚接触Pwn的朋友一看到汇编就头大。我完全理解——满屏的mov、push、pop,看着像天书。但你要知道,汇编是逆向工程的母语。你读不懂汇编,就相当于拿着英文词典去读中文古诗——每个字都认识,连起来不知道啥意思。

这一章,咱们就把x86/x64汇编的底裤扒干净。不扯虚的,直接上干货。

3.1 寄存器体系:CPU的“手边记事本”

寄存器是什么?说白了就是CPU内部的一小块存储空间。它离运算单元最近,读写速度比内存快几个数量级。我习惯把寄存器想象成程序员桌面上的便签纸——随手记、随手用,但数量有限。

3.1.1 x86(32位)通用寄存器

寄存器 名称 我的理解
EAX 累加器 算数运算的主力,函数返回值也放这
EBX 基址寄存器 常用来存内存地址的基址
ECX 计数器 循环、移位操作默认用它计数
EDX 数据寄存器 配合EAX做乘除法,存余数
ESI 源索引 字符串操作时指向源地址
EDI 目标索引 字符串操作时指向目标地址
ESP 栈指针 指向当前栈顶,千万别乱改
EBP 基址指针 指向当前栈帧底部,方便访问局部变量
小技巧:在x64下,这些寄存器前面加个R——RAX、RBX、RCX……而且多了R8~R15共8个新寄存器。64位模式下,函数参数前6个用寄存器传,不用全压栈了,效率高不少。

3.1.2 标志寄存器(EFLAGS/RFLAGS)

这个寄存器不存数据,存的是状态。比如上一条指令结果是不是0(ZF标志)、有没有进位(CF标志)、有没有溢出(OF标志)。逆向时经常看到jz、jnz这类跳转指令,它们就是根据标志位来决定跳不跳的。

我记得有一次逆向一个CTF题目,函数里有个cmp eax, 0x1337后面跟了个jz。我当时没注意标志位变化,硬是看了半天才反应过来——哦,这是在检查返回值是不是等于0x1337。

3.2 栈与函数调用约定

栈是什么?一个后进先出的数据结构。在汇编层面,栈就是内存里一块连续区域,由ESP/RSP指向栈顶。push让ESP减小,pop让ESP增大。嗯,这里要注意:栈是向下增长的——你往栈里压东西,栈顶地址反而变小。

3.2.1 函数调用过程

一个函数调用,CPU干了三件事:

  1. 传参:按约定把参数放到寄存器或压栈
  2. 保存返回地址:call指令会把下一条指令地址压栈
  3. 跳转:CPU跳到函数入口执行

函数返回时,ret指令从栈顶弹出返回地址,跳回去继续执行。

核心概念:栈帧(Stack Frame)——每个函数调用在栈上占用的那一段区域。EBP指向栈帧底部,ESP指向栈帧顶部。局部变量、保存的寄存器、返回地址都在这个帧里。

3.2.2 三种调用约定对比

约定 参数传递 谁清理栈 适用场景
cdecl 全部压栈(从右到左) 调用者 C语言默认,CTF最常见
stdcall 全部压栈(从右到左) 被调用者 Win32 API
fastcall 前两个参数用ECX/EDX,其余压栈 被调用者 追求速度的场景

我个人习惯在逆向时先看函数开头有没有ret 0x10这种带参数的返回——如果有,八成是stdcall或fastcall。cdecl的话,ret后面什么都不跟,调用者自己加add esp, xxx来清理。

避坑指南:我曾经在分析一个DLL导出函数时,默认按cdecl去解析参数,结果栈一直对不齐。折腾了两小时才发现人家用的是stdcall。记住:调用约定不对,逆向全白费。

3.3 常用指令详解

下面这些指令,你在逆向里每天都会见到。我按使用频率排了个序。

3.3.1 数据传送类

  • mov dst, src:把src的值复制到dst。注意,两个操作数不能都是内存地址。
  • push src:把src压栈,ESP减4(32位)或8(64位)。
  • pop dst:从栈顶弹出数据到dst,ESP增加。
  • lea dst, [addr]:加载有效地址。不访问内存,只计算地址。比如lea eax, [ebx+ecx*4]——这比mov加乘法快多了。
一个坑:lea和mov的区别。你想想看,mov eax, [ebx]是把ebx指向的内存里的值取出来;而lea eax, [ebx]是把ebx的地址本身给eax。我见过新手把这两个搞混,调试半天找不到bug。

3.3.2 控制流类

  • call addr:先把返回地址压栈,然后跳转到addr。
  • ret:从栈顶弹出返回地址,跳转过去。
  • jmp addr:无条件跳转到addr。
  • jz / je addr:如果ZF=1(结果为0/相等),跳转。
  • jnz / jne addr:如果ZF=0(结果不为0/不相等),跳转。
  • jg / jge / jl / jle:有符号数比较跳转。
  • ja / jae / jb / jbe:无符号数比较跳转。

这里有个小规律:j开头的指令都是条件跳转,后面两个字母代表条件。比如jz就是jump if zero。逆向时看到cmp后面跟jz,基本就是个if判断。

3.3.3 算术与逻辑类

  • add dst, src:dst += src
  • sub dst, src:dst -= src
  • inc dst:dst++
  • dec dst:dst--
  • xor dst, src:dst ^= src。注意,xor eax, eax是清零的惯用写法,比mov eax, 0更高效。
  • and / or / not:按位与、或、非。
  • shl / shr:左移/右移。左移1位相当于乘2,右移1位相当于除2。

3.4 知识体系总览

下面这张图把本章的核心知识点串起来了。你把它存脑子里,以后逆向时随时调取。

汇编语言基础知识体系 汇编语言基础(x86/x64)知识体系 寄存器体系 栈与调用约定 常用指令 通用寄存器(EAX/EBX/ECX/EDX…) 指针寄存器(ESP/EBP) 标志寄存器(EFLAGS) x64扩展(R8~R15) 栈帧结构(EBP/ESP) call/ret 执行流程 cdecl(调用者清理) stdcall(被调用者清理) fastcall(寄存器传参) mov / push / pop / lea call / ret / jmp / jz / jnz add / sub / inc / dec xor / and / or / not shl / shr(移位) 三者结合:寄存器存数据 → 栈管理调用 → 指令驱动执行

3.5 实战小练习

光看不练假把式。我给你一段简单的汇编代码,你试着分析它在干什么:

push ebp
mov ebp, esp
sub esp, 0x10
mov dword [ebp-0x4], 0xa
mov dword [ebp-0x8], 0x14
mov eax, [ebp-0x4]
add eax, [ebp-0x8]
mov esp, ebp
pop ebp
ret

这段代码做了什么事?我提示你:它先保存了旧的EBP,然后开辟了16字节的局部空间,存了两个数,加起来,最后返回。嗯,说白了就是一个加法函数。

本章核心:寄存器是CPU的便签纸,栈是函数调用的舞台,指令是舞台上的演员。你把这套逻辑理清了,逆向工程的大门就推开了一半。