函数栈帧的创建与销毁(保姆级讲解)

局部变量是怎么创建的?

在为main函数开辟栈帧空间时,在一定范围内初始化成0CCCCC,再把里面0CCCC的一些开辟空间给局部变量使用。

为什么局部变量的值是随机值?

因为我们在为main函数开辟栈帧空间时,会将一定范围内空间初始成0CCCCCC里面什么也没有,所以如果局部变量不给初始化,局部就会进入随意开辟栈帧空间,就是为随机值或者是烫烫烫。

函数是怎么传参的?传参的顺序是怎样的?

eax(b)和ecx(a)进行压栈,先传的b再传的a,从右向左。

形参和实参是什么关系?

形参是实参的一份临时拷贝,它们的值是相同的,但所使用的空间是不同的,所以形参的改变不影响实参,形参确实只是实参的一份临时拷贝。

函数调用是怎么做的?

下面我画图所解释的非常清楚了。(如果不明白的同学可以私信互相交流下)

函数调用是结束后怎么返回的?

由函数一步一步建立空间,再一步一步销毁空间返回

用保存call指令下一条指令地址与ebp-main函数的保存位置进行寄存器返回值

用寄存器eax返回最终的值。


知道和函数栈帧的创建和销毁就都会了,其实就是修炼了自己的内功,也能搞懂后期更多的知识。
进入正题
今天讲解使用的环境是VS2019
同时在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。

首先我们要了解什么是函数栈帧

函数栈帧就是在函数调用过程中,程序为函数所开辟的栈空间,函数一般放在栈区。

而编译器为了方便动态内存管理,一般划分为了三个区域:栈区 堆区 静态区

函数栈帧的创建与销毁(保姆级讲解)_第1张图片

而什么又是栈呢?

栈的概念及结构
栈:一种特殊的线性表,其只允许在 固定的一端 进行 插入和删除 元素操作。 进行数据插入和删除 操作的一端称为 栈顶 ,另一端称为 栈底 。栈中的数据元素遵守 后进先出 LIFO (Last in First Out) 的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈(Push), 入数据在栈顶
出栈: 栈的删除操作叫做出栈(Pop)。 出数据也在栈顶 。
特点:栈只能在栈顶进行插入和删除。

为了更加清楚了解函数栈帧,我们还需要了解下以下寄存器

eax :保留临时数据,常用于返回值

ebx:保留临时数据

ecx

edx

ebp:栈低指针

esp:栈顶指针

ebp和esp这两个寄存器存放的是地址,这两个地址用来维护函数栈帧的。

每一个函数调用,都要在栈区创建一个空间。

接下来以如下图代码来解释函数栈帧的创建与销毁过程

#include
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);

	return 0;
}

在调用main函数时ebp和esp会维护main函数的函数栈帧。函数栈帧的创建与销毁(保姆级讲解)_第2张图片

当我按F10进入调试,并且打开调用堆栈

函数栈帧的创建与销毁(保姆级讲解)_第3张图片


函数栈帧的创建与销毁(保姆级讲解)_第4张图片函数栈帧的创建与销毁(保姆级讲解)_第5张图片

我们可以看见函数的调用关系,调用堆栈是反应函数的调用关系的

由调试我们可以看见main函数是被invoke_main()函数调用的

而Add函数是被main()函数调用的

函数栈帧的创建与销毁(保姆级讲解)_第6张图片

那么理所当然的invoke_main函数与Add函数也是有自己的函数栈帧空间的,并且由ebp和esp来维护函数栈帧空间。

F10进入调试点击右键转到反汇编

函数栈帧的创建与销毁(保姆级讲解)_第7张图片

如下图所示是此次代码的反汇编指令 

函数栈帧的创建与销毁(保姆级讲解)_第8张图片

但我们为了方便更加清晰的观察,再次点右键取消显示符号名。

函数栈帧的创建与销毁(保姆级讲解)_第9张图片

函数栈帧的创建与销毁(保姆级讲解)_第10张图片 函数栈帧的创建与销毁(保姆级讲解)_第11张图片

main函数第一条反汇编指令是Push(压栈) ebp。

函数栈帧的创建与销毁(保姆级讲解)_第12张图片

如果是ebp压栈那么ebp的值是会变小的 因为是由高地址到低地址

这是原来的值

当我push以后的值,果然变小了

函数栈帧的创建与销毁(保姆级讲解)_第13张图片

 所以是真的压进去了吗?我们可以通过内存来查看

当我们查看esp内存的地址时,ebp确实压进去了,因为esp内存的值是ebp的地址

函数栈帧的创建与销毁(保姆级讲解)_第14张图片

fc f9 f3 00  VS编译器一般是:小端字节存储 低位字节数据放低地址处 高位字节数据放高地址处 

mov的意思是把esp的值给ebp 我们依然可以通常调试来查看是不是这样的

函数栈帧的创建与销毁(保姆级讲解)_第15张图片

函数栈帧的创建与销毁(保姆级讲解)_第16张图片

函数栈帧的创建与销毁(保姆级讲解)_第17张图片

sub是subtraction减法的缩写。就是给esp减去0E4h函数栈帧的创建与销毁(保姆级讲解)_第18张图片 函数栈帧的创建与销毁(保姆级讲解)_第19张图片

 用16进制显示就是228

函数栈帧的创建与销毁(保姆级讲解)_第20张图片

 当我们给esp减去所对应的值时,那么esp不能再指向原来的位置,而是指向了上一块的某块区域

函数栈帧的创建与销毁(保姆级讲解)_第21张图片

函数栈帧的创建与销毁(保姆级讲解)_第22张图片

函数栈帧的创建与销毁(保姆级讲解)_第23张图片

当我们查看内存ebp和esp

ebp

函数栈帧的创建与销毁(保姆级讲解)_第24张图片

esp

函数栈帧的创建与销毁(保姆级讲解)_第25张图片

这些内存空间都是为main函数所开辟的空间 

函数栈帧的创建与销毁(保姆级讲解)_第26张图片

接下来是在栈顶压3个元素 

函数栈帧的创建与销毁(保姆级讲解)_第27张图片

随着压栈esp也会随着压栈指向位置会发生变化 

函数栈帧的创建与销毁(保姆级讲解)_第28张图片

 函数栈帧的创建与销毁(保姆级讲解)_第29张图片函数栈帧的创建与销毁(保姆级讲解)_第30张图片

VS2019栈区内存存放习惯:先放高地址,再放低地址

下面esp的值会随着压栈 esp位置也产生了变化

函数栈帧的创建与销毁(保姆级讲解)_第31张图片

lea指令是=load effective address. 意思是加载有效地址

函数栈帧的创建与销毁(保姆级讲解)_第32张图片

 这个指令有效果的其实是rep stos意思是把edi开始下面将内存空间改成OCCCCCCCCh 以双倍字节开辟 dword=double word  es:[edi]把edi开始下面所有空间以双字节开辟成0CCCCCCCCh。

edi到ebp开辟内存空间

esp-24h的值如下图

函数栈帧的创建与销毁(保姆级讲解)_第33张图片

函数栈帧的创建与销毁(保姆级讲解)_第34张图片 函数栈帧的创建与销毁(保姆级讲解)_第35张图片

函数栈帧的创建与销毁(保姆级讲解)_第36张图片

函数栈帧的创建与销毁(保姆级讲解)_第37张图片

函数栈帧的创建与销毁(保姆级讲解)_第38张图片

 将十进制数0Ah放进ebp-8 就是相当于把10放到了ebp-8里面

函数栈帧的创建与销毁(保姆级讲解)_第39张图片

如果不给a初始值那么就是随机值,因此在为main函数开辟空间时使用的就是CCCCCC的值,所以会出现烫烫烫(字符串 字符)或者随机值(变量)。

函数栈帧的创建与销毁(保姆级讲解)_第40张图片 函数栈帧的创建与销毁(保姆级讲解)_第41张图片

0a 00 00 00  就是10的十六进制存储 内存存储一般是十六进制存储

函数栈帧的创建与销毁(保姆级讲解)_第42张图片函数栈帧的创建与销毁(保姆级讲解)_第43张图片

函数栈帧的创建与销毁(保姆级讲解)_第44张图片函数栈帧的创建与销毁(保姆级讲解)_第45张图片

又是隔着两个字节存放的C的值0 (不同编译器存放位置不同,取决于编译器)

函数栈帧的创建与销毁(保姆级讲解)_第46张图片

 函数栈帧的创建与销毁(保姆级讲解)_第47张图片

把20的值给eax再把eax压栈压进去

函数栈帧的创建与销毁(保姆级讲解)_第48张图片

函数栈帧的创建与销毁(保姆级讲解)_第49张图片

函数栈帧的创建与销毁(保姆级讲解)_第50张图片

下一步指令把10给ecx然后再把ecx进行压栈

函数栈帧的创建与销毁(保姆级讲解)_第51张图片

函数栈帧的创建与销毁(保姆级讲解)_第52张图片

函数栈帧的创建与销毁(保姆级讲解)_第53张图片

函数栈帧的创建与销毁(保姆级讲解)_第54张图片

 按F11进入call令

函数栈帧的创建与销毁(保姆级讲解)_第55张图片

内存中存放的是下一条add指令的地址00171987

 当我们再按一次F11会跳到Add函数的反汇编指令当中去

函数栈帧的创建与销毁(保姆级讲解)_第56张图片

这和main函数的反汇编极其相似,这是在为Add函数开辟函数栈帧空间

 第一步Push压栈 第二步mov esp给ebp 第三步把0cch 给esp 相当于esp又往上走了

函数栈帧的创建与销毁(保姆级讲解)_第57张图片

 函数栈帧的创建与销毁(保姆级讲解)_第58张图片

 函数栈帧的创建与销毁(保姆级讲解)_第59张图片

 函数栈帧的创建与销毁(保姆级讲解)_第60张图片

再进行压栈 

函数栈帧的创建与销毁(保姆级讲解)_第61张图片

随着压栈esp的值也变化 esp的指向位置也随着变化

函数栈帧的创建与销毁(保姆级讲解)_第62张图片

函数栈帧的创建与销毁(保姆级讲解)_第63张图片

函数栈帧的创建与销毁(保姆级讲解)_第64张图片

把ebp-0ch值给edi 把3给ecx 然后从edi开始下面所有位置改成0CCCCCCCH

 函数栈帧的创建与销毁(保姆级讲解)_第65张图片

函数栈帧的创建与销毁(保姆级讲解)_第66张图片

 

 把0放到ebp-8 

 函数栈帧的创建与销毁(保姆级讲解)_第67张图片

 函数栈帧的创建与销毁(保姆级讲解)_第68张图片

 函数栈帧的创建与销毁(保姆级讲解)_第69张图片

函数栈帧的创建与销毁(保姆级讲解)_第70张图片

 

ebp+0ch相当于+12 epb+12

函数栈帧的创建与销毁(保姆级讲解)_第71张图片

函数栈帧的创建与销毁(保姆级讲解)_第72张图片

函数栈帧的创建与销毁(保姆级讲解)_第73张图片

通过调试过程看见传参是从右向左,先传的b再传的a

最后返回的时候把ebp-8的值也就是z的值给了寄存器eax

下面指令pop三次 esp也随着产生位置变化

函数栈帧的创建与销毁(保姆级讲解)_第74张图片

当pop了三次esp的值 增加了3次

函数栈帧的创建与销毁(保姆级讲解)_第75张图片

函数栈帧的创建与销毁(保姆级讲解)_第76张图片

当pop三次要返回main函数 那么Add创建的函数栈帧就要销毁 这几个指令完成了把esp的值给ebp

函数栈帧的创建与销毁(保姆级讲解)_第77张图片

再pop ebp 我们这个位置所保存main函数的ebp-main 就是为了函数返回时找到main函数的ebp

函数栈帧的创建与销毁(保姆级讲解)_第78张图片

函数栈帧的创建与销毁(保姆级讲解)_第79张图片

返回以后ebp和esp又开始维护了main函数的函数栈帧空间

这条指令就是为了执行call指令下一条add的功能 弹出了add指针地址

所以我们在开辟函数栈帧时保存了add指令地址

函数栈帧的创建与销毁(保姆级讲解)_第80张图片

 当我们再按F10就回到了main函数Add的位置

函数栈帧的创建与销毁(保姆级讲解)_第81张图片

函数栈帧的创建与销毁(保姆级讲解)_第82张图片

main函数add指令 00171987与我们刚才在函数栈帧保存的call指令下一条指令的地址一模一样,也就是add指令函数的地址。就是为了方便回来,简直就是荣归故里!设计的太牛了!

函数栈帧的创建与销毁(保姆级讲解)_第83张图片

给esp+8

函数栈帧的创建与销毁(保姆级讲解)_第84张图片

随着esp+8形参的空间也销毁了。

把eax 30的值给ebp-20h 就是刚才c的位置

函数栈帧的创建与销毁(保姆级讲解)_第85张图片

然后在程序结束时寄存器会返回所对应的值。

你可能感兴趣的:(开发语言,c语言,c++)