函数栈帧的形成和销毁(26张图助你深入理解函数栈帧)

  个人主页:欢迎大家光临——>沙漠下的胡杨

  各位大帅哥,大漂亮

 如果觉得文章对自己有帮助

 可以一键三连支持博主

 你的每一分关心都是我坚持的动力

 

 ☄: 本期重点:我们今天讲解下函数栈帧的形成和销毁

  希望大家每天都心情愉悦的学习工作

 ☄: 本期重点:我们今天讲解下函数栈帧的形成和销毁

相关的寄存器:

相关的汇编命令:

首先我们了解下C程序地址空间(VS版)

接着我们看下函数调用的逻辑

​ 

 下面我们以一个简单的例子来进行理解:

创建main函数栈帧

分析main函数中的代码

调用MyAdd函数前的准备:

进行函数调用:

 开辟MyAdd的栈帧空间:

分析MyAdd代码

准备返回

ret之后

返回call的下一条指令处

剩下一个有意思的证明:

本篇总结:

下期预告:


函数栈帧讲解我们通过汇编的一些分析讲解,所以我们先了解下一些寄存器和汇编指令来进一步学习栈帧吧。

相关的寄存器:

eax:通用寄存器,保留临时数据,常用于返回值

ebx:通用寄存器,保留临时数据

ebp:栈低寄存器

esp:栈顶寄存器

eip:指令寄存器,保留当前指令的下一条指令地址。

相关的汇编命令:

mov:数据转移指令

push:数据入栈,同时esp栈顶寄存器发生改变

pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变

sub:减法命令

add:加法命令

call:函数调用 1.压入返回值 2.转入目标函数

jmp:通过修改eip,转入目标函数,进行调用

ret :恢复返回值地址,压入eip,类似pop eip命令

首先我们了解下C程序地址空间(VS版)

C程序地址空间的图如下:

不同的编译器可能方向不同,增长方向是一样的,必如栈都是向低地址去,堆区向高地址去。 

接着我们看下函数调用的逻辑

main函数也是函数,是被谁调用的呢?

是被 __tmainCRTStartup()这个函数调用的,

然后这个函数 __tmainCRTStartup()又被 mainCRTStartup()这个函数调用

这个函数 mainCRTStartup() 是被操作系统进行直接调用的。

 

 下面我们以一个简单的例子来进行理解:

int MyAdd(int x, int y)
{
	int c = 0;
	c = x + y;

	return c;
}

int main()
{
	int a = 0xA;
	int b = 0xB;
	int z = 0;
	z = MyAdd(a, b);

	printf("z = %x\n", z);
	return 0;
}

很简单的一个函数,就是两数之和然后封装为一个函数来进行调用。

创建main函数栈帧

这是上面就是main函数的栈帧的创建,但是不重要。 

分析main函数中的代码

代码进行汇编分析:

先看a变量,是把 0A的值赋给  ebp - 8这个空间,同理 b 和 z 也是。

看图解:

调用MyAdd函数前的准备:

我们看下调用函数前,汇编指令都做些什么吧。

首先是把ebp - 14h(变量b的值)的值放入eax中,其实就是b的值放入寄存器中,然后压入eax,同理压入ebp - 8(变量 a的值)。

看示意图,观察此时的寄存器的指向,最下面为内存布局:

 

上面的过程证明了:

1.函数调用前,就已经形成了临时变量。

2.形参实例化的顺序是从右向左的。

进行函数调用:

执行 call 命令,进入函数体内。

call要做的是 :

1:压入返回值    2.转入目标函数

为什么要压入返回值呢?因为函数可能会调用结束,那么就需要返回了。

压入返回值,就是压入 call 指令的下一条指令的地址。

看下压入前的内存和地址:

压入后内存,重点看是不是 call 的下一条指令的地址是否压入了:

如下图所示,确实已经压入啦

另外说一下,jmp 后的值,就是MyAdd函数的地址,就是跳转到该函数处。 

所以eip的值也就由原来的 main函数值 变为 MyAdd函数的值啦。

看示意图:

 开辟MyAdd的栈帧空间:

我们 jmp 之后就该进入函数中了,也就是执行函数啦,

我们先看下汇编:

这些汇编就是为MyAdd开辟战帧空间

首先逐步分析,push ebp,把ebp压入栈中,ebp就是main函数的栈低位置,mov 把 esp的地址移动到ebp中,ebp其实是MyAdd的栈顶,最后把 esp 的值减去0CC。其实就是把esp的当前位置向上移动,然后和 ebp 围成一段空间,为了MyAdd函数使用。

如图所示:

示意图为:

其中 ebp 和 esp围成的空间就是MyAdd函数的栈帧。

这是 esp 和 ebp 都指向了,main函数的栈顶,那么栈低指针呢?

不用担心main函数栈低的地址找不到,我们刚才把main函数栈低指针压入了MyAdd的栈帧空间中啦,到时候可以直接返回了。

分析MyAdd代码

还是先进行反汇编代码查看:

逐个分析:

mov  ebp - 8, 0,其实就是在ebp -8的位置处赋值为0,接着把 ebp + 8的值放入eax中,再把ebp + 0C 的值和原来eax中的值相加,最后把eax中的值放入ebp - 8空间中去

翻译下就是:

开辟空间 c 变量,初始化为0,把原来形参实例化中的a的值放入eax中,把形参实例化中的b也放进去与a相加,最后的结果在eax中,把eax的值放入变量 c 的空间中。这就是上述的汇编。下面看下示意图:

准备返回

说下返回值把,返回值其实是别放在了eax寄存器中进行了返回,所以eax的值,就是0xC

 接着我们继续分析汇编:

分析下:首先是把变量 c 的值放入eax寄存器中,接着我们弹出edi,esi,ebx这些都不管,重点看下,mov esp ,ebp这句代码,这个就是把ebp的地址赋值给esp,相当于esp 和 ebp同时指向了MyAdd的栈低处,也就是释放了栈帧空间, 然后pop ebp,就是弹栈,ebp处是栈底,栈底就是main函数的栈底,也就是说pop后,ebp又回到了main函数的栈底,而esp就在call的下一条指令处啦。

如图所示:

ret之后

ret其实就是类似pop一样,就是把esp的值再向下移动

如图:

返回call的下一条指令处

我们ret之后返回到了call的下一条指令处

接着看汇编,把esp的值加 8 ,其实就是把所形成的临时变量销毁。

把eax的值移动到 ebp - 20,其实上就是把寄存器中0xC的值移动到变量z中去。

看示意图:

 剩下的打印不在叙述啦。

剩下一个有意思的证明:

形参实例化时,是从左到右,连续排布的。

int MyAdd(int x, int y)
{
	printf("Before:%d\n", y);
	*(&x + 1) = 100;
	printf("After: %d\n", y);

	return 0;
}

本篇总结:

首先我们从汇编底层来看和了解了函数栈帧的创建和销毁,其中每个细节,每个汇编指令做出了解释和相对性的示意图,希望大家有所收获吧。

下期预告:

下期会更新可变参数列表的相管内容

猜你喜欢

转载自blog.csdn.net/m0_64770095/article/details/124395143