C/C++反汇编之函数栈帧的理解

📅 2026/7/19 15:05:19 👁️ 阅读次数 📝 编程学习
C/C++反汇编之函数栈帧的理解

🎯 二进制安全必备:函数栈帧与反汇编全景复习笔记

这份笔记为你完整还原了 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, ebppop 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 和关键的返回地址。