函数栈帧的创建和销毁

10 篇文章 0 订阅

注意:本片只考虑局部变量,不考虑静态,全局变量等

困惑:

  • 局部变量是怎么创建的?
  • 为什么局部变量的值是随机值?
  • 函数是怎么传参的?传参的顺序是怎样的?
  • 形参和实参是什么关系?
  • 函数调用是怎么做的?
  • 函数调用是结束后怎么返回的?

注意:函数栈帧的创建和销毁在不同编译器下是略有差异的

函数栈帧理解前的铺垫:

寄存器:(集成到cpu中的,任何代码都可以使用)

  1. EAX (Extended Accumulator):扩展累加器,用于算术和逻辑运算。在32位模式下,它与8位的 AL 和16位的 AX 寄存器相关联。

  2. EBX (Extended Base):扩展基址寄存器,通常用于存储数组索引或内存地址。

  3. ECX (Extended Counter):扩展计数寄存器,常用于循环计数。

  4. EDX (Extended Data):扩展数据寄存器,用于存储数据,特别是在输入/输出操作中。

  5. EBP (Extended Base Pointer):扩展基址指针,通常用作栈帧的基址指针,用于访问局部变量和函数参数。

  6. ESP (Extended Stack Pointer):扩展栈指针,指向当前栈顶的地址。

函数栈帧:

EBP,ESP这两个寄存器中存放的是地址,这两个地址是用来维护函数栈帧的

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

下面,我们写一个足够简单的代码来观察:

#include<stdio.h>

int Add(int a, int b)
{
	return a + b;
}

//精华讲解改为:
int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}

int main()
{
	int a = 0;
	int b = 20;
	int c = 0;
	c = Add(a, b);
	printf("%d\n", c);
	return 0;
}

ebp,esp寄存器的作用:维护调用的函数的函数栈帧: 

我们可以使用调试窗口中的调用堆栈观察函数调用的顺序: 

其实在VSmain函数也是由别的函数调用的:(main函数的调用关系)

因为函数在栈上是要分配空间的,也就是说:

函数栈帧的创建:

通过以上的简单理解,我们可以通过反汇编来更好的理解函数栈帧的创建:

在调用main函数之前,调用的函数是_mainCRTStartup,既然进入到main函数了,_mainCRTStartup和mainCRTStartup的函数栈帧就已经创建好了,那么,_mainCRTStartup就应该有ebp,esp指针来维护:

进入main函数后的第一步是push,也就是压栈操作:

push:ebp

:后的变化:(push操作后,esp的指向位置会发生改变)

mov:ebp,esp

:接下来是mov指令,mov指令是把esp的值给ebp,也就是ebp存放的指针就不再指向原来的位置,而是指向现在esp指向的位置:

 

sub:esp,0E4h

:sub其实就是减操作,就是将esp减去0E4h(十六进制数字):也就是原来的esp变小了,不再指向原来位置:esp和ebp也就维护了一段新的空间,这是为main函数开辟好的空间:

后面的三个push:也就是压栈3个元素:

lea指令也叫load effective address(加载有效地址) ,也就是把edi后面的地址加载到edi,简单来说,就是给edi一个地址:其实给edi的地址是【ebp-0E4h】,也就是存放指向原来esp(上图中打岔)位置:

然后又mov了ecx,eax,将其对应的值给到了本身:

接着,rep stos:dword ptr es:[edi](重要操作):要把从刚才的edi位置开始,向‘下’的ecx,也就是39h这么多个dword的数据全部改成eax的内容(0CCCCCCCCh),注意:一个word是2个字节,一个dword是4个字节:(每次初始化4个字节--->0CCCCCCCCh)

到这一步,已经是为main函数的开辟准备好了,接下来就是要执行正式有效的代码了:

int a = 10;

对应的汇编代码就是:

dward ptr [a],0Ah

去掉符号名:

dward ptr [ebp-8],0Ah

 就是把0Ah这个十六进制数字放到ebp-8指向的位置上:

剩下的两条指令也是如此:

这是接下来的汇编代码:

mov   eax,dword ptr [ebp-14h]

注意:这里的ebp-14h便是上面b的位置,这条指令的意思就是把【ebp-14h】的值放到eax里边去,也就是把b的值(20)放到eax里边去了,接着:

push eax

也就是压栈,把eax放到栈顶,push操作伴随着esp的位置改变:

接下来的指令:

mov   ecx,dword ptr [ebp-8]
push   ecx

也是同理:将a的值(1下划线0)放到ecx里边去:

那么,这两个动作是不是在传参呢?答案是确实是在传参

可以看出:传参是从右向左传参的

接下来的指令:

call 00C210E1

call指令就是要调去用函数区了,在此之前,我们应该注意这条指令对应的地址:

00C2144B

 接下来,在调试(F11)下,就是执行call指令:

注意红色区域:call就把他的下一个地址add对应的地址压栈了,也就是:

这样的目的是为了call完后,程序能回到对应的位置,而压栈了call指令的下一条指令的地址,就可以保证,程序能够回到正确位置,接着调试,就进入到真正的Add函数里了:

这些指令也和main函数栈帧的创建思路一致:

push   ebp

本来维护main函数的ebp进入到了压栈操作:为维护Add函数准备:

以下操作省略:

接下来执行计算操作:我们的x,y其实就是:

 

也就是形参是实参的一份临时拷贝--->改变形参不会影响实参

接下来的操作:将值返回:

return z

mov   eax,dward ptr [ebp - 8]

return z的意思就是把ebp-8位置的值先给eax,不然z这个值出了Add函数就要销毁了

函数栈帧的销毁: 

紧接着上述操作:要进行Add函数栈帧的销毁了

pop edi

就是将栈顶元素弹出,弹出到edi(edi弹出到edi),esp++

以下2个pop同理 :

接下来:

mov   esp,ebp

 将ebp的值赋值给esp,也就是esp指向ebp的位置,原本的空间得到释放回收:

接下来的pop指令,就是要pop掉栈顶:ebp->main,这就指令就可以使ebp退回指向原来维护的main函数位置,pop带着esp++ :

接下来的ret指令就是跳出Add到main函数栈帧了,这里靠的是call指令想在放在栈顶的地址,也就是说,存这个地址就是为了调用完函数,程序还可以回到原来位置

接下来的add指令:

add   esp,8

就是esp+8:esp位置变化:

形参空间也就是被销毁了

接下来就把eax的值给了c

本篇逐步演示了栈帧的创建和销毁,感谢观看

本项目是一个基于SSM(Spring+SpringMVC+MyBatis)后端框架与Vue.js前端框架开发的疫情居家办公系统。该系统旨在为居家办公的员工提供一个高效、便捷的工作环境,同时帮助企业更好地管理远程工作流程。项目包含了完整的数据库设计、前后端代码实现以及详细的文档说明,非常适合计算机相关专业的毕设学生和需要进行项目实战练习的Java学习者。 系统的核心功能包括用户管理、任务分配、进度跟踪、文件共享和在线沟通等。用户管理模块允许管理员创建和管理用户账户,分配不同的权限。任务分配模块使项目经理能够轻松地分配任务给团队成员,并设置截止日期。进度跟踪模块允许员工实时更新他们的工作状态,确保项目按计划进行。文件共享模块提供了一个安全的平台,让团队成员可以共享和协作处理文档。在线沟通模块则支持即时消息和视频会议,以增强团队之间的沟通效率。 技术栈方面,后端采用了Spring框架来管理业务逻辑,SpringMVC用于构建Web应用程序,MyBatis作为ORM框架简化数据库操作。前端则使用Vue.js来实现动态用户界面,搭配Vue Router进行页面导航,以及Vuex进行状态管理。数据库选用MySQL,确保数据的安全性和可靠性。 该项目不仅提供了一个完整的技术实现示例,还为开发者留下了扩展和改进的空间,可以根据实际需求添加新功能或优化现有功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值