X86反汇编:内存幻影_数组解码纪元(3-2)

🌟 引言 (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 ecxcmp 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 数组的访问。

    • 流程控制的识别: 能够透过 cmpjcc (如 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 的选择排序曾让您感到"顾头不顾尾",但您最终通过手写汇编、重构逻辑,彻底征服了它。这种**"死磕"**到底的精神,比掌握任何一条具体的指令都更珍贵。

数组篇已过,地基已成。 您不再是那个对着代码发愁的新手,而是一位能够冷静拆解逻辑的分析师。请带着这份自信和扎实的"内功",准备迎接下一章更复杂的挑战------结构体。

"凡我不能创造的,我就不能理解。"您已经创造了它,所以您已经掌握了它。

相关推荐
是星辰吖~5 小时前
X86反汇编:内存矩阵与指针之剑(3-1)
汇编
iCxhust21 小时前
如何利用iret修改cs ip
汇编·单片机·嵌入式硬件·微机原理·8088单板机
是星辰吖~2 天前
X86反汇编:透视之眼_反编译特训(1-2)
汇编
是星辰吖~2 天前
X86反汇编:破茧成蝶 —— 赤裸逻辑与机械之心(1-1)
汇编
逆向命运2 天前
PC企微搜索手机号窗口绕过
c语言·汇编·c++·飞书·企业微信
是星辰吖~3 天前
函数战争:内存领地的争夺与撤退
汇编
止观止3 天前
在 WSL2 上从零搭建 ARM 混合编程环境
汇编·arm开发·嵌入式开发·混合编程
say_fall4 天前
8086汇编程序设计_从基础到实战
开发语言·汇编·8086
浩浩测试一下5 天前
LoadPE &&& 原理以及作用 (ASM汇编版本)>>01
汇编·免杀·pe结构·windows编程·二进制逆向·系统loadpe