栈帧的创建与销毁干货!

在学期的过程中,我们可能有很多困惑?

比如:

  • 局部变量是怎么创建的?

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

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

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

  • 函数调用是怎么做的?

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

    当看完这篇文章,学习了函数栈帧的创建与销毁后,上面的问题,自然水落石出,对于接下来的学习,会有很大的帮助。

前言

接下来我要使用的是VS2013的环境,不使用太高级的编译器,是因为越高级的编译器,越不容易学习和观察。同时在不同的编译器下,函数调用过程中栈帧的创建是有差异的,具体细节取决于编译器的实现。

铺垫

为了能看懂接下来的内容,我们先来了解以下几个知识。

1.栈区的使用习惯

在这里插入图片描述
我们知道堆区和栈区是相对着使用空间的,堆区从低地址到高地址,而栈区从高地址到低地址。
每个函数的调用,都需要在栈区中开辟一个空间,而这个空间的开辟,是要从高地址向低地址执行。也就是说调用函数时,会从栈区的高地址处开辟空间。

栈帧

栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。
从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。
---------------------------------说白了就是用来记录的。

寄存器

寄存器是CPU内部用来存放数据的一些小型存储区域,用来占时存放参与运算的数据和运算结果。
常见的寄存器有:eax,ebx,ecx,edx,ebp,esp。
其中 ebp 与esp 这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的。ebp通常指向栈低,存储栈低的地址,又称栈低指针。esp 通常指向栈顶,存储栈顶地址,又称栈顶指针

汇编指令

其他指令请点击https://blog.csdn.net/sinat_27382047/article/details/72810788

接下来要用到的汇编指令
先不用看,等用到了再看
1.add:加法指令,第一个是目标操作数,第二个是源操作数,格式为:目标操作数 = 目标操作数 + 源操作数;
2.sub:减法指令,格式同 add;
3.call:调用函数,一般函数的参数放在寄存器中;
4.ret:跳转会调用函数的地方。对应于call,返回到对应的call调用的下一条指令,若有返回值,则放入eax中;
5.push:把一个32位的操作数压入堆栈中,这个操作在32位机中会使得esp被减4(字节),esp通常是指向栈顶的(这里要指出的是:学过单片机的同学请注意单片机种的堆栈与Windows下的堆栈是不同的,请参考相应资料),这里顶部是地址小的区域,那么,压入堆栈的数据越多,esp也就越来越小;
6.pop:与push相反,esp每次加4(字节),一个数据出栈。pop的参数一般是一个寄存器,栈顶的数据被弹出到这个寄存器中;
一般不会把sub、add这样的算术指令,以及call、ret这样的跳转指令归入堆栈相关指令中。但是实际上在函数参数传递过程中,sub和add最常用来操作堆栈;call和ret对堆栈也有影响。
7.mov:数据传送。第一个参数是目的操作数,第二个参数是源操作数,就是把源操作数拷贝到目的一份。
8.lea:取得第二个参数地址后放入到前面的寄存器(第一个参数)中。

        然而lea也同样可以实现mov的操作,例如:

                              lea edi,[ebx-0ch]

方括号表示存储单元,也就是提取方括号中的数据所指向的内容,然而lea提取内容的地址,这样就实现了把(ebx-0ch)放入到了edi中,但是mov指令是不支持第二个操作数是一个寄存器减去一个数值的。

有了上面的知识,接下来我通过一段代码来讲解,栈帧的创建与销毁

#include<stdio.h>

int Add(int a, int b)
{
	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);
}

因为每次调用函数的时候都要在栈区开辟空间

首先调用main函数 ,ebp esp 存放着一个指向main函数这块空间的地址,
所以 在调用main函数时 ebp esp 这时候用来维护 main函数在栈帧中的这块空间如下图

在调用那个函数,ebp esp 就去维护那个空间,例如接下来就要调用add函数,这时候就ebp esp 就要去维护add函数空间了
在这里插入图片描述

所以我们就了解了 原来ebp esp这两个寄存器是用来维护函数栈帧的。
通常 我们把这个ebp叫做栈低指针 esp叫做栈顶指针

int main()
{
009219F0  push        ebp  
009219F1  mov         ebp,esp  
009219F3  sub         esp,0E4h  
009219F9  push        ebx  
009219FA  push        esi  
009219FB  push        edi  
009219FC  lea         edi,[ebp-0E4h]  
00921A02  mov         ecx,39h  
int main()
{
00921A07  mov         eax,0CCCCCCCCh  
00921A0C  rep stos    dword ptr es:[edi]  
	int a = 10;
00921A0E  mov         dword ptr [a],0Ah  
	int  b = 20;
00921A15  mov         dword ptr [b],14h  
	int c = 0;
00921A1C  mov         dword ptr [c],0  
	c = Add(a, b);
00921A23  mov         eax,dword ptr [b]  
00921A26  push        eax  
00921A27  mov         ecx,dword ptr [a]  
00921A2A  push        ecx  
00921A2B  call        _Add (09211DBh)  
00921A30  add         esp,8  
00921A33  mov         dword ptr [c],eax  
	
	printf("%d\n", c);
00921A36  mov         esi,esp  
00921A38  mov         eax,dword ptr [c]  
00921A3B  push        eax  
00921A3C  push        925858h  
00921A41  call        dword ptr ds:[929114h]  
00921A47  add         esp,8  
00921A4A  cmp         esi,esp  
00921A4C  call        __RTC_CheckEsp (0921136h)  
}
00921A51  xor         eax,eax  
00921A53  pop         edi  
00921A54  pop         esi  
00921A55  pop         ebx  
}
00921A56  add         esp,0E4h  
00921A5C  cmp         ebp,esp  
00921A5E  call        __RTC_CheckEsp (0921136h)  
00921A63  mov         esp,ebp  
00921A65  pop         ebp  
00921A66  ret  

在这里插入图片描述
从这张图可以看出,main函数是被调用的,那么是被谁调用?
在这里插入图片描述
在VS2013 main函数也是被别调用的 是
_tmainCRTStartup 这个然后又被
mainCRTStartup调用
在这里插入图片描述
大概就是怎么一个逻辑,
接来进行详细的讲解

main函数是被别调用的,那么调用它的函数的栈帧就已经创建好了
如图
在这里插入图片描述
接下里执行汇编语言第一句

002119F0  push        ebp  
002119F1  mov         ebp,esp  
002119F3  sub         esp,0E4h  
002119F9  push        ebx  
002119FA  push        esi  
002119FB  push        edi  
002119FC  lea         edi,[ebp+FFFFFF1Ch]  
00211A02  mov         ecx,39h  
00211A07  mov         eax,0CCCCCCCCh  
00211A0C  rep stos    dword ptr es:[edi]  
002119F0  push        ebp  

这句话是什么意思呢?
push:把一个32位的操作数压入堆栈中,这个操作在32位机中会使得esp被减4(字节),esp通常是指向栈顶的(这里要指出的是:学过单片机的同学请注意单片机种的堆栈与Windows下的堆栈是不同的,请参考相应资料),这里顶部是地址小的区域,那么,压入堆栈的数据越多,esp也就越来越小;
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
esp的地址发生了变化,压上去的ebp 里存放的就是_tmain的ebp的值0x012ffe7c

002119F1  mov         ebp,esp 

把esp 的值放到ebp中
所以这时候ebp就指向
在这里插入图片描述
ebp esp是寄存器 ,里面的值发生改变,指向的目标也会改变
在这里插入图片描述
mov以后 esp ebp的值相同了

002119F3  sub         esp,0E4h 
// 继续下一条

esp 减去 0E4h 后就变成
在这里插入图片描述
这意味着esp指向的位置要向上指了
在这里插入图片描述
终于为main函数开辟空间了
接下来三个push
在这里插入图片描述
至于edi esi edx 里面放的是什么 不用管它
它们一会 会通过 POP 弹出去 ’

002119FC  lea         edi,[ebp-0E4h]  
00211A02  mov         ecx,39h  
00211A07  mov         eax,0CCCCCCCCh  
00211A0C  rep stos    dword ptr es:[edi]

上面的意思是 从ebp-0E4h一下的39h(57)次的dword(4个字节)空间都要变成的eax(0cccccccch)的内容 0E4h=57个字节 =228个bit
在这里插入图片描述
在这里插入图片描述
0x00CFFAE4就是 esp

push 就是压栈 给栈顶上放一个元素
pop 就是出栈 从栈顶删除一个元素

main函数已经开辟了
也进行了初始化了
接下来执行main语句块

局部变量的初始化就是这样创建的

在这里插入图片描述

调用Add函数

当我们创建好了a b c变量后,接下来就要调用Add函数了

函数调用前先进行传参

00211A23  mov         eax,dword ptr [ebp-14h]  
00211A26  push        eax  
00211A27  mov         ecx,dword ptr [ebp-8]  
00211A2A  push        ecx  

把b的值放到 寄存器eax 里 ,然后 把eax的值压栈
把a的值放到 ecx 里 , 把ecx 压栈
在这里插入图片描述

00211A2B  call        002111DB  
00211A30  add         esp,8  
00211A33  mov         dword ptr [ebp-20h],eax  

call指令调用函数 ,先压栈存放下一条指令的地址 00211A30
为什么要存放这个地址 ,因为调用完Add函数后返回的时候计算机可以通过这个地址,找到下一条执行的语句,继续往下执行
在这里插入图片描述
现在才正式进入Add函数里

int Add(int a, int b)
{
002113C0  push        ebp  
002113C1  mov         ebp,esp  
002113C3  sub         esp,0CCh  
002113C9  push        ebx  
002113CA  push        esi  
002113CB  push        edi  
002113CC  lea         edi,[ebp+FFFFFF34h]  
002113D2  mov         ecx,33h  
002113D7  mov         eax,0CCCCCCCCh  
002113DC  rep stos    dword ptr es:[edi]  
	int z = 0;
002113DE  mov         dword ptr [ebp-8],0  
	z = a + b;
002113E5  mov         eax,dword ptr [ebp+8]  
002113E8  add         eax,dword ptr [ebp+0Ch]  
002113EB  mov         dword ptr [ebp-8],eax  
	return z;
002113EE  mov         eax,dword ptr [ebp-8]  
}

下面的就是为Add函数创建栈帧

002113C0  push        ebp  
002113C1  mov         ebp,esp  
002113C3  sub         esp,0CCh  
002113C9  push        ebx  
002113CA  push        esi  
002113CB  push        edi  
002113CC  lea         edi,[ebp+FFFFFF34h]  
002113D2  mov         ecx,33h  
002113D7  mov         eax,0CCCCCCCCh  
002113DC  rep stos    dword ptr es:[edi]

在这之前我们应该知道,我们电脑为main函数开辟空间的范围
如图黄色就是
在这里插入图片描述

	int z = 0;
002113DE  mov         dword ptr [ebp-8],0  
	z = a + b;
002113E5  mov         eax,dword ptr [ebp+8]  
002113E8  add         eax,dword ptr [ebp+0Ch]  
002113EB  mov         dword ptr [ebp-8],eax  
	return z;
002113EE  mov         eax,dword ptr [ebp-8]  
}

在调用add之前,先传参,为add创建空间,执行a+b时找回,之前传参时压上去的值,压上去的值就等于a b ,所以说,形参只是实参的一份临时拷贝而已。
在这里插入图片描述

返回的值是如何带回去的

通过eax

如何回收空间的

002113F1  pop         edi  
002113F2  pop         esi  
002113F3  pop         ebx  
002113F4  mov         esp,ebp  
002113F6  pop         ebp  
002113F7  ret  

执行完pop三条后
在这里插入图片描述
然后把ebp的值赋给esp
接下来关键来了

002113F6  pop         ebp 

先让esp +4 ,把弹出元素 ,把元素放到ebp中,也就是把ebp-main 放到ebp中,这时候ebp瞬间指向 维护main函数了
在这里插入图片描述

形参是如何销毁的

00211A30  add         esp,8 

Add函数空间回收后,ret ,弹出,执行弹出元素的执行地址,
在这里插入图片描述
最后就把eax的值赋值给了c;

============================================
上述描述的比较通俗,只是讲解了每条汇编指令具体是怎么做的,
如果上述描述不清楚,可以使用VS2013,自己跟着走一遍

下面是问题的解答
在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

2023框框

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值