第11章:反汇编基础:x86/x64指令集概述、常用指令与寻址方式

说实话,很多刚入行的朋友一看到汇编指令就头大。密密麻麻的字母和数字,看着像天书。我当年第一次接触反汇编时,也是这种感觉。但后来我发现,只要抓住几个核心指令和寻址方式,整个x86/x64的世界就会清晰很多。

这一章,我们就来聊聊反汇编的基础。说白了,就是让你能看懂IDA或Ghidra吐出来的那些汇编代码。嗯,咱们不搞那些花里胡哨的理论,直接上干货。

11.1 x86与x64:到底有什么区别?

先解决一个最基础的问题。x86是32位的,x64是64位的。我经常在项目中看到有人把这两个混为一谈,结果调试了半天发现地址对不上。

它们的主要区别,我列个表给你看:

特性 x86 (32位) x64 (64位)
通用寄存器 EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP + R8~R15
指针宽度 32位 (4字节) 64位 (8字节)
最大内存 4GB 理论上16EB
调用约定 __cdecl, __stdcall, __fastcall Microsoft x64 calling convention / System V AMD64 ABI

我个人习惯,在反汇编时第一件事就是看地址宽度。如果看到地址是0x00401000这种,那就是32位;如果是0x7FFE12345678这种,那就是64位。这个判断几乎不会出错。

小技巧: 在IDA中,按"Alt + G"可以快速切换显示模式。我经常用这个来确认当前分析的是32位还是64位代码。

11.2 核心指令:MOV、JMP、CALL、RET

这四个指令,你反汇编100个样本,99个都会遇到。剩下的1个可能是加了花指令的。我们先从最简单的开始。

11.2.1 MOV:数据搬运工

MOV指令,说白了就是把数据从一个地方搬到另一个地方。它不改变源数据,只复制。格式很简单:

MOV 目标操作数, 源操作数

举个例子:

MOV EAX, 0x12345678    ; 将立即数0x12345678存入EAX
MOV EBX, EAX           ; 将EAX的值复制到EBX
MOV ECX, [EAX]         ; 将EAX指向的内存地址中的值存入ECX

我在项目中遇到过一个问题:有人用MOV指令去修改代码段,结果触发了DEP(数据执行保护)。嗯,这里要注意,MOV虽然强大,但不能随意往代码段写数据,除非你明确知道自己在做什么。

11.2.2 JMP:无条件的跳转

JMP就是"跳",不带任何条件。遇到JMP,程序就跑到别的地方去了。它常用于:

  • 循环的末尾跳回开头
  • switch-case的跳转表
  • 代码混淆(比如花指令中的垃圾跳转)
JMP 0x401000    ; 直接跳转到地址0x401000
JMP EAX         ; 跳转到EAX中存储的地址

你想想看,如果反汇编时看到连续好几个JMP,那多半是遇到了混淆。我曾经逆向过一个恶意软件,它用JMP把自己绕成了迷宫,我花了整整两天才理清它的控制流。

11.2.3 CALL:调用子程序

CALL和JMP有点像,但它多了一步:它会先把下一条指令的地址压入栈中,然后再跳转。这样,被调用的函数执行完后,可以用RET指令回到原来的位置继续执行。

CALL 0x401020    ; 调用地址0x401020处的函数
; 执行完函数后,会回到这里继续

为什么CALL要压栈?因为栈是后进先出的结构,正好适合嵌套调用。你想想看,A调用B,B调用C,C返回B,B返回A,这个顺序用栈来实现再自然不过了。

重点: 在反汇编时,CALL指令后面的地址就是函数的入口。我通常会在IDA中给这个地址重命名,比如"sub_401020"改成"decrypt_data",这样看起来就清晰多了。

11.2.4 RET:返回调用者

RET就是CALL的逆操作。它从栈顶弹出一个地址,然后跳转到那个地址。说白了,就是"我干完了,该回去了"。

RET      ; 从栈顶弹出地址并跳转
RET 8    ; 弹出地址后,再调整栈指针ESP+8(用于清理参数)

这里有个坑。我曾经调试一个程序,发现RET之后程序崩溃了。查了半天,原来是栈被破坏了,RET弹出了一个错误的地址。嗯,栈平衡是逆向分析中必须时刻关注的点。

11.3 寻址方式:数据在哪里?

指令要操作数据,首先得知道数据在哪。寻址方式就是告诉CPU"数据在哪"的方法。x86/x64的寻址方式挺多的,但常用的就那么几种。

11.3.1 立即数寻址

数据直接写在指令里。比如:

MOV EAX, 0x100    ; 0x100就是立即数

11.3.2 寄存器寻址

数据在寄存器里。比如:

MOV EAX, EBX      ; 数据在EBX中

11.3.3 直接内存寻址

数据在内存中,地址直接给出。比如:

MOV EAX, [0x401000]    ; 从地址0x401000读取数据

11.3.4 寄存器间接寻址

数据在内存中,地址存在寄存器里。比如:

MOV EAX, [EBX]    ; 从EBX指向的地址读取数据

11.3.5 变址寻址

这是最灵活的,常用于数组和结构体。格式是:

[基址 + 变址 * 比例 + 偏移]

比如:

MOV EAX, [EBP + ECX * 4 + 0x10]

这个在遍历数组时特别常见。ECX是索引,乘以4是因为每个元素占4字节,EBP是基址,0x10是结构体内部的偏移。

注意: 在x64下,有些寻址方式不能用了。比如,你不能直接用32位寄存器作为基址去访问64位地址空间。我见过有人把32位的代码直接移植到64位,结果寻址全乱了。

11.4 知识体系总览

为了让你更直观地理解这一章的内容,我画了一张图。它把指令集、常用指令和寻址方式的关系梳理了一遍。你看完应该能有个整体印象。

反汇编基础:x86/x64指令集与寻址方式 x86 / x64 指令集 常用指令 MOV(数据搬运) | JMP(无条件跳转) | CALL(调用子程序) | RET(返回) 寻址方式 立即数寻址 寄存器寻址 直接内存寻址 寄存器间接寻址 变址寻址 反汇编分析:识别指令、理解控制流、定位数据

11.5 实战中的一点体会

说了这么多,其实反汇编的核心就两件事:看懂指令在干什么看懂数据在哪。MOV、JMP、CALL、RET这四个指令,加上几种寻址方式,基本能覆盖90%以上的场景。

我记得有一次分析一个加壳的样本,脱壳后看到满屏的JMP和CALL,一开始完全摸不着头脑。后来我静下心来,一个一个地跟踪CALL的目标,把每个子函数的功能标注出来,慢慢地整个程序的逻辑就浮现出来了。

嗯,逆向工程就是这样,没有捷径。但只要你把基础打牢了,后面再复杂的样本,也不过是这些基础指令的排列组合而已。

我的建议: 刚开始学反汇编时,不要急着用高级工具。打开一个简单的程序,用调试器单步执行,看着寄存器怎么变,栈怎么压怎么弹。这个过程虽然慢,但效果最好。

公众号:蓝海资料掘金营,微信deep3321