第二章:计算机基础与汇编入门

说实话,很多刚入行的朋友问我:“逆向工程是不是就是对着二进制看?”我通常会反问一句:“你知道你写的代码在CPU眼里长什么样吗?”

这一章,我们就来聊聊计算机最底层的那些事。CPU怎么干活、内存怎么布局、寄存器是干嘛的。这些东西,说白了就是逆向工程的“内功心法”。

核心观点:不懂汇编的逆向工程师,就像不懂语法的翻译官。你可以靠猜,但永远成不了高手。

2.1 计算机体系结构:CPU、内存、寄存器

先看一张图,这是我个人习惯用来给学生讲“计算机怎么跑起来”的框架。

计算机体系结构核心组件 CPU(中央处理器) 控制单元 算术逻辑单元(ALU) 寄存器组 内存(RAM) 代码段 数据段 栈区 / 堆区 数据总线 / 地址总线 寄存器(CPU内部的高速存储单元) 通用寄存器(EAX, EBX, ECX, EDX...) 指针寄存器(ESP, EBP) 指令寄存器(EIP) 特点:容量极小(几十字节),速度极快(1个时钟周期)

这张图我每次讲课都会画一遍。你想想看,CPU从内存拿指令,放到寄存器里算,算完再写回内存。就这么简单的一个循环,支撑起了整个计算机世界。

2.1.1 CPU:大脑

CPU就是计算机的大脑。它负责取指令、解码、执行。我在做漏洞分析时,经常需要单步跟踪CPU的执行流,看它到底走了哪条分支。

CPU内部有三个关键部件:

  • 控制单元:负责取指令、解码、调度。说白了就是“指挥官”。
  • ALU(算术逻辑单元):负责加减乘除、与或非等运算。这是“干活的人”。
  • 寄存器:CPU内部的高速缓存。容量极小,但速度极快。

我的经验:调试恶意软件时,我经常盯着EIP(指令指针寄存器)看。它指向哪,CPU就执行哪。一旦EIP被劫持,程序就失控了。这就是缓冲区溢出攻击的核心原理。

2.1.2 内存:仓库

内存是程序的“工作台”。程序运行时的代码、数据、栈、堆全在内存里。x86架构下,内存是线性排列的字节序列,每个字节都有一个唯一的地址。

内存布局大致如下:

区域用途特点
代码段(.text)存放机器指令只读,不可写
数据段(.data/.bss)全局变量、静态变量可读写
堆(Heap)动态分配的内存手动管理,增长方向向上
栈(Stack)局部变量、函数调用自动管理,增长方向向下

嗯,这里要注意:栈是向下增长的。每次调用函数,栈顶指针(ESP)会减小。我第一次看反汇编时,看到ESP不断减小,还以为程序出bug了。后来才明白,这就是栈的工作原理。

2.1.3 寄存器:CPU的便签纸

寄存器是CPU内部的小块存储。你可以把它想象成你手边的便签纸——随手记、随手用,比翻笔记本(内存)快得多。

x86架构下,主要的通用寄存器有8个:

寄存器32位名16位名8位名常见用途
累加器EAXAXAL/AH算术运算、函数返回值
基址寄存器EBXBXBL/BH数据寻址
计数器ECXCXCL/CH循环计数、移位
数据寄存器EDXDXDL/DHI/O操作、乘除辅助
源变址ESISI-字符串操作源地址
目的变址EDIDI-字符串操作目标地址
栈指针ESPSP-指向栈顶
基址指针EBPBP-指向栈帧基址

避坑指南:我曾经在分析一个加壳程序时,发现EAX的值总是不对。后来才意识到,壳代码修改了EAX作为解密密钥。如果你在调试时发现寄存器值“莫名其妙”,先想想是不是有代码在背后动了手脚。

2.2 x86/x64汇编基础

汇编语言,说白了就是机器指令的“助记符”。每条汇编指令都对应一条机器码。逆向工程的核心工作之一,就是把汇编代码“翻译”回高级语言的逻辑。

2.2.1 常见指令

我整理了一份“逆向工程必会指令表”,这些指令你几乎每天都会遇到:

指令格式作用示例
MOVMOV dst, src数据传送MOV EAX, 0x10
ADD/SUBADD dst, src加减运算ADD EAX, EBX
PUSH/POPPUSH src / POP dst压栈/出栈PUSH EAX
CALL/RETCALL addr / RET函数调用/返回CALL 0x401000
JMP/JccJMP addr / JE addr无条件/条件跳转JE 0x401020
CMP/TESTCMP a, b / TEST a, b比较/测试CMP EAX, 0
LEALEA dst, [addr]加载有效地址LEA EAX, [EBP-4]

我个人习惯把指令分成三类:数据搬运、算术逻辑、控制流。你只要掌握了这三类,大部分汇编代码都能看懂。

2.2.2 栈操作:函数调用的幕后英雄

栈是逆向工程中最重要的数据结构之一。每次函数调用,都会在栈上创建一个“栈帧”。

一个典型的函数调用过程是这样的:

  1. 调用者把参数压栈(PUSH)
  2. 调用CALL指令,把返回地址压栈
  3. 被调用函数保存EBP(PUSH EBP)
  4. 设置EBP = ESP(MOV EBP, ESP)
  5. 分配局部变量空间(SUB ESP, N)
  6. ... 执行函数体 ...
  7. 恢复ESP,恢复EBP,执行RET
; 一个典型的函数序言(Prologue)
push   ebp          ; 保存调用者的EBP
mov    ebp, esp     ; 设置当前函数的栈帧基址
sub    esp, 0x40    ; 分配64字节局部变量空间

; 函数体
mov    dword [ebp-4], 0x1234  ; 局部变量赋值

; 函数尾声(Epilogue)
mov    esp, ebp     ; 恢复ESP
pop    ebp          ; 恢复EBP
ret                 ; 返回

我的经验:分析恶意软件时,我经常在函数入口处下断点。看它PUSH了哪些参数,就能猜出它要调用什么API。比如看到PUSH 0; PUSH 0; CALL [MessageBoxA],就知道这是个弹窗。

2.3 汇编与高级语言的对应关系

这一节,我们来点“翻译练习”。把高级语言代码和汇编代码对照着看,你会发现很多规律。

2.3.1 if-else 语句

C语言代码:

if (x > 0) {
    y = 1;
} else {
    y = -1;
}

对应的汇编(简化版):

cmp   dword [x], 0    ; 比较 x 和 0
jle   .else_branch    ; 如果 x <= 0,跳转到else
mov   dword [y], 1    ; y = 1
jmp   .end_if
.else_branch:
mov   dword [y], -1   ; y = -1
.end_if:

看到了吗?高级语言里的if-else,在汇编里就是CMP + 条件跳转。你想想看,所有的分支逻辑,底层都是这么干的。

2.3.2 循环语句

C语言代码:

for (int i = 0; i < 10; i++) {
    sum += i;
}

对应的汇编:

mov   dword [i], 0     ; i = 0
.loop_start:
cmp   dword [i], 10    ; 比较 i 和 10
jge   .loop_end        ; 如果 i >= 10,跳出循环
mov   eax, dword [sum]
add   eax, dword [i]   ; sum += i
mov   dword [sum], eax
inc   dword [i]        ; i++
jmp   .loop_start
.loop_end:

循环的本质就是:初始化、条件判断、循环体、增量、跳回。逆向分析时,看到这种模式,基本就能确定是个循环。

2.3.3 函数调用

C语言代码:

int add(int a, int b) {
    return a + b;
}
int result = add(3, 5);

对应的汇编:

; 调用者
push  5               ; 压入第二个参数
push  3               ; 压入第一个参数
call  add             ; 调用函数
add   esp, 8          ; 清理参数(调用者清理)
mov   dword [result], eax  ; 返回值在EAX中

; 被调用函数 add
push  ebp
mov   ebp, esp
mov   eax, dword [ebp+8]   ; 取第一个参数 a
add   eax, dword [ebp+12]  ; 加第二个参数 b
pop   ebp
ret

关键点:函数返回值通常放在EAX中。参数通过栈传递(x86)或寄存器传递(x64 fastcall)。这是逆向分析时识别函数调用的重要线索。

2.4 本章小结

这一章的内容,说白了就是逆向工程的“识字课”。你得先认识CPU、内存、寄存器这些“零件”,再学会汇编指令这个“语言”,最后才能看懂程序在“说什么”。

我个人建议:学汇编不要死记硬背。打开调试器(比如x64dbg或OllyDbg),随便找个程序单步跟踪一下。看ESP怎么变、EIP怎么跳、EAX怎么算。看多了,自然就熟了。

我曾经带过一个学生,他花了一周时间,把notepad.exe从头到尾单步了一遍。虽然什么都没改,但他说:“老师,我现在看汇编就像看中文一样自然。”这就是最好的状态。


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