🌟 引言 (Prologue):破雾之旅
两周前,当第一行汇编指令映入眼帘时,那一堆枯燥的 mov、sub、jmp 仿佛是一堵密不透风的高墙。对于初学者而言,汇编最可怕的不是指令本身,而是逻辑的破碎感------原本连贯的 C 语言思维,被编译器打碎成了无数个微小的碎片。
这个阶段,本质上是一场 "破雾之旅"。
我们从最简单的 Level 1(变量赋值)出发,一路跨越了循环的陷阱、指针的跳跃,最终在 Level 8(选择排序)的战场上,完成了一次没有提示、没有源码的"盲测"。这不仅仅是技术的胜利,更是心性的磨练。您证明了:只要掌握了正确的方法论,再混乱的指令流,也能被还原成清晰的逻辑大厦。
Level 1 序列的初见 (The First Glimpse)
-
核心考点: 数组元素的直接寻址(无循环)。
-
汇编特征:
mov eax, [ebp-4](基址) ->mov ecx, [eax](arr0) ->mov edx, [eax+4](arr1)。 -
任务: 还原简单的数组计算,例如
int sum = arr[0] + arr[2] - arr[1];。 -
目的: 熟悉
base+offset的硬编码 形式,确立 "偏移量 4 = 下标 1" 的直觉。
反汇编代码
c
; --- 函数序言 (标准的 VC6.0 开场) ---
push ebp
mov ebp,esp
sub esp,40h ; 开辟局部变量缓冲区
push ebx
push esi
push edi
lea edi,[ebp-40h]
mov ecx,10h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi] ; 填充 0xCC (Debug 特征)
; ================== 核心逻辑区 ==================
; [动作 1]
mov eax,dword ptr [ebp+8] ; 取出参数 arr (数组首地址) 放入 EAX
mov ecx,dword ptr [eax] ; 取出 [EAX] (即偏移为0) 的值,放入 ECX
此时,EAX是首元素地址,ECX是arr[0]的值
; [动作 2]
mov edx,dword ptr [ebp+8] ; 再次取出参数 arr 放入 EDX
add ecx,dword ptr [edx+8] ; 取出 [EDX + 8] 的值,加到 ECX 上
此时EDX是首元素地址,arr[2] + arr[0]
; [动作 3]
mov eax,dword ptr [ebp+8] ; 第三次取出参数 arr 放入 EAX
sub ecx,dword ptr [eax+4] ; 取出 [EAX + 4] 的值,从 ECX 中减去
此时EAX是首元素地址,ECX - arr[1]
最后结果:arr[0] + arr[2] - arr[1]
; [动作 4]
mov eax,ecx ; 将最终结果 ECX 放入 EAX (作为返回值)
; ================== 核心逻辑结束 ==================
; --- 函数尾声 (恢复现场) ---
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
具体代码
c
int Level1(int* arr)
{
// 汇编逻辑:arr[0] + arr[2] - arr[1]
return arr[0] + arr[2] - arr[1];
}
Level 2 指针的游走 (The Wandering Pointer)
-
核心考点: 指针递增 vs 下标访问。
-
汇编特征: 看不到
[base + ecx*4],而是看到指针本身的移动:add eax, 4(指针后移) ->mov edx, [eax](取值)。 -
任务: 还原使用指针遍历或访问数组的代码
*(p++)。 -
目的: 区分 C 语言中
arr[i]和*p在汇编层面的异同。
反汇编代码
c
; --- 核心逻辑区 ---
; [阶段 A]
mov eax, dword ptr [ebp+8] ; 1. 拿到数组首地址 -> EAX
mov ecx, dword ptr [eax] ; 2. 取出当前 EAX 指向的值,放入 ECX (累加器)
; [阶段 B]
add eax, 4 ; 3. 【关键】EAX 自己增加了 4!
add ecx, dword ptr [eax] ; 4. 取出 **新** EAX 指向的值,加到 ECX 上
; [阶段 C]
add eax, 4 ; 5. 【关键】EAX 又增加了 4!
add ecx, dword ptr [eax] ; 6. 取出 **新** EAX 指向的值,加到 ECX 上
mov eax, ecx ; 7. 返回结果
具体代码
c
int Level2(int* arr)
{
int* p = arr; // 定义一个指针指向数组开头
int sum = *p; // 取第1个
p++; // 指针后移 (对应 add eax, 4)
sum += *p; // 取第2个
p++; // 指针再后移
sum += *p; // 取第3个
return sum;
}
Level 3 循环的巡礼 (The Loop of Traversal)
-
核心考点: 数组遍历(读操作)。
-
汇编特征: 标准的
[esi + ecx*4]配合inc ecx和cmp ecx, count。 -
任务: 还原一个遍历数组求和、求平均值或打印数组的逻辑。
-
目的: 这是逆向中最常见的模式,必须做到"一眼秒杀"。
反汇编代码
bash
; --- 初始化 ---
mov dword ptr [ebp-4], 0 ; sum = 0
xor ecx, ecx ; ecx = 0 (这就是 i = 0 的常见写法,异或自己等于0)
; --- 循环入口 (Label) ---
SHORT_LOOP_START:
; [1. 边界检查]
cmp ecx, dword ptr [ebp+0xC] ; 比较 ecx (i) 和 count
jge SHORT_LOOP_END ; 如果 i >= count,跳出循环
; [2. 核心逻辑]
mov eax, dword ptr [ebp+8] ; eax = arr 基址
mov edx, dword ptr [eax + ecx*4] ; edx = arr[i] (注意这种寻址!)
add dword ptr [ebp-4], edx ; sum += arr[i]
; [3. 步进]
inc ecx ; i++ (inc 是 add x, 1 的简写)
jmp SHORT_LOOP_START ; 【关键】强制跳回开头
; --- 循环结束 ---
SHORT_LOOP_END:
mov eax, dword ptr [ebp-4] ; 返回 sum
具体代码
c
int level3(int* arr, int count)
{
int sum = 0;
for(int i = 0; i < count; i++)
{
sum += arr[i];
}
return sum;
}
Level 4 守门人的筛选 (The Filter of The Gate)
-
核心考点: 遍历 + 条件判断(If in Loop)。
-
汇编特征: 在循环内部出现
test eax, 1(判断奇偶) 或cmp eax, 0,随后有 jz/jnz 跳过处理逻辑。 -
任务: 还原"统计数组中偶数的个数"或"查找数组中是否存在 -1"的代码。
-
目的: 训练在数组遍历中识别"过滤逻辑"。
反汇编代码
c
push ebp
mov ebp, esp
sub esp, 0x8
核心参数:
ebp + 8 -> count
ebp + 0xC -> arr
--------------------------
初始化变量
---------------------------
mov dword ptr [ebp-4], 0
mov dword ptr [ebp-8], 0
SHORT_TAG_A:
暂时可知[ebp - 8]的值跟[ebp + 0xC]进行比较
---------------------------
mov eax, dword ptr [ebp-8]
cmp eax, dword ptr [ebp+0xC]
jge SHORT_TAG_B
根据偏移量来看ebp + 8是数组首元素地址
由下面分析可得[ebp - 8]是计数器
那么这段代码意思应该就是数组首元素 + 偏移量 -> arr[i]
---------------------------------
mov ecx, dword ptr [ebp+8]
mov edx, dword ptr [ebp-8]
mov eax, dword ptr [ecx + edx*4]
eax是arr[i]的值
如果eax是0直接跳入累加区
---------------------------
cmp eax, 0
jne SHORT_TAG_C
如果eax==0,取出[ebp - 0x4]
[ebp - 0x4]++
sum++;
-------------------------
mov eax, dword ptr [ebp-4]
add eax, 1
mov dword ptr [ebp-4], eax
SHORT_TAG_C:
[ebp - 0x8]++
由此可得[ebp - 0x8]是一个计数器
----------------------------
mov eax, dword ptr [ebp-8]
add eax, 1
mov dword ptr [ebp-8], eax
jmp SHORT_TAG_A
SHORT_TAG_B:
返回[ebp - 0x4]
mov eax, dword ptr [ebp-4]
mov esp, ebp
pop ebp
ret
具体代码
c
int Level4(int* arr, int count)
{
// [ebp-4]
int result = 0;
// [ebp-8]
for (int i = 0; i < count; i++)
{
// 核心过滤逻辑:统计 0 的个数
if (arr[i] == 0)
{
result++;
}
}
return result;
}
Level 5 镜像的重塑 (The Reshaping Mirror)
-
核心考点: 数组修改(写操作)。
-
汇编特征: 不仅有
mov reg, [mem](读),还有mov [mem], reg(写)。 -
任务: 还原 "数组元素取反" 、"数组元素统一加 1" 或 "简单的加密(异或)" 逻辑。
-
目的: 从 "观察者" 变成 "修改者",理解数据是如何被原位改变的。
反汇编代码
c
push ebp
mov ebp, esp
push esi
mov ecx, 0
SHORT_TAG_START:
//[ebp + 0xC]取出来跟0进行比较
cmp ecx, dword ptr [ebp+0xC]
//大于等于则跳转到集合点
jge SHORT_TAG_END
//取出[ebp + 0x8]放入eax中
mov eax, dword ptr [ebp+8]
//已知ecx->0,那么这里应该是取出arr[0],索引ebp+0x8是arr
mov edx, dword ptr [eax + ecx*4]
//这里有个是对取出来的值进行加密
xor edx, 0xFF
//将加密的数据放回数组中 -> arr[i] ^= 0xFF
mov eax, dword ptr [ebp+8]
mov dword ptr [eax + ecx*4], edx
//根据这个inc,能判断ecx应该是计数器
//那么也就能判断ebp + 0xC是count,也就是数组本身个数
inc ecx
//这里又进行了跳转,cmp + jmp的组合是循环核心特征
jmp SHORT_TAG_START
SHORT_TAG_END:
pop esi
mov esp, ebp
pop ebp
ret
具体代码
c
int level5(int* arr, int count)
{
for(int i = 0; i < count; i++)
{
arr[i] ^= 0xFF
}
}
Level 6 跨步的追踪 (The Stride Tracking)
-
核心考点: 非连续访问 / 复杂下标。
-
汇编特征: 循环计数器
i每次add i, 2,或者访问arr[i*2]。 -
任务: 还原 "只处理数组偶数位下标" 或 "隔位采样" 的逻辑。
-
目的: 打破 "挨个遍历" 的惯性思维,适应更灵活的内存访问模式。
反汇编代码
c
push ebp
mov ebp, esp
sub esp, 0x8
//两个变量先确定
mov dword ptr [ebp-4], 0
//计数器
mov dword ptr [ebp-8], 0
SHORT_LABEL_X:
//提取[ebp - 0x8]放入eax中
mov eax, dword ptr [ebp-8]
//[ebp - 0x8]跟[ebp + 0xC]做比较
//从之前练习中来看这种比较极有可能确定[ebp - 0x8]是计数器
//[ebp + 0xC]是数组个数
cmp eax, dword ptr [ebp+0xC]
jge SHORT_LABEL_Y
//[ebp + 8]是首元素地址
//这段代码意思是提取arr[i]
mov ecx, dword ptr [ebp+8]
mov edx, dword ptr [ebp-8]
mov eax, dword ptr [ecx + edx*4]
//放入到变量中,结合循环语法应该是把遍历到的值放入变量中
add dword ptr [ebp-4], eax
//提取i,然后 i += 2,说明一次走两格
mov eax, dword ptr [ebp-8]
add eax, 2
mov dword ptr [ebp-8], eax
jmp SHORT_LABEL_X
SHORT_LABEL_Y:
mov eax, dword ptr [ebp-4]
mov esp, ebp
pop ebp
ret
具体代码
c
int level6(int* arr, int count)
{
int sum = 0;
for(int i = 0; i < count; i+=2)
{
sum += arr[i]
}
return sum;
}
Level 7 双针的博弈 (The Duel of Two Pointers)
-
核心考点: 双变量控制(同层循环)。
-
汇编特征: 一个循环里有两个变化的变量,例如
eax(左指针) 往右走,ebx(右指针) 往左走,直到相遇。 -
任务: 还原 "数组逆序 (Reverse Array)" 的逻辑(首尾交换)。
-
目的: 这是算法题的雏形,考察对多寄存器状态的同步跟踪。
反汇编代码
c
push ebp
mov ebp, esp
sub esp, 0x8
push esi
push edi
//变量i
mov dword ptr [ebp-4], 0
//提取[ebp + 0xC]放入寄存器eax中,然后 - 1
mov eax, dword ptr [ebp+0xC]
sub eax, 1
//然后放入[ebp - 0x8]中 -> [ebp - 8] = [ebp + 0xC] - 1
mov dword ptr [ebp-8], eax
SHORT_TAG_Start:
//提取[ebp - 0x4]
mov eax, dword ptr [ebp-4]
//[ebp - 0x4] 跟 [ebp - 0x8]比较
cmp eax, dword ptr [ebp-8]
//如果大于等于[ebp - 0x8]则跳转至集合点
//从这里来看说明是存在两个变量进行判断
//初步判断是左右下标,[ebp - 0x4]是左下标,[ebp - 0x8]是右下标
jge SHORT_TAG_End
//这里出现了新的参数,[ebp + 0x8] -> ecx
mov ecx, dword ptr [ebp+8]
//[ebp - 0x4] -> 之前说过的左下标值
mov edx, dword ptr [ebp-4]
//偏移量,说明[ebp + 0x8]是首元素地址,esi存储最左边的数据
mov esi, dword ptr [ecx + edx*4]
//整理思路
//[ebp + 0xC] -> count
//[ebp + 0x8] -> arr
//[ebp - 0x4] -> left
//[ebp - 0x8] -> right
//首元素地址放入ecx
mov ecx, dword ptr [ebp+8]
//右指针放入edi中
mov edi, dword ptr [ebp-8]
//偏移量应该是右边的数据,放入eax
mov eax, dword ptr [ecx + edi*4]
//首元素放入ecx
mov ecx, dword ptr [ebp+8]
//左指针i放入edx中
mov edx, dword ptr [ebp-4]
//将右边数据放入左边中
mov dword ptr [ecx + edx*4], eax
//首元素地址重新放入ecx中
mov ecx, dword ptr [ebp+8]
//右指针j放入edi中
mov edi, dword ptr [ebp-8]
//esi是左边数据赋值给最右边的地址背后的数据
mov dword ptr [ecx + edi*4], esi
//总结这里核心代码应该是三杯水交换原则
//i++
inc dword ptr [ebp-4]
//j--
dec dword ptr [ebp-8]
//跳转标签头
jmp SHORT_TAG_Start
SHORT_TAG_End:
pop edi
pop esi
mov esp, ebp
pop ebp
ret
具体代码
c
void Level7(int* arr, int count)
{
// [ebp-4] -> left
int left = 0;
// [ebp-8] -> right
int right = count - 1;
// 汇编里的 jge End (>= 就跳走) 等价于 while (left < right)
while (left < right)
{
// --- 三杯水交换 (利用寄存器做临时杯子) ---
// esi = arr[left]
// eax = arr[right]
int temp = arr[left]; // 汇编中用 esi 暂存
arr[left] = arr[right]; // 将 eax 写入左边
arr[right] = temp; // 将 esi 写入右边
// --- 步进 ---
left++; // inc
right--; // dec
}
}
Level 8 秩序的终焉 (The Final Boss: Selection Sort)
-
核心考点: 双层循环 + 最值查找 + 交换(综合考核)。
-
汇编特征:
-
外层循环控制"起始位置"。
-
内层循环只找最小值的下标(这是它和冒泡排序最大的区别)。
-
交换发生在内层循环结束后,外层循环结束前。
-
-
任务: 给你一段 选择排序 (Selection Sort) 的反汇编,还原 C 源码。
-
目的: 阶段性大考。你能否区分出"冒泡(边跑边换)"和"选择(跑完再换)"的区别?这将是你数组逆向能力的毕业证。
反汇编代码

具体代码
c
void Level8(int* arr, int count) {
// 1. 初始化 i
for (int i = 0; i < count - 1; i++) {
// 2. 设定 min_idx
int min_idx = i;
// 3. 内层循环 j
for (int j = i + 1; j < count; j++) {
// 4. 比较与更新
if (arr[j] < arr[min_idx]) {
min_idx = j;
}
}
// 5. 交换逻辑 (这里简化,不管是不是自己都交换)
swap(arr[i], arr[min_idx]);
}
}
9. 斐波那契额数列

具体代码

核心纠错_1
-
案例一:
-
写法:eax + esi\*4 - 4
-
是否合法:✅ 合法
-
原因:地址=Base (基址寄存器)+(Index (变址寄存器)×Scale (1, 2, 4, 8))+Displacement (常量偏移)\text{地址} = \text{Base (基址寄存器)} + (\text{Index (变址寄存器)} \times \text{Scale (1, 2, 4, 8)}) + \text{Displacement (常量偏移)}地址=Base (基址寄存器)+(Index (变址寄存器)×Scale (1, 2, 4, 8))+Displacement (常量偏移)
-
-
案例二:
-
写法:eax - esi\*4
-
是否合法: ❌ 非法
-
原因: 括号内不能减寄存器,
-
替代方案: 先 neg esi 或 sub 算好
-
-
案例三:
-
写法: "add ecx, esi*4",
-
是否合法: ❌ 非法,
-
原因: ADD 指令不支持乘法操作数,
-
替代方案: "用 lea ecx, ecx + esi\*4"
-
-
案例四:
-
写法: "mov eax, esi*4",
-
是否合法: ❌ 非法,
-
原因: MOV 指令不支持乘法操作数,
-
替代方案: "用 lea eax, esi\*4"
-
核心纠错_2
写法,是否合法,为什么?,解析视角
-
案例一:
-
写法: eax - 4,
-
是否合法: ✅ 合法,
-
为什么: 减的是常数,
-
解析视角: CPU 视为 EAX + (-4)
-
-
案例二:
-
写法: eax - 0x100,
-
是否合法: ✅ 合法,
-
为什么: 减的是常数,
-
解析视角: CPU 视为 EAX + (-0x100)
-
-
案例三:
-
写法: eax - esi,
-
是否合法: ❌ 非法,
-
为什么: 减的是寄存器,
-
解析视角: 硬件不支持 Base - Index
-
-
案例四:
-
写法: eax - esi \* 4,
-
是否合法: ❌ 非法,
-
为什么: 减的是寄存器,
-
解析视角: 硬件不支持 Base - (Index*Scale)
-
-
案例五:
-
写法: eax + esi \* -4,
-
是否合法: ❌ 非法,
-
为什么: 比例因子不能为负,
-
解析视角: "Scale 只能是 1, 2, 4, 8"
-
🧠 核心收获 (Knowledge Points)
在这个阶段,我们不仅学会了"术"(指令),更修炼了"道"(思维)。
-
硬核技术 (Hard Skills)
-
数组寻址公式 (The Golden Rule): 刻入肌肉记忆的核心特征:
-
Address=Base+Index×4Address = Base + Index \times 4Address=Base+Index×4
-
只要看到
寄存器乘以 4([ecx + edx*4]),就能瞬间识别出这是int 数组的访问。
-
-
流程控制的识别: 能够透过
cmp和jcc(如jge,jl) 的组合,精准识别出for 循环、while 循环以及if-else分支结构。 -
双层循环模型: 攻克了逆向新手的噩梦------嵌套循环。学会了区分 外层计数器(i) 和 内层计数器(j) 的边界与交互。
-
栈帧布局 (Stack Layout): 习惯了
ebp-4,ebp-8这种局部变量的表示法,并能通过初始化代码(sub esp, 0x40)反推变量空间的大小。
-
-
逆向心法 (Mindset & Methodology)
-
抗干扰分析 (Anti-Biased Analysis): 这是本阶段最大的亮点。 学会了在证据不足时,不给变量乱起名字(如
temp),而是用客观的代号(如[ebp-8])跟踪到底,直到逻辑闭环。 -
路标重命名 (Label Renaming): 克服了对
LABEL_ALPHA这种无意义标签的恐惧,学会了主动将其修改为Outer_Loop_Start等具有语义的名字,从而掌控代码结构。 -
宏观指挥官思维: 从"逐行阅读"进化为"三步走战略":
-
定参数(看栈帧输入);
-
搭骨架(看跳转结构);
-
攻核心(推导数据流向)。
-
-
🖋️ 结语 (Epilogue):见山又是山
在逆向工程的学习中,有三重境界:
-
看山是山:看 C 语言源码,觉得很简单。
-
看山不是山:看汇编代码,觉得全是乱码,迷失在寄存器里。
-
见山又是山:看着汇编代码,脑海中却浮现出 C 语言的结构。
此刻的您,已经站在了第三重境界的门口。
虽然 Level 8 的选择排序曾让您感到"顾头不顾尾",但您最终通过手写汇编、重构逻辑,彻底征服了它。这种**"死磕"**到底的精神,比掌握任何一条具体的指令都更珍贵。
数组篇已过,地基已成。 您不再是那个对着代码发愁的新手,而是一位能够冷静拆解逻辑的分析师。请带着这份自信和扎实的"内功",准备迎接下一章更复杂的挑战------结构体。
"凡我不能创造的,我就不能理解。"您已经创造了它,所以您已经掌握了它。