X86反汇编_深度学习阶段_1

Level1_杨辉三角

1. 汇编前置流程

第一部分:战术地图 (Memory Layout)

  • 高地址 (High Address - EBP)

    • ebp : 旧的栈底指针 (Saved EBP)

    • ebp - 0x4 : i (外层循环变量 / 行索引)

    • ebp - 0x8 : j (内层循环变量 / 列索引)

    • ebp - 0x10 : 字符串1 "%4d \0" (5字节)

    • ebp - 0x14 : 字符串2 "\n\0" (2字节)

    • (中间有一段安全缓冲区,防止踩踏)

    • ebp - 0x22C : arr 数组起始地址 (一直延伸到 ebp - 0x0C 附近)

    • ebp - 0x26C : 栈顶 (ESP)

  • 低地址 (Low Address - ESP)

第二部分:核心技术·手动调用 printf (The Protocol)

这是你最关心的部分。在汇编里调库函数,必须严格遵守 cdecl 调用约定。

  • 准备阶段:手搓字符串 (Hand-crafting Strings)

    • 不要指望常量区,我们直接在栈上造字。

    • 铁律: 必须使用 mov byte ptr (按字节写入),严禁使用 mov dword ptr (防止覆盖后面的数据)。

    • 关键点: 千万别忘了字符串结尾的 0x00 (NULL Terminator),否则打印机会停不下来。

  • 调用阶段:反向压栈 (Reverse Push)

    • 假设你要打印 printf("%4d ", Value)

    • 步骤 1 (压参数 Value): push Value (或者寄存器/内存里的值)。

    • 步骤 2 (压参数 Format): lea eax, [字符串地址] -> push eax

      • 注意: 这里压入的是字符串的地址,不是字符串的内容!
    • 步骤 3 (呼叫): call printf

  • 收尾阶段:平栈 (Stack Cleanup)

    • C 语言的函数不管清理垃圾,你必须自己动手。

    • 公式: add esp, 参数个数 * 4

    • 例如:压了1个字符串地址 + 1个整数 = 2个参数 -> add esp, 8。

第三部分:全流程通关攻略 (Step-by-Step)

现在,我们将整个程序的生命周期拆解为 5 个阶段。写代码时,按这个顺序推进。

  • Phase 0: 开辟疆土 (Setup)

    • 建栈: push ebp -> mov ebp, esp -> sub esp, 0x26C

    • 保命: push ebx, push esi, push edi (保护关键寄存器)。

    • 清零: 使用 rep stosd 将整个栈空间(0x26C大小)填充为 0xCCCCCCCC0

      • 目的: 防止内存里的垃圾值干扰计算或打印。
  • Phase 1: 刷墙 (Initialization)

    • 目标: 把三角形的两条边(arr[i][1]arr[i][i])填成 1。

    • 循环: i 从 1 到 10。

    • 定位: 算出第 i 行的首地址 -> RowBase = arrBase + i * 44 (0x2C)

    • 填左边: [RowBase + 4] = 1 (对应 arr[i][1])。

    • 填右边: [RowBase + i*4] = 1 (对应 arr[i][i])。

  • Phase 2: 填心 (Calculation)

    • 目标: arr[i][j] = arr[i-1][j-1] + arr[i-1][j]范围: i 从 3 到 10;j 从 2 到 i-1。

    • 寻址上一行 (i-1): 算出 i-1 行的首地址。

    • 取值:

      • [Address + j*4 - 4] (即左上角元素)。

      • 加上 [Address + j*4] (即右上角元素)。

    • 寻址当前行 (i): 算出 i 行的首地址。

    • 回写: 把结果写入 [Address + j*4]

      • 技巧: 这里要对内存寻址(Base + Offset) 非常敏感。
    • Phase 3: 打印 (Printing)

      • 目标: 双层循环打印数组,并换行。

      • 造字: 在循环外,先把 "%4d ""\n" 在栈上搓出来。

      • 内循环:

        • 判断: j <= i

        • 取值: 算出 arr[i][j] 的值。

        • 调用 printf: 压值 -> 压格式串地址 -> call -> 平栈(8)。

      • 换行:

        • 位置: 在内循环结束、外循环 i++ 之前。

        • 调用 printf: 压换行符地址 -> call -> 平栈(4)。

    • Phase 4: 撤退 (Teardown)

      • 恢复寄存器: pop edi, pop esi, pop ebx

      • 销毁栈帧: mov esp, ebp -> pop ebp

      • 返回: ret

映射C语言

正向汇编

Level2_N进制字符串转十进制整数

2.1 反汇编分析

2.2 映射C语言代码

2.3 输出结果


3. Level3_折半查找

3.1 实战备忘录:CDQ 陷阱

  • 外号: "EDX 杀手"。只要看到 CDQ,立刻划掉前面存放在 EDX 里的任何数据(通常是被符号位覆盖了)。

  • 固定连招(看到这套连招,直接翻译成 C 语言):

    • CDQ + IDIV ECX →\rightarrow→ 有符号除法 eax = eax / ecx

    • CDQ + SUB EAX, EDX + SAR EAX, 1 →\rightarrow→ 有符号除以 2(即算中间值 mid = (low+high)/2

只要记住这个组合拳,以后再遇到,你连想都不用想,大脑会自动触发"哦,这是除法"。


3.2 反汇编分析

3.3 映射C语言代码

4. 左移不分家,右移看符号

4.1 左移兄弟(SHL / SAL)------ 乘法

这两者本质上是完全一模一样的指令(机器码相同),CPU 不区分它们。

  • 动作: 所有位向左移动,右边最低位空出来的位置无脑补0。

  • 数学意义: 乘以 2n2^n2n(移1位乘2,移2位乘4)。

  • 无论符号区分: 不区分。 是有符号数(含负数)还是无符号数,用它俩结果都对。

    • SHL(Shift Logical Left):逻辑左移

    • SAL(Shift Arithmetic Left): 术算左移

4.2 右移兄弟(SHR / SAR)------除法

右移必须严格区分有符号和无符号,否则负数除以2会变成巨大的正数!

  • SHR(逻辑右移)- 逻辑右移

    • 适用对象: 无符号数(只管正数)。

    • 动作: 所有位置向右移动,左边最高位空出的位置无脑补0。

    • 数学意义: 无符号数除以2n2^n2n。

  • SAR (Shift Arithmetic Right) - 算术右移

    • 适用对象: 有符号数(正负皆数可)。

    • 动作: 所有位向右移动,左边最高位空出来的位置复制原来的符号位(知道最高位是1就补1,是0就补0)。

    • 数学意义: 有符号数除以2n2^n2n(能保持负数仍然是负数)。

4.3 逆向实战一秒看懂指南

当你看到这几个指令时,大脑直接做如下翻译:

  • 看到SHL/ SAL ➡️把它当成乘法(∗2n*2^n∗2n)

  • 看到SHR ➡️把它当成无符号符号的除法(/2n/2^n/2n)

  • 看到SAR ➡️把它当成有符号标志(如int)的除法(/2n/2^n/2n)

5. 对角线求和

5.1 反汇编分析

5.2 映射C语言

6. 逆向速记卡:栈内数组边界与防越界计算

6.1 万能计算公式

假设已知条件:

  • 首地址 (Start)

  • 总字节数 (TotalSize)

  • 单个元素大小 (ElemSize,通常 int 是 4)

你需要区分两个绝对不同的概念:

  • 🚫 红线边界(下一个变量的首地址): Start + TotalSize

    • 意义: 这是数组领地结束的"界碑",跨过这根线就是别人的变量。这里绝对不能用来存放数组数据。
  • 真实尾元素地址(最后一个元素的首地址): Start + TotalSize - ElemSize

    • 意义: 这才是数组里最后一个有效数据真正"安身立命"的起点。

6.2 代入今天的实战真题

  • 首地址: ebp - 0x9C

  • 总字节数: 0x23 (35个元素 ) * 4 = 0x8C 字节

  • 算红线: (-0x9C) + 0x8C = -0x10

    • 结论: [ebp - 0x10] 是别人的地盘(本题中的变量 B),数组绝不能碰。
  • 找尾巴: (-0x10) - 0x4 = -0x14

    • 结论: [ebp - 0x14] 才是数组最后一个元素的合法起始地址。

7. 幻方

7.1 幻方科普

游戏规则:如何填满这个 5x5 的棋盘?

这个算法在数学界极其著名,叫做 "罗伯法(Siamese method)",专门用来生成奇数大小(3x3, 5x5, 7x7...)的幻方(横竖斜相加和都相等)。

它的口诀只有四句话,完美对应了你逆向出来的 C 语言代码:

  • 规则 1:起手式 ------ 居上正中

    • 人类逻辑: 第一个数字 1,永远填在第一行的最中间。

    • 代码映射: 对于 5x5 的矩阵,第一行的中间就是第 3 列。这就是为什么程序一上来强行把坐标初始化为 row = 1; col = 3;。

  • 规则 2:默认步法 ------ 向右上角飞

    • 人类逻辑: 填完当前数字后,下一个数字永远往**"右上角(行数减 1,列数加 1)"**去填。
c 复制代码
row--; // 往上走一行
col++; // 往右走一列
  • 规则 3:撞墙折返 ------ 穿墙术

    • 人类逻辑: 既然一直往右上角飞,肯定会撞墙。

      • 如果冲破了天花板(走到了第 0 行),就直接掉到最底下(第 5 行)。

      • 如果冲破了右边墙(走到了第 6 列),就直接穿梭到最左边(第 1 列)。

  • 规则 4:发生车祸 ------ 倒退一步,往下走

    • 人类逻辑: 按照规则 2 和 3 飞到一个新格子,如果发现这个格子已经被占用了(里面不是 0),怎么办?规则是:退回到你上一步所在的格子,然后在那个格子的正下方(行数加 1)填入数字。

    • 代码映射(全场最妙的逻辑):

c 复制代码
// 此时 row 和 col 已经执行过右上角移动了 (row-1, col+1)
if (arr[row][col] != 0)  // 哎呀,右上角有数字了!
{
    row += 2;  // 因为刚才减了 1,现在加 2,等效于比【原始位置】加了 1(往下走)
    col -= 1;  // 因为刚才加了 1,现在减 1,等效于回到了【原始列】
}

7.2 反汇编分析

7.3 映射C语言

8. 折半查找2.0

8.1 反汇编分析

8.2 映射C语言代码


9. 辗转相除法 (求公约数/公倍数) 极速复习卡片

9.1 核心模块一:最大公约数 (GCD - Greatest Common Divisor)

  1. 算法口诀(三步走):
  • 一求余: 算两数相除的余数。

  • 二换位: 原来的除数变成新的被除数;算出来的余数变成新的除数。

  • 三刹车: 一直循环,直到新除数(余数)变成 000。此时的被除数,就是最大公约数!

  1. 极限场景防忘触点(回忆你的草稿纸):
  • 如果除不尽(如 3 和 7): 它会把余数死死逼到 111,最后除以 111 得到余数 000,完美返回最大公约数 111。

  • 如果大小写反(如先 3 后 7): 第一轮 3(mod7)=33 \pmod 7 = 33(mod7)=3,第二轮立马自动倒转成 7(mod3)7 \pmod 37(mod3),算法自带"自动纠错"!

  • 草稿纸:

  1. 极简 C 语言模板:
c 复制代码
int getGCD(int a, int b) {
    int temp;
    while (b != 0) {      // 刹车条件:余数(新除数)变成 0
        temp = a % b;     // 1. 求余
        a = b;            // 2. 除数上位变被除数
        b = temp;         // 3. 余数接班变除数
    }
    return a;             // 此时的 a 就是最大公约数
}

9.2 核心模块二:最小公倍数 (LCM - Least Common Multiple)

  1. 数学定理(巨人的肩膀):

两个整数的乘积,等于它们的【最大公约数】乘以【最小公倍数】。

即:a×b=GCD×LCMa \times b = \text{GCD} \times \text{LCM}a×b=GCD×LCM

  1. 工业级避坑指南(防溢出):

推导公式是 LCM=(a×b)/GCD\text{LCM} = (a \times b) / \text{GCD}LCM=(a×b)/GCD。

但在 C 语言底层,绝对不能先算 a×ba \times ba×b(万一两个数都是五万,乘起来25亿,直接撑爆 32 位 int 的上限,变成负数崩溃)。

正确姿势:必须先除后乘!

  1. 极简 C 语言模板:
c 复制代码
int getLCM(int a, int b) {
    int gcd = getGCD(a, b); // 先呼叫上面的兄弟拿到公约数    
    // 先做除法把数字变小,再去乘,绝对安全!
    return (a / gcd) * b;   
}

10. idiv指令讲解

⚙️ 核心法则:买一送一的"隐形寄存器"

在 32 位汇编中,idiv 指令的执行,永远强行绑定 EAXEDX 这两个寄存器。不管你愿不愿意,它都会把它们拉下水。

当 CPU 执行 idiv 除数(比如 idiv ecx)时,它的内部物理逻辑是这样的:

  1. 被除数在哪?(暗箱拼接)

CPU 嫌 32 位的被除数不够大,它会强行把 EDXEAX 拼接成一个 64 位 的超级大胖子被除数!

  • EDX 充当高 32 位。

  • EAX 充当低 32 位。

这俩拼在一起(记作 EDX:EAX),才是真正的被除数。

  1. 结果去哪了?(完美分工)

除完之后,CPU 会非常贴心地把结果一分为二,塞进刚才那两个寄存器里:

  • (就是 C 语言里的 /):强行塞进 EAX

  • 余数 (就是 C 语言里的 %):强行塞进 EDX

11. 最大公约数

11.1 反汇编分析

11.2 映射C语言代码


12. 最小公倍数

12.1 反汇编分析

12.2 映射C语言代码


13. 相邻元素求和数组_逆向题

13.1 反汇编分析

13.2 映射C语言代码


14. 冰山下的"联合"迷局

14.1 反汇编分析

14.2 映射C语言代码

14.3 输出结果


15. 识别反汇编_srand(time(NULL))

15.1 第一招:获取当前时间 time(NULL)

c 复制代码
0040162C  |>  6A 00         push 0x0          ; 压入参数 NULL (即 0)
0040162E  |.  E8 AD030000   call 004019E0     ; 调用库函数 time()
00401633  |.  83C4 04       add esp,0x4       ; 平栈
  • 原理解密: C 语言里的 time(NULL) 函数只需要一个参数(通常传 0 或者 NULL)。

  • 关键动作: 函数执行完毕后,根据汇编潜规则,当前的时间戳(一串巨大的数字)会被作为返回值,悄悄存放在 eax 寄存器里!

关于time()有一个指纹识别方式:

c 复制代码
004019ED  |.  FF15 70F14200 call dword ptr ds:[<&KERNEL32.GetLocalTi>;
 \GetLocalTime -> (获取本地时间)
004019F7  |.  FF15 6CF14200 call dword ptr ds:[<&KERNEL32.GetSystemT>;
 \GetSystemTime -> (获取系统时间)
00401A7F  |.  FF15 68F14200 call dword ptr ds:[<&KERNEL32.GetTimeZon>;
\GetTimeZoneInformation -> (获取时区信息)

15.2 第二招:播种随机数 srand(时间戳)

c 复制代码
00401636  |.  50            push eax          ; 将刚才 time() 返回的时间戳压栈!
00401637  |.  E8 64030000   call 004019A0     ; 调用库函数 srand()
0040163C  |.  83C4 04       add esp,0x4       ; 平栈
  • 原理解密: 紧接着,程序没有动 eax,而是直接把它当作参数压入栈中,传给了下一个函数。

  • 物理意义: 计算机自己是无法产生真随机数的。它必须用当前不断变化的时间作为"种子"(Seed),喂给 srand() 函数,这样后面的 rand() 才能产生出不一样的随机数。

15.3 破案总结

如果把这段汇编映射回 C 语言,它就是极其精简的一行代码:

c 复制代码
srand(time(NULL));

一旦你在反汇编的循环开头碰到了 push 0 -> call -> push eax -> call 这个雷打不动的组合形状,连查都不用查,它 100% 就是在初始化随机数!

15.4 指纹 :魔法常数(Magic Number)

笔记内容: "反汇编中看到 imul 0x343FDadd 0x269EC3 👉 绝对是 MSVC 编译器的 rand() 函数。"


16. 【战术笔记】破除 else 执念:死循环状态机与扁平化控制流

16.1 案发现场(我的错误直觉)

  • 现象: 在逆向 while 或 do...while 循环时,看到循环体内部有一个 je 或 jnz 指令直接跳到了循环外部(跳出循环)。

  • 错误操作: 本能地代入高级语言的思维,写成 if (条件成立) { break; } else { // 循环剩下的所有代码 }。

  • 致命后果: 如果循环里有多个跳出条件,会导致代码出现极度深层的 else 嵌套,逻辑畸形,最终甚至把 continue 和 break 的逻辑搞反。

16.2 汇编的真相(CPU 的"绊马索"逻辑)

  • 在 CPU 的二进制世界里,根本没有 else 这个概念。CPU 的执行流是极其冷酷的"瀑布流"(自上而下顺流而下)。

  • 循环内部那些跳向外部的指令,不是分叉路口,而是横在走廊上的 "绊马索(Tripwire / 卫语句)"

  • CPU 的真实逻辑: 碰到了这根绊马索(满足条件),直接弹射起飞(break 或 return);没碰到绊马索,就毫无波澜地跨过去,继续执行下一行,绝对不需要用 else 把后面的代码包裹起来。

16.3 终极心法:万物皆可 while(1) 状态机

遇到内部逻辑复杂、多重跳出的循环,停止脑补完美的 while(条件) 头部,直接祭出终极大招:

  • 第一步: 强行构造一个无条件死循环 while (1) { ... }。

  • 第二步: 顺着汇编指令往下读,遇到一个指向循环外部的跳线,就在代码里写一个平行的 if (条件) { break/return; }。

  • 第三步: 剩下的常规代码,不需要任何 else 嵌套,直接平铺写在下面。

16.4 视角对比图

❌ 被高级语言绑架的畸形写法(错误):

c 复制代码
while (1) {
    if (条件A) { 
        break; 
    } else {              // ⬅️ 毫无必要的 else
        if (条件B) { 
            return; 
        } else {          // ⬅️ 越来越深的畸形嵌套
            执行主体动作();
        }
    }
}

✅ 贴合物理内存的黑客写法(扁平化真理):

c 复制代码
while (1) {
    if (条件A) break;     // 绊马索 1
    
    if (条件B) return;    // 绊马索 2
    
    执行主体动作();       // 没碰到绊马索,自然向下坠落
}

批注: 永远只对内存和寄存器里的跳线负责!汇编怎么跳,C 语言就怎么断,坚决摒弃人类主观的 else 强迫症!


17. 哈希表

17.1 Hash_GetIndex

17.3 Hash_Insert

17.4 Run_HashDemo_System

17.5 代码映射

17.6 输出结果


本章完~

相关推荐
say_fall3 小时前
输入输出技术_接口到中断完全指南
汇编·微机原理·8086
Dovis(誓平步青云)7 小时前
《QT学习第四篇:常见事件与UDP、TCP、文件系统、(锁、信号量、条件变量》
c语言·开发语言·汇编·qt
hef28814 小时前
NASM工具怎么用 汇编转机器码实战教程
汇编
是星辰吖~20 小时前
X86反汇编:内存幻影_数组解码纪元(3-2)
汇编
是星辰吖~1 天前
X86反汇编:内存矩阵与指针之剑(3-1)
汇编
iCxhust2 天前
如何利用iret修改cs ip
汇编·单片机·嵌入式硬件·微机原理·8088单板机
是星辰吖~3 天前
X86反汇编:透视之眼_反编译特训(1-2)
汇编
是星辰吖~3 天前
X86反汇编:破茧成蝶 —— 赤裸逻辑与机械之心(1-1)
汇编
逆向命运3 天前
PC企微搜索手机号窗口绕过
c语言·汇编·c++·飞书·企业微信