函数栈帧(详解版)

发布于:2022-11-08 ⋅ 阅读:(311) ⋅ 点赞:(0)


前言

前期学习的时候,我们可能有很多困感?
比如:
●局部变量是怎么创建的?I为什么局部变量的值是随机值?

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

●形参和实参是什么关系?

●函数调用过程是怎样的?

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

学习函数栈帧的创建和销毁,其实就是修炼了自己的内功,也能搞懂后期更多的知识。我使用的环境是vs2019,不要使用太高级的编译器,越高级的编译器,越不容易学习和观察。同时在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现。


1.浅谈C语言内存

1.1 内存分配

在C语言中内存分别分为栈区(stack)、堆区(heap)、未初始化全局数据区、已初始化全局数据区、静态常量区(static)、代码区(data)。

1.2 栈

栈区(stack):存放函数参数和局部变量,以及函数调用开辟的栈帧;函数结束返回时自动释放空间。先进后出,向地址减小方向增长(由高地址到低地址),每一个函数调用都要在栈区开辟一块空间。

1.3 寄存器

寄存器往往直接被入在CPU逻辑电路中,速度量快,便于CPU快速调用。例如,我们今天会用到的eax,eba,ecx,edx以及比较重要的esp(栈顶指针),ebp(栈底指针)。这两个寄存器中存放的是地址,是用来维护函数栈帧的。

2. 为main()函数开辟栈帧

我们以以下这段代码为例:

int ADD(int x, int y)
{
	int z = 0;
	z = x + y;
	return z;
}
int main()
{
	int a = 20;
	int b = 10;
	int c = 0;
	c = ADD(a, b);
	printf("%d", c);
	return 0;
}

我们将上述代码的反汇编码调用出来,方便我们理解

int main()
{
	007518B0  push        ebp**//压入一个ebp(栈底)指针**
		007518B1  mov         ebp, esp**//使得ebp,esp指向同一位置**
		007518B3  sub         esp, 0E4h**//esp减0E4h(十六进制),也就是为main开辟了一块(esp-0E4h)这么大的空间

如图,为main函数栈帧的开辟过程。

		007518B9  push        ebx//压入一个ebx
		007518BA  push        esi//压入一个esi
		007518BB  push        edi//压入一个edi

		007518BC  lea         edi, [ebp - 24h]//将[ebp - 24h]这个地址放入edi中
		007518BF  mov         ecx, 9
		007518C4  mov         eax, 0CCCCCCCCh                               
		007518C9  rep stos    dword ptr es : [edi]//从edi往下,每次初始化四个字节,初始化ecx次这么多数据,全部初始化为 CCCCCCCC
/也就是说为main函数开辟的空间全部初始化为CCCCCCCC

如图,为main函数函数栈帧的初始化过程。
在这里插入图片描述

3.变量的初始化及函数调用

        int a=10;
	007518D5  mov         dword ptr[ebp - 8], 0Ah//将0Ah(也就是a的值)放在[ebp - 8]这个位置
		int b = 20;
	007518DC  mov         dword ptr[ebp - 14h], 14h//将14h(也就是b的值)放在[ebp - 14h]这个位置
		int c = 0;
	007518E3  mov         dword ptr[ebp - 20h], 0//将0(也就是c的值)放在[ebp - 20h]这个位置
		c = ADD(a, b);
	007518EA  mov         eax, dword ptr[ebp - 14h]//把b即([ebp - 14h])放到eax中
		007518ED  push        eax//在栈顶压入一个eax
		007518EE  mov         ecx, dword ptr[ebp - 8]//将a放在ecx中
		007518F1  push        ecx//在栈顶压入一个ecx
		007518F2  call        00751217//先将call指令的下一条语句的地址压入,然后调用add函数
		007518F7  add         esp, 8
		007518FA  mov         dword ptr[ebp - 20h], eax

相信不少小伙伴会疑惑为什么在执行call指令时,要先在栈顶压入call指令下一条语句的地址:,我们知道,我们的代码在编译器中是一条接着一条像下处理编译的,当我们调用结束ADD函数后,要继续执行ADD函数的下一条指令,这是我们提前保存到这个地址就起到了作用,方便我们快速的找到下一条指令,通俗讲就是从哪出去,从哪回来。
在这里插入图片描述

4.ADD函数

4.1 ADD函数的创建

int ADD(int x, int y)
{
	    00F81770  push        ebp//压入一个ebp
		00F81771  mov         ebp, esp//使得压入的ebp指向和esp一样的位置
		00F81773  sub         esp, 0CCh//esp减0CC(十六进制)开辟了一块空间
		00F81779  push        ebx//压入一个ebx
		00F8177A  push        esi//压入一个esi
		00F8177B  push        edi//压入一个edi
		00F8177C  lea         edi, [ebp - 0Ch]//将[ebp - 0Ch]这个地址放入edi中
		00F8177F  mov         ecx, 3
		00F81784  mov         eax, 0CCCCCCCCh
		00F81789  rep stos    dword ptr es : [edi]//将edi往下,每次初始化四个字节,初始化ecx次这么多数据,全部初始化为 0CCCCCCCCh

4.2 ADD函数使用与销毁

		int z = 0;
	00F81795  mov         dword ptr[ebp - 8], 0//将0放入[ebp - 8]中
		z = x + y;
	00F8179C  mov         eax, dword ptr[ebp + 8]//将[ebp + 8](即a)放在eax中
		00F8179F  add         eax, dword ptr[ebp + 0Ch]//将[ebp + 8](即a)与[ebp + 0Ch](即b)加起来
		00F817A2  mov         dword ptr[ebp - 8], eax//将eax放在[ebp - 8]的位置(即z的位置)
		return z;
	00F817A5  mov         eax, dword ptr[ebp - 8]//将[ebp - 8](即z)放在eax中,由于z是定义在ADD函数中的,因此出ADD函数z就会销毁,所以要将结果放在寄存器eax中
}
008917A8  pop         edi//将edi弹出
008917A9  pop         esi//将esi弹出
008917AA  pop         ebx//将ebx弹出
008917B8  mov         esp, ebp//使ebp指向esp的位置,即为将ADD开辟的函数栈帧销毁
008917BA  pop         ebp//将ebp弹出,使得ebp返回到main()函数ebp的位置
008917BB  ret//从栈顶弹出我们在main()函数中压入的call指令的下一指令的地址,使得我们找到call指令的下一条指令的位置

如图,为ebp指向esp的位置,即为将ADD开辟的栈帧销毁的示意图。
ebp指向esp的位置,为ADD开辟的栈帧销毁
如图,左下的ebp就是上述的" 008917BA pop ebp//将ebp弹出,使得ebp返回到main()函数ebp的位置 ",的示意图 。
在这里插入图片描述

 printf("%d", c);
 008918FD  mov         eax, dword ptr[ebp - 20h]//将eax(即z)放在[ebp -       20h](即c)中
 00891900  push        eax//将eax弹出
 00891901  push       
 897B30h/00891906  call        008910CD
 0089190B  add         esp, 8
 return 0;
 0089190E xor eax, eax
}

main()函数的销毁与ADD函数的销毁过程一样,我在这里就不在讲解。

5.总结

通过本文的讲解,我们最终了解了为什么创建的局部变量是随机值;函数是怎样传参的:在传值传参中,形参是实参的一份临时拷贝,传参的顺序是从右往左的,等等这样我们前面提到的问题。
最后,创作不易,希望大家能够点赞,关注,,如有错误,或不清楚的地方希望各位读者能够指出来,我会一 一回复。

本文含有隐藏内容,请 开通VIP 后查看