第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位。这个判断几乎不会出错。
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,这个顺序用栈来实现再自然不过了。
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是结构体内部的偏移。
11.4 知识体系总览
为了让你更直观地理解这一章的内容,我画了一张图。它把指令集、常用指令和寻址方式的关系梳理了一遍。你看完应该能有个整体印象。
11.5 实战中的一点体会
说了这么多,其实反汇编的核心就两件事:看懂指令在干什么,看懂数据在哪。MOV、JMP、CALL、RET这四个指令,加上几种寻址方式,基本能覆盖90%以上的场景。
我记得有一次分析一个加壳的样本,脱壳后看到满屏的JMP和CALL,一开始完全摸不着头脑。后来我静下心来,一个一个地跟踪CALL的目标,把每个子函数的功能标注出来,慢慢地整个程序的逻辑就浮现出来了。
嗯,逆向工程就是这样,没有捷径。但只要你把基础打牢了,后面再复杂的样本,也不过是这些基础指令的排列组合而已。
公众号:蓝海资料掘金营,微信deep3321