栈内行为分析
栈内行为分析
一、源码分析
我们以以下简单的 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 ; 从栈顶取出返回地址并跳转执行