栈内行为分析
一、源码分析
我们以以下简单的 C 程序为例,通过 GDB 动态调试分析函数调用过程中的栈内布局变化:
#include <stdio.h>
int add(){int a = 10;int b = 20;return (a + b);
}int main() {add();return 0;
}
编译为 32 位程序:
gcc -m32 test.c -o test
gdb ./test
动态调试
依次在对应位置打上断点
(gdb) b *main
Breakpoint 1 at 0x11d9
(gdb) b *add
Breakpoint 2 at 0x11ad
(gdb) b *add+42
Breakpoint 3 at 0x11d7
(gdb) r
Starting program: /root/test
Breakpoint 1, 0x565561d9 in main ()
(gdb) layout regs
补充: esp 永远指向栈顶,ebp永远指向栈底 先记住这句话
然后我们继续运行c进入add函数在下面这个图我们还没进入add函数,eip指向的地址就是add函数的入口地址
程序进入 main() 函数后,还未调用 add() 函数之前,EIP 指向的是 add() 函数的入口地址。此时:
- ESP 指向当前栈顶
- EBP 尚未参与本次函数调用帧的构造
重点部分解释
push %ebp ;这就是我们经常说的压栈 将调用者(main)的 ebp 存入当前栈顶,用于函数返回后恢复上下文
mov %esp, %ebp ;设置新的栈基址,构造当前函数的栈帧
sub $0x10, %esp ;压栈 留出栈空间(0x10 字节)用于局部变量(如 a、b)

当我们进入add函数到这一步我们会发现 此时的esp=ebp 可以观察到:
程序刚刚进入函数,ESP == EBP,栈帧尚未展开。
类似于“空水桶”,当前的栈顶和栈底都指向相同位置。
这一步体现了函数调用刚发生、栈帧尚未初始化的状态。

但是接下来,我们继续si几步我们会发现esp在不停变化代码比较简单 但是能看出esp是不断变化的
ESP 向低地址移动(因为栈向下生长)
为局部变量 a 与 b 分配空间
函数内部指令执行期间不断使用和调整 ESP
这一过程中,ESP 表示当前操作的顶部位置,而 EBP 固定在该栈帧的底部,作为局部变量的偏移基准。

直到走到我们第三个断点 add+42 我们继续si esp和ebp又继续相等了,同时我们回到了main函数当中
ESP 与 EBP 再次恢复为相同值,意味着当前栈帧已被销毁
程序执行流程回到 main() 函数,继续往下执行
汇编解释
leave ; 出栈 实际上等价于:mov %ebp, %esp(还原 esp)→ pop %ebp(恢复上层 ebp)
ret ; 跳转 弹出栈顶的返回地址(调用者 call 指令之后的地址),跳转回主函数
-------------------分割线---------------
leave 是一个复合指令,相当于做了这两件事:
mov %ebp, %esp ; 清理当前栈帧(还原栈顶)
pop %ebp ; 恢复调用者的 ebp
-------------------分割线---------------
ret 的作用
ret 会做这件事:
pop %eip ; 从栈顶取出返回地址并跳转执行
