C/C++反汇编之函数栈帧的理解
📅 2026/7/19 15:05:19
👁️ 阅读次数
📝 编程学习
🎯 二进制安全必备:函数栈帧与反汇编全景复习笔记
这份笔记为你完整还原了 C 语言源码到经典 x86(32位 Intel 语法、无优化编译)反汇编的映射关系,包含了行级详细解析与核心复习表。你可以直接复制本段 Markdown 源码保存为本地文件,方便随时复盘。
💻 1. C 语言源代码
intmain(){add(0xAA,0xBB);return0;}voidadd(intx,inty){intz=0xCC;}🛠️ 2. 反汇编代码行级详解
🏢 main 函数反汇编流
; ======= main 函数的开端 (函数序言 Prologue) ======= push ebp ; 1. 将 main 上级函数的 EBP 压栈备份(保存现场) mov ebp, esp ; 2. 让 EBP 等于当前的 ESP,正式确立 main 函数自己的栈帧基准线 ; ======= 准备参数并调用 add(0xAA, 0xBB) ======= push 0BBh ; 3. [对应C: y = 0xBB] 将第二个参数压入栈顶 (ESP = ESP - 4) push 0AAh ; 4. [对应C: x = 0xAA] 将第一个参数压入栈顶 (ESP = ESP - 4) ; 注:x86 下参数按从右往左(Right-to-Left)顺序压栈 call _add ; 5. [对应C: add(...)] 核心动作: ; A. 将下一行指令的地址(返回地址)压入栈顶 (ESP = ESP - 4) ; B. 将 EIP 寄存器修改为 _add 函数的入口地址,实现跳转 ; ======= 从 add 函数返回后的清理工作 ======= add esp, 8 ; 6. [栈平衡 Balance] 重点!由调用者(main)亲自清理栈顶。 ; 刚才 push 了两个参数(4字节 + 4字节 = 8字节), ; 这里把 ESP 加上 8,强行把这两个参数从栈顶“踢除” ; ======= main 函数的收尾 (函数尾声 Epilogue) ======= xor eax, eax ; 7. [对应C: return 0] 清零 EAX,作为 main 函数的默认返回值 pop ebp ; 8. 弹栈,恢复 main 上级函数的 EBP ret ; 9. 结束 main 函数,返回更上层🧪 add 函数反汇编流
_add proc near ; add 函数入口 ; ======= 1. 函数序言 (建立专属办公室) ======= push ebp ; 将 main 函数的 EBP 压栈备份 (ESP = ESP - 4) mov ebp, esp ; 让 EBP 等于当前的 ESP。从此,当前函数的 EBP 固定在分水岭位置! ; ======= 2. 局部变量空间开辟 ======= sub esp, 4 ; [对应C: int z] 将 ESP 减去 4,在栈上强行留出 4 字节的空地给变量 z ; ======= 3. 执行业务逻辑 (赋值) ======= mov dword ptr [ebp-4], 0CCh ; [对应C: z = 0xCC] ; 往 [EBP - 4](刚才开辟的空地)写入 4 字节的常数 0xCC ; ======= 4. 函数尾声 (拆除办公室、退租) ======= mov esp, ebp ; 清空局部变量!让 ESP 直接回到 EBP 的位置 (无视并放弃了 [EBP-4] 的空间) pop ebp ; 弹栈,把刚才保存的 main 函数的 EBP 还给 EBP 寄存器 (ESP = ESP + 4) ; 此时 EBP 恢复为 main 的,ESP 指向栈顶的返回地址 ret ; 魂归故里!弹出当前栈顶的返回地址直接赋给 EIP 寄存器, ; CPU 控制权瞬间跳回 main 函数中(即 call _add 的下一行) _add endp⚙️大佬敲黑板:
在真实的 IDA Pro 反汇编中,你经常会看到编译器把add函数尾声的mov esp, ebp和pop ebp两条指令合并精简为一条著名的指令:leave。它们在硬件层面上完全等价。
📊 3. 终极复习作弊表(Cheatsheet)
当你在汇编中看到以EBP为基准的内存寻址(中括号表达式)时,直接套用下表进行肉眼翻译,这是分析栈溢出(Pwn)和逆向的关键:
| 汇编表达式 | 它的真实身份 | 产生的原因 | Pwn 视角(漏洞安全意义) |
|---|---|---|---|
[ebp] | Saved EBP(旧 EBP 的备份) | 函数序言第一步push ebp留下的。 | 栈溢出时如果把它改掉,原函数在退栈时会迷失方向,导致程序崩溃(SegFault)。 |
[ebp + 4] | Return Address(返回地址) | call指令在跳入函数前自动强行压入的。 | Pwn 的终极劫持目标!只要覆盖这里,就能在函数执行ret时,让程序跳去执行任意恶意代码(如 system 逻辑)。 |
[ebp + 8] | 第 1 个输入参数 (x) | 在返回地址的上方(高地址),由调用者提前压入。 | 只读。改变它不会破坏栈结构,但会改变函数的业务运行逻辑。 |
[ebp + 12] | 第 2 个输入参数 (y) | 在第 1 个参数的上方。多个参数以此类推(+16, +20…)。 | 只读。 |
[ebp - 4] | 第 1 个局部变量 (z) | 函数内部sub esp, 4之后在低地址方向自己开辟的。 | 溢出的源头!如果这个变量是个缓冲区(如char buf[16]),当你输入过长数据时,数据会**向上(往正号/高地址方向)**依次淹没[ebp]和[ebp+4](返回地址)。 |
🧠 4. 核心逻辑闭环思维导图
高地址 (楼顶) | ... | [EBP + 12] -> 参数 2 (0xBB) <--【Caller压入】 | [EBP + 8] -> 参数 1 (0xAA) <--【Caller压入】 | [EBP + 4] -> 返回地址 (EIP) <--【CALL指令自动压入】-- 🎯 漏洞劫持核心 | [EBP] -> 旧 EBP 备份 <--【Callee序言 push ebp】 | [EBP - 4] -> 局部变量 z <--【Callee自减开辟 sub esp】-- 🌊 溢出污染源 V 低地址 (地下室)一句话总结复习流:栈是倒着长的(向低地址生长);输入数据是顺着写的(向高地址溢出)。所以,位于低地址的局部变量一旦发生溢出,会无情地向高地址方向横扫,依次冲毁旧 EBP 和关键的返回地址。
编程学习
技术分享
实战经验