80x86汇编与C语言-3(续)
x86-64过程调用与运行栈
看清栈帧从创建到销毁,每一行汇编在做什么
按 空格/→ 演示下一步
全部页面点击任意一页,跳回舞台从这页播放
x86-64过程调用与运行栈
看清栈帧从创建到销毁,每一行汇编在做什么
为什么需要运行栈
call/ret 看上去只是两个指令,背后却隐藏一整套内存管理子系统。为什么必须用栈?为什么不能像全局变量那样直接放在 .data 段?这一页从内存管理的视角拆开来看。
后放的盘子在最上面,洗碗工先拿走;最新调用的函数也最先返回
x86-64内存布局全景
代码区/数据区/堆/栈的完整映射,栈生长方向
栈帧的完整结构
自上而下:地址递减,每格代表栈上一块连续区域;箭头方向即栈增长方向。
反汇编看栈帧布局
用 objdump 看一个简单加法函数,把上一页抽象的栈帧结构映成真实的机器指令。
前3行序言建帧,中段把参数和局部 c 在 rbp 偏移位置存取,后两行 leave+ret 拆帧返回——上一页的栈帧布局一一对上。
RBP与RSP的角色
反汇编里每个函数都同时出现RSP和RBP两个寄存器。它们各司其职——一个随时在动,一个相对固定。为什么需要两个?分工有何讲究?
GPS(RSP)每步更新坐标;营地旗(RBP)钉在进入此段路时的固定位置,作为回查装备的稳定锚点
局部变量的栈上位置
数组/结构体/指针在栈帧中的存储方式
参数传递规则
栈帧是被调用函数的'工作台',那参数是怎么被送进去的?System V ABI给前6个整数参数安排了6条VIP通道——寄存器。多出来怎么办?
6个固定格口对应RDI~R9按序投递;超出的件倒序堆在柜台上,最右参数压在最上面
寄存器角色分工
从根节点向左分两路:左路调用方需自行保护,右路被调方必须保留。
cdecl vs fastcall vs System V
cdecl 是32位C默认,全走栈;fastcall 把前两参数塞进寄存器;System V 把这条路推到6个寄存器。
- 参数全部压栈,从右向左入栈
- 栈由调用者(caller)清理
- 参数数量无硬性上限
- 返回值放EAX
- 前6参数走RDI/RSI/RDX/RCX/R8/R9
- 栈由被调用者(callee)清理
- 第7个起才压栈,从右向左
- 返回值放RAX
call指令的执行过程
call指令内部只做两件大事:把回家的地址记到栈上,然后跳过去。
ret指令的执行过程
ret 是 call 的逆操作,三步归还控制权,第四步悄悄把栈对齐复原。
递归调用的栈帧链
从上往下读是层层压栈,从下往上是逐层弹栈,直观呈现递归的「洋葱式」展开与回溯。
函数序言(prologue)
函数序言是每个函数开头必做三件事:把旧现场收好、给新栈帧定锚、再为局部变量腾出空间。
函数尾声(epilogue)
尾声按固定顺序还原栈帧,然后用ret把控制权交还调用者。
栈帧的优化变体
GCC -O2 把 sq_sum 内联掉,norm_sq 成了叶子函数;小结构体走寄存器,帧指针直接省了。
对照完整序言/尾声:叶子函数 + -fomit-frame-pointer 让 push %rbp、sub %rsp、leave 全消失;16 字节的 pair_t 被 System V ABI 拆进 xmm0/xmm1,参数和返回值全程活在寄存器里。
栈展开(Stack Unwinding)
崩溃时调试器打印的调用链从何而来?它要逆着刚才讲过的栈帧链一格格回溯,这就是栈展开。
栈像一卷紧绕的纸,上面印着每层调用信息,展开才能逐层读取
缓冲区溢出攻击原理
先看上方攻击链,再看下方canary防御链
栈溢出调试实战
用 GDB 现场观察:40 字节输入如何涂掉 saved RBP 并触发 canary 检测。
canary 与 saved rbp 都被 A 涂掉;glibc 在函数尾声比对失败,abort 并打印 'stack smashing detected'。这正是 -fstack-protector 存在的理由:把溢出从 ret 时崩溃提前到 ret 前被发现。
核心要点回顾
- ✓ABI是契约,栈帧是物理实现
- ✓call/ret本质是RSP移动与返回地址存取
- ✓寄存器传参与栈传参各有性能边界
- ✓帧指针省略时,调试依赖DWARF元数据
- ✓递归即栈帧的链式堆叠
深入思考题
先遮住答案,自己想一会再写下来;再对照参考答案的思路,不必拘泥文字一致。
参考答案SSE(如 movaps)要求内存操作数 16 字节对齐;Windows 未对齐直接触发 #GP 崩溃,Linux 由内核兜底但性能更差。call 压入的 8 字节 rip 正是偏移的根源。
参考答案形式上可省 push rbp,但 rsp 本就在充当帧底。问题场景:函数后续被改成非叶子、编译器需对齐栈上数组、或调试器与异常处理器依赖标准帧回溯——缺帧会让栈展开失败。
参考答案栈是 CPU+ABI 的一等公民:return 时 O(1) 弹帧、寄存器直接寻址、缓存友好、递归深度可预测。堆 malloc/free 开销大、回收不确定,缺硬件 LIFO 支撑,无法一对一替代。