函数栈帧的创建和销毁

全文目录
- 前言
- 寄存器
- main函数的调用
- 调用main函数的函数
- main函数的栈帧如何开辟的
- `push`(保存调用方的`ebp`)
- `move`(维护新开栈帧的栈底)
- `sub`(维护新开栈帧的栈顶)
- 三连`push`(添加栈帧的信息的变量)
- `lea` (存放栈顶地址)
- `rep stos`(初始化栈帧)
- add函数的执行
- 创建变量
- 传参
- `call` (函数调用 )
- 参数的使用
- 函数的返回值和栈帧的销毁
- 形参的销毁
- 总结
前言
前面在使用函数时一直说到函数栈帧的创建与销毁,但也只是云里雾里的,今天就来讲讲关于函数栈帧的知识。实验环境:VS2013,系统环境X86
我们通过一段简单的代码来了解函数的栈帧:
#include <stdio.h>int add(int x, int y)
{int z = 0; z = x + y;return z;
}int main()
{int a = 10;int b = 20; int c = 0;c = add(a, b);printf("%d\\n", c);
}
寄存器
再了解函数栈帧前,我们需要先知道一些前景知识,首先来了解一下寄存器。再VS2013中,我们可以通过调试窗口中的寄存器窗口查看有哪些寄存器:

可以看到有下面几种寄存器:
通用寄存器(数据存放数据使用):
eax
ebx
ecx
edx变址寄存器(偏移量):
esi
edi指令寄存器(下一条指令的地址):
eip指针寄存器(地址,维护函数栈帧):
esp(堆栈指针寄存器,用于存放栈顶指针的位置)
ebp(基址指针寄存器,用于寻找栈内的元素)标志性寄存器(不知道什么东西):
efl
寄存器的详细知识可以点这里:汇编——寄存器的分类和功能
main函数的调用
每一个函数的调用都需要在栈区上开辟一块开空间,在栈上一块专门为函数开辟的空间就是函数的栈帧。这么一块栈帧其实是由两个寄存器维护的,根据上面寄存器的介绍,大概能猜到是esp, ebp两个寄存器维护的了:

调用main函数的函数
main函数也是函数,所以我们可以在调试中通过函数调用堆栈来看一下main函数是由谁调用的:

由此我们可以很明显得看到main函数的调用关系:

调用main函数的函数__tmainCRTStartup也是需要栈帧的,同样的是由esp, ebp来维护

main函数的栈帧如何开辟的
然后我们可以通过反汇编来看一下main函数是怎么调用的:

push(保存调用方的ebp)
当执行第一个反汇编指令时相当于将ebp的值放到__tmainCRTStartup的栈帧的顶部:

那么esp的值,就相当于减去了 4 ,我们可以通过监视来看一下
执行前:

执行完push指令后:

至于为什么是减4,是因为在32为系统下指针的大小是4字节
这样之后的函数返回后就可以快速找到调用方的栈底,从而继续维护调用方。
move(维护新开栈帧的栈底)
move指令相当于将 esp的值复制给ebp:

通过监视窗口可以看到ebp值的变化:
执行前:

执行完move指令后:

这样ebp 就是新的函数栈帧的栈底,继续干着栈底指针的老本行
sub(维护新开栈帧的栈顶)
执行sub命令相当于esp向下走了 0E4h,那么esp ~ ebp中间的空间就是main函数的栈帧。
执行前:

执行完sub命令后:

相当于:

三连push(添加栈帧的信息的变量)
push前:

push 后:

注意每次压栈后都
esp的值都会变化
相当于:

这三个新压栈的寄存器在后面将会起到大作用。
lea (存放栈顶地址)

lea ,全称 load effictive address,翻译一下就是加载有效地址。可以看到ebp - 0E4h 就是三连 push前main函数的栈顶,相当于将该地址放到 edi 中。
后面两个move等价于:ecx = 39h, eax = 0CCCCCCCCh
rep stos(初始化栈帧)
执行前:

dword:
d: double
word: 一个word是2字节
dword: 4字节
整条指令就是相当于将edi (ebp - 0E4h)往下,每次操作4字节,操作 ecx (39h)次,全部初始化为 eax (0xcccccccc)

也就是:

至此,一个函数的栈帧的创建就完成了,这也是为什么局部变量没有初始化的时候,它的值会是随机数。
add函数的执行
创建变量
函数栈帧创建完成之后就是正常的执行代码了,后面三条语句就是普通的初始化

赋值后:

也就是相当于:

可以看到在VS2013 中,是隔了两个整型的大小来进行初始化的,但是在不同的编译器下可能实现的方式不同。
传参
接下来就是万众瞩目的传参的过程了:

将a和b的值依次传给eax和ecx压栈
也就是:

正好印证了形参是实参的一份临时拷贝
call (函数调用 )
执行完传参后,就是函数调用了,call 指令会将下一条指令压栈,让函数返回时,正常往下执行

然后跳到指定的地址

再通过jmp 命令进行跳到add函数内部

跳到add函数内部之后,就开始函数的创建和初始化等一系列操作,

参数的使用
参数使用时通过ebp找到对应的参数,然后将计算的结果返回回去

也就是:

可以发现参数在传递的时候是从右向左传,在使用形参时是从左向右取,刚好跟参数传递的顺序一致
函数的返回值和栈帧的销毁
返回时,先将返回值放到寄存器 eax 中

然后依次销毁函数栈帧,现将顶上的三个寄存器弹出

也就是:

然后通过move指令将栈顶指向栈底

也就是:

然后通过pop命令将ebp指向main函数的栈底,那么ebp, esp又重新开始维护main函数的栈帧。

也就是:

可以看到esp 指向的就是调用函数之后的下一条指令的地址,ret指令就是通过pop回到调用函数的下一条指令

执行ret指令后回到下一条指令的位置:

esp也向下走了一个位置:

形参的销毁

执行该命令后,形参的栈帧也就销毁了,意味着返回到了main函数中,add函数的栈帧彻底销毁了

之后的就是正常的进行计算等等,各种函数的调用,其栈帧的创建和销毁基本上都是一样的。
总结
自己总结的草图:栈帧的创建和销毁都是一一对应的,怎么创建,就怎么反着销毁。



