80x86汇编与C语言-3
透彻理解C代码在80x86上的汇编映射,掌握栈帧、调用约定与反汇编逆向
按 空格/→ 演示下一步
全部页面点击任意一页,跳回舞台从这页播放
80x86汇编与C语言-3
透彻理解C代码在80x86上的汇编映射,掌握栈帧、调用约定与反汇编逆向
程序运行栈的基本操作-1
调用函数时,CPU要记下「等会回到哪继续执行」,函数里又要腾出空间放临时变量——这些都靠一块由硬件辅助管理的特殊内存区域:程序运行栈。
餐盘只能从顶部放入或取出,弹簧压住的那块就是栈顶——对应 ESP 始终指向栈顶
程序运行栈的基本操作-2
上一页只动了 ESP(压栈出栈),没解释「函数调用怎么自动完成」。这一页补全:调用方如何跳过去,被调方如何跳回来,以及中间的栈空间怎么划分。
开会时放下旧工位牌、领新牌、用桌面放资料;散会还原工位、按便签回原处
过程调用与栈
过程调用与栈:定义、要点与典型应用
栈帧
上一页我们看到 call 把返回地址压栈、ret 又把它弹出——但一次调用在栈上占多大地方?里面又放些什么?这一格「函数专属的栈区」就是栈帧。
每次入住分一间房,退房收回;EBP 像固定房号,方便定位行李,行李不会混进隔壁房间
一个实例
前几页我们拆开了栈帧的零件——参数压栈、保存 ebp、call 推返回地址。这页把这些零件装回原位,看一段真实的 C 函数怎样被翻译成汇编指令。
入住=prologue 记旧房号领新钥匙;退房=epilogue 归还钥匙回到自己房间
寄存器使用惯例
拆完栈帧我们看到调用前后寄存器值变了。问题来了:哪些寄存器被调函数必须原样还回来,哪些可以随手改写?分开编译的模块靠什么对接参数和返回值?这就是寄存器使用惯例要约定的事。
工位上原有物品(callee-saved)借完要放回,自己带的(caller-saved)丢了自负
一个递归调用的实例-1
有了栈帧和寄存器使用惯例的基础,我们来看一个完整的递归调用实例。从最简单的阶乘函数入手,看每次递归调用时栈帧是如何一层层叠加又缩回的。
外层套娃对应外层调用,内层是递归进入的下一层;打开是进入递归,合上是逐层返回
一个递归调用的实例-2
上一节我们看到 factorial(3) 的入口代码如何保存寄存器、建立自己的栈帧。这一节接着看:当它发现还需要继续算时,如何「叫醒」下一个自己——也就是递归自调用是如何发生的。
每摞一本对应一次递归调用,最底下的先放最后才能取
另一个递归调用的实例-1
上例的 n! 是「单链」递归,每层只调自己一次。这次换成斐波那契 fib(n)=fib(n-1)+fib(n-2)——每层同时展开两个子调用,调用栈会变成什么样?
领导(调用层)必须等两个下属(两次递归)都交报告,再合并上报给上级
另一个递归调用的实例-2
上一个例子我们看到调用链一路压栈到基例。这一页反过来走:基例算出结果后,栈帧怎么逐个弹出、返回值怎么一层层回传上去。
穿时从里到外一件件套上,退时从外到里一件件脱下
x86-32过程调用小结
看了两个递归调用的完整流程后你会发现,每一层进入都建栈帧、退出都拆栈帧——背后的规则其实非常一致。下面把 x86-32 过程调用涉及的全部约定归为五条核心要点。
材料按反序压栈,柜员在固定工位(%ebp)办事,办完回执从窗口(%eax)推出,再清理桌面(拆栈帧)。
本节要点
- ✓栈帧是过程调用的核心:参数、返回地址、局部变量按固定布局
- ✓寄存器分两类:caller-save与callee-save
- ✓递归本质是反复构造栈帧,深度受栈空间硬性限制
- ✓cdecl惯例:参数从右往左压栈,调用者负责清理
课后思考
先独立想一遍再看参考答案,重点体会问题背后的设计权衡。
参考答案叶子函数无需保存 ebp(可用 esp 直接寻址),也无须为被调用者保存寄存器预留空间;多数编译器会省去 push ebp / mov ebp,esp 这两步。
参考答案循环版本通常只需常数级栈空间,递归版本随深度线性增长。但树遍历等本质递归问题,循环需自维护栈,反而更复杂,这时递归不可替代。
参考答案新增寄存器是为了减少调用开销(更多数据放寄存器、少压栈),但调用栈「保存现场、层层返回」的本质没变——栈仍是表达调用嵌套最自然的结构。