二进制基础回顾:计算机体系结构基础

说实话,每次带新人入门逆向,我都要先问一句:「你清楚程序在机器里到底是怎么跑的吗?」很多人能写出不错的代码,但一说到 CPU 怎么取指令、内存怎么布局、数据怎么存,就开始含糊了。这一章,我们就来把这些基础夯实。我个人习惯把计算机体系结构比作一个「车间」——CPU 是工人,内存是工作台,寄存器是工人手里的工具。搞懂这三者的配合,逆向的大门才算真正推开。

CPU、内存与寄存器:铁三角

CPU 负责执行指令,内存负责存储指令和数据,寄存器则是 CPU 内部的高速缓存单元。这三者通过总线连接,构成了程序运行的基础。

核心理解: 程序在运行前,代码和数据都躺在内存里。CPU 从内存取指令,放到寄存器里运算,结果再写回内存。逆向分析时,你看到的反汇编代码,就是 CPU 视角下的「操作清单」。

我在项目中遇到过不少新手,一上来就盯着反汇编代码看,却搞不清当前操作的是寄存器还是内存地址。嗯,这里要记住:寄存器是 CPU 内部的,访问速度极快;内存是外部的,访问相对慢得多。逆向时,频繁出现的 mov eax, [ebx] 这类指令,就是典型的「从内存读数据到寄存器」。

x86/x64 寄存器一览

x86 架构有 8 个通用寄存器(32 位),x64 扩展到了 16 个(64 位)。我习惯把它们分成几类:

类别32位64位常见用途
通用寄存器EAX, EBX, ECX, EDXRAX, RBX, RCX, RDX算术运算、函数返回值(EAX/RAX)
索引寄存器ESI, EDIRSI, RDI字符串操作、内存拷贝源/目标
栈指针ESPRSP指向当前栈顶
基址指针EBPRBP指向当前栈帧基址
指令指针EIPRIP指向下一条要执行的指令

我的小技巧: 逆向时,我通常会先关注 EAX/RAX(返回值)、ESP/RSP(栈操作)和 EIP/RIP(控制流)。这三个寄存器能帮你快速定位函数调用和跳转逻辑。

数据在内存中的表示:大端序与小端序

这个问题,我当年刚入行时栽过跟头。有一次分析一个网络协议的数据包,明明按字节读出来是 0x12345678,但解析出来数值完全不对。折腾了半天,才发现是字节序搞反了。

说白了,大端序(Big-Endian) 就是把高位字节存在低地址,小端序(Little-Endian) 则是把低位字节存在低地址。x86/x64 架构默认使用小端序,而网络协议通常使用大端序。

举个例子,数值 0x12345678 在内存中的存放方式:

地址小端序(x86/x64)大端序(网络字节序)
0x10000x780x12
0x10010x560x34
0x10020x340x56
0x10030x120x78

避坑指南: 我曾经在分析一个恶意软件样本时,它用大端序存储了加密密钥。我直接用小端序去读,结果密钥全错,浪费了整整一天。后来我养成了习惯:看到多字节数据,先确认字节序。在 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 调用约定的一个特征,能帮你快速识别函数边界。

知识体系总览

下面这张图,是我自己梳理的「二进制逆向基础」知识框架。每次带新人,我都会先让他们看这张图,建立全局观。

二进制逆向基础 · 知识体系 CPU 体系结构 内存与数据表示 汇编语言基础 寄存器 指令流水线 大端序/小端序 栈与堆 指令分类 调用约定 💡 逆向分析 = 理解 CPU 如何执行指令 + 数据如何在内存中流动 掌握这三块,你就能读懂反汇编代码,定位关键逻辑

实战:用汇编思维看代码

光说不练假把式。我们来看一段简单的 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 相对寻址