-
开辟战场(序言 Prologue)
-
有序撤退(结语 Epilogue)
-
实战避坑指南(我的错题本)
引言
逆向工程的本质,是在废墟中重建秩序。
我们面对的二进制代码,往往丢失了所有的变量名和注释,就像一座被炸毁的城市。而 '函数栈帧' ,就是这座城市地下的地基与骨架。如果不理解它是如何被 创建(Create)和销毁(Destroy) 的,我们就永远只能在代码的表层游荡。
我将这一章的学习称为 '函数战争',因为每一次堆栈的开辟,都是程序在向操作系统索取资源;而每一次完美的平衡,都是为了确保程序能活着回到起点。这篇笔记,是我试图解构这一微观世界秩序的尝试。"
1. 开辟战场(序言 Prologue)
1.1 立下军旗 (保存旧世界)
-
push ebp:把上一层函数的基址存起来。 -
mov ebp, esp:插旗! 从现在开始,EBP 就是我的地盘锚点,雷打不动。
1.2 挖掘战壕 (分配空间)
sub esp, 40h:往低地址扩地盘。心得:这里的 40h 是编译器算好的,对应 64 字节。
1.3 请保镖 (保存寄存器)
-
push ebx->push esi->push edi。 -
注意: 标准 MSVC 习惯把 EDI 放在最上面(最后压入)。
1.4 埋雷/装修 (Debug填充)
-
lea edi, [ebp-40h] + rep stosd (填入 0xCC)。 -
顿悟: 那些"烫烫烫"不是乱码,是防止野指针踩踏的"地雷"。
2. 有序撤退
2.1 送客 (LIFO 原则)
-
核心逻辑: 怎么穿衣服就怎么脱衣服。
-
pop edi->pop esi->pop ebx。 -
我的坑: 一开始默写把顺序搞反了,要记得栈顶的先出来。
2.2 填平战壕 (回收空间)
add esp, 40h:手动把 ESP 拉回来。
2.3 战后安检 (Check)
-
cmp ebp, esp+call __chkesp。 -
**顿悟:**这里是在查账!看上面的 add 有没有算对,或者中间有没有子函数搞破坏。
2.4 暴力归位 (Reset)
-
mov esp, ebp。 -
心得: 不管账平不平,为了活着回家,必须强行把 ESP 拉回 EBP。这是最后一道保险。
2.5 班师回朝
pop ebp(恢复基址) ->ret(跳回家)。
3. 实战避坑指南(我的错题本)
3.1 Debug模式下的函数跳转 (ILT)
-
核心现象:地址是假的
-
现象: 在 VS Debug 模式下,CALL 00401xxx 后面的地址,通常不是函数真正的入口。
-
本质: 那是 ILT (增量链接表) 的地址,相当于"前台"或"中转站"。
-
目的: 为了编译加速(改代码只改中转站,不用改所有调用者)。
-
-
跳转流程:三步走
-
不要以为是 CALL -> 函数,实际路线是:
-
CALL (调用者): CALL 中转站 -> 压入返回地址 (Stack +1)。
-
JMP (中转站/帐篷): JMP 真实地址 -> 不压栈,直接飞 (Stack 不变)。
-
PUSH EBP (真实函数): 这里才是真正的函数开头 (Stack 此时只有 1 个返回地址)。
-
-
关键区别:CALL vs JMP
-
CALL: 会留下痕迹(压入返回地址),像去住酒店要登记。
-
JMP: 是无痕的(不压栈),像穿过一扇任意门。
-
结果: 正因为中间用的是 JMP,函数最后的 RET 才能直接跳回 CALL 的地方,完美忽略中转站。
-
-
满屏乱码:CC (int 3)
-
位置: 如果你在中转站看到 JMP 下面有一堆 CC。
-
实质: 汇编指令 int 3 (断点中断)。
-
作用:
-
凑整(对齐): 让下一个 JMP 的地址好看(4字节对齐)。
-
地雷(防跑飞): 程序正常跑绝不会碰它;碰了就是出 Bug 了,调试器会报警。
-
策略: 直接无视,那是填充物。
-
-
⚡️ 一句话实战口诀
"Debug 模式看 Call 别当真,那是中转站; 进去看见 JMP (帐篷) 和 CC (乱码) 别纠结; 再跳一次到 push ebp,那才是真战场!"
3.2 以为 __chkesp 只有主函数有。
- 正解: 只要当了"老板"(调用了子函数),为了防坑,都会有这个检查。
3.3 以为销毁时是先 mov esp, ebp 再检查。
- 正解: 先检查(暴雷),再归位(兜底)。顺序不能乱!
3.4 视觉特征
-
看到
55 8B EC= 函数开始。 -
看到
5F 5E 5B(连续 pop) = 准备跑路。
4. 手写函数栈帧

结语
平衡,是万物的法则,也是堆栈的法则。
从 SUB ESP 的借取,到 ADD ESP 的归还;从 PUSH 的入栈,到 POP 的出栈。我看到了一种极致的对称美。任何一次微小的失衡,任何一次忘记的 POP,都将导致程序的崩溃。
原来,看懂汇编不仅仅是看懂指令,更是看懂 '有借有还' 的契约精神。至此,函数栈帧的秘密已无处遁形。我也终于明白:要想控制程序的流向,首先要学会控制内存的起落。
笔记至此合上,但我的逆向之路,才刚刚启程。