二进制基础回顾:计算机体系结构基础
说实话,每次带新人入门逆向,我都要先问一句:「你清楚程序在机器里到底是怎么跑的吗?」很多人能写出不错的代码,但一说到 CPU 怎么取指令、内存怎么布局、数据怎么存,就开始含糊了。这一章,我们就来把这些基础夯实。我个人习惯把计算机体系结构比作一个「车间」——CPU 是工人,内存是工作台,寄存器是工人手里的工具。搞懂这三者的配合,逆向的大门才算真正推开。
CPU、内存与寄存器:铁三角
CPU 负责执行指令,内存负责存储指令和数据,寄存器则是 CPU 内部的高速缓存单元。这三者通过总线连接,构成了程序运行的基础。
核心理解: 程序在运行前,代码和数据都躺在内存里。CPU 从内存取指令,放到寄存器里运算,结果再写回内存。逆向分析时,你看到的反汇编代码,就是 CPU 视角下的「操作清单」。
我在项目中遇到过不少新手,一上来就盯着反汇编代码看,却搞不清当前操作的是寄存器还是内存地址。嗯,这里要记住:寄存器是 CPU 内部的,访问速度极快;内存是外部的,访问相对慢得多。逆向时,频繁出现的 mov eax, [ebx] 这类指令,就是典型的「从内存读数据到寄存器」。
x86/x64 寄存器一览
x86 架构有 8 个通用寄存器(32 位),x64 扩展到了 16 个(64 位)。我习惯把它们分成几类:
| 类别 | 32位 | 64位 | 常见用途 |
|---|---|---|---|
| 通用寄存器 | EAX, EBX, ECX, EDX | RAX, RBX, RCX, RDX | 算术运算、函数返回值(EAX/RAX) |
| 索引寄存器 | ESI, EDI | RSI, RDI | 字符串操作、内存拷贝源/目标 |
| 栈指针 | ESP | RSP | 指向当前栈顶 |
| 基址指针 | EBP | RBP | 指向当前栈帧基址 |
| 指令指针 | EIP | RIP | 指向下一条要执行的指令 |
我的小技巧: 逆向时,我通常会先关注 EAX/RAX(返回值)、ESP/RSP(栈操作)和 EIP/RIP(控制流)。这三个寄存器能帮你快速定位函数调用和跳转逻辑。
数据在内存中的表示:大端序与小端序
这个问题,我当年刚入行时栽过跟头。有一次分析一个网络协议的数据包,明明按字节读出来是 0x12345678,但解析出来数值完全不对。折腾了半天,才发现是字节序搞反了。
说白了,大端序(Big-Endian) 就是把高位字节存在低地址,小端序(Little-Endian) 则是把低位字节存在低地址。x86/x64 架构默认使用小端序,而网络协议通常使用大端序。
举个例子,数值 0x12345678 在内存中的存放方式:
| 地址 | 小端序(x86/x64) | 大端序(网络字节序) |
|---|---|---|
| 0x1000 | 0x78 | 0x12 |
| 0x1001 | 0x56 | 0x34 |
| 0x1002 | 0x34 | 0x56 |
| 0x1003 | 0x12 | 0x78 |
避坑指南: 我曾经在分析一个恶意软件样本时,它用大端序存储了加密密钥。我直接用小端序去读,结果密钥全错,浪费了整整一天。后来我养成了习惯:看到多字节数据,先确认字节序。在 IDA Pro 或 Ghidra 里,可以右键选择「Data Format」来切换显示方式。
汇编语言基础:x86/x64 架构
汇编语言是逆向工程师的「母语」。你不需要像编译器那样写出完美的汇编,但必须能读懂。我个人觉得,掌握 20% 的常用指令,就能覆盖 80% 的逆向场景。
指令分类速览
- 数据传送:
mov、push、pop、lea—— 搬数据 - 算术运算:
add、sub、mul、div、inc、dec—— 算数 - 逻辑运算:
and、or、xor、not、shl、shr—— 位操作 - 控制转移:
jmp、jz、jnz、call、ret—— 改变执行流 - 比较与测试:
cmp、test—— 设置标志位
你想想看,一个函数调用在汇编层面是什么样的?
; 调用前:参数压栈(x86 调用约定)
push arg3
push arg2
push arg1
call 函数地址
; 函数内部:
push ebp ; 保存旧基址
mov ebp, esp ; 设置新基址
sub esp, 0x10 ; 分配局部变量空间
; ... 函数体 ...
mov esp, ebp ; 恢复栈指针
pop ebp ; 恢复旧基址
ret ; 返回
这段代码我闭着眼都能写出来。为什么?因为逆向时天天见。你只要记住:call 会把返回地址压栈,ret 会弹出返回地址并跳转。这是理解函数调用栈的关键。
x64 与 x86 的主要差异
从 x86 到 x64,不只是寄存器变宽了那么简单。有几个关键点:
- 寄存器数量翻倍: 多了 R8-R15,参数传递更灵活
- 调用约定变了: x64 下,前四个参数用 RCX、RDX、R8、R9 传递(Windows),而不是全部压栈
- 地址空间更大: 可以访问超过 4GB 的内存
- 相对寻址更常见: x64 常用 RIP 相对寻址,比如
lea rax, [rip+0x1234]
我的经验: 分析 x64 程序时,我习惯先看函数开头有没有 sub rsp, xxx 来分配「影子空间」(Shadow Space)。这是 x64 调用约定的一个特征,能帮你快速识别函数边界。
知识体系总览
下面这张图,是我自己梳理的「二进制逆向基础」知识框架。每次带新人,我都会先让他们看这张图,建立全局观。
实战:用汇编思维看代码
光说不练假把式。我们来看一段简单的 C 代码,然后把它编译成汇编,看看底层发生了什么。
// C 代码
int add(int a, int b) {
return a + b;
}
int main() {
int result = add(3, 4);
return result;
}
编译成 x86 汇编后(简化版):
; add 函数
push ebp
mov ebp, esp
mov eax, [ebp+8] ; 取参数 a
add eax, [ebp+12] ; 加参数 b
pop ebp
ret
; main 函数
push 4 ; 压入第二个参数
push 3 ; 压入第一个参数
call add ; 调用 add
add esp, 8 ; 清理栈(调用者清理)
; 此时 eax 中就是返回值 7
; ... 后续代码 ...
看到了吗?add(3, 4) 这个简单的调用,在汇编层面涉及了栈操作、参数传递、函数调用和返回值处理。逆向分析时,你看到的就是这些指令。我建议你养成一个习惯:看到高级语言代码,试着在脑子里翻译成汇编。久而久之,你就能「看穿」二进制了。
本章核心要点:
- CPU + 内存 + 寄存器 = 程序运行的铁三角
- x86/x64 默认小端序,网络协议常用大端序,逆向时务必确认
- 掌握 20% 的常用汇编指令(mov、push、pop、call、ret、jmp、cmp 等)就能应对大部分场景
- x64 相比 x86,寄存器更多、调用约定不同、支持 RIP 相对寻址