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大小)填充为0xCCCCCCCC或0。- 目的: 防止内存里的垃圾值干扰计算或打印。
-
-
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)
- 算法口诀(三步走):
-
一求余: 算两数相除的余数。
-
二换位: 原来的除数变成新的被除数;算出来的余数变成新的除数。
-
三刹车: 一直循环,直到新除数(余数)变成 000。此时的被除数,就是最大公约数!
- 极限场景防忘触点(回忆你的草稿纸):
-
如果除不尽(如 3 和 7): 它会把余数死死逼到 111,最后除以 111 得到余数 000,完美返回最大公约数 111。
-
如果大小写反(如先 3 后 7): 第一轮 3(mod7)=33 \pmod 7 = 33(mod7)=3,第二轮立马自动倒转成 7(mod3)7 \pmod 37(mod3),算法自带"自动纠错"!
-
草稿纸:

- 极简 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)
- 数学定理(巨人的肩膀):
两个整数的乘积,等于它们的【最大公约数】乘以【最小公倍数】。
即:a×b=GCD×LCMa \times b = \text{GCD} \times \text{LCM}a×b=GCD×LCM
- 工业级避坑指南(防溢出):
推导公式是 LCM=(a×b)/GCD\text{LCM} = (a \times b) / \text{GCD}LCM=(a×b)/GCD。
但在 C 语言底层,绝对不能先算 a×ba \times ba×b(万一两个数都是五万,乘起来25亿,直接撑爆 32 位 int 的上限,变成负数崩溃)。
正确姿势:必须先除后乘!
- 极简 C 语言模板:
c
int getLCM(int a, int b) {
int gcd = getGCD(a, b); // 先呼叫上面的兄弟拿到公约数
// 先做除法把数字变小,再去乘,绝对安全!
return (a / gcd) * b;
}
10. idiv指令讲解
⚙️ 核心法则:买一送一的"隐形寄存器"
在 32 位汇编中,idiv 指令的执行,永远强行绑定 EAX 和 EDX 这两个寄存器。不管你愿不愿意,它都会把它们拉下水。
当 CPU 执行 idiv 除数(比如 idiv ecx)时,它的内部物理逻辑是这样的:
- 被除数在哪?(暗箱拼接)
CPU 嫌 32 位的被除数不够大,它会强行把 EDX 和 EAX 拼接成一个 64 位 的超级大胖子被除数!
-
EDX充当高 32 位。 -
EAX充当低 32 位。
这俩拼在一起(记作 EDX:EAX),才是真正的被除数。
- 结果去哪了?(完美分工)
除完之后,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 0x343FD 和 add 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.2 Hash_search

17.3 Hash_Insert

17.4 Run_HashDemo_System

17.5 代码映射

17.6 输出结果

本章完~