首页 > 其他分享 >函数的栈帧空间创建与销毁全过程(详解~)

函数的栈帧空间创建与销毁全过程(详解~)

时间:2024-11-09 17:45:53浏览次数:3  
标签:销毁 函数 esp mov eax 详解 ebp 全过程 push

目录

一.什么是函数栈帧?

二.理解函数栈帧的创建能解决哪些问题?

三.创建函数栈帧空间的之前认知

3.1 什么是栈

3.2认识相关寄存器

3.3 汇编指令

四.创建和销毁全过程

4.1预备知识

4.1.1调用堆栈

4.2打开反汇编

4.3函数栈帧创建

​编辑

4.4函数栈帧销毁


一.什么是函数栈帧?

        函数在c语言编程里面是一个具有单独功能不在main主函数中的一个独立存在,我们把它抽象的理解为函数,可以说c语言的程序实现是由一个个函数组成的。那什么是函数栈帧,其实就是c语言为调用函数时在程序调用栈中单独开辟的空间。这些空间里面存放着函数的形参,实参,函数返回值,变量以及esp,ebp等。

二.理解函数栈帧的创建能解决哪些问题?

在理解之前,在写函数的时候会有一些问题,比如:

函数的实参是怎么传给形参的?

传参的顺序是和函数实参顺序一致吗?

函数运行后返回的最终值是怎么传回到main函数的?

为什么没初始化的局部变量是随机的?

形参和实参是什么关系?

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

局部变量是怎么创建的?

......

在理解函数栈帧的创建与销毁后,这些问题可能会烟消云散。一起看看

三.创建函数栈帧空间的之前认知

3.1 什么是栈

        栈,应该都有所耳闻,程序员都曾听到全栈,函数栈的概念,栈,一种数据结构,可以将数据压栈,入栈(push),也可以将数据弹出(pop)栈。

        数据的出入规则是:先入栈的数据后出 栈( FIFO)。就像叠成一叠的书本放在一个箱子里,先叠上去的书在最下面,因此要最后才能取出。不能从底下去抽出来。 在计算机中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据 从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。 在windows系统中,栈总是向下增长(由高地址向低地址)的。即最下面为高地址,最上面是低地址。

3.2认识相关寄存器

        在栈帧空间中,这些寄存器是需要用的,需要提前了解,才能更好理解栈帧的运行和维护

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

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

ebp:栈底寄存器

esp:栈顶寄存器

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

3.3 汇编指令

mov:数据转移指令

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

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

sub:减法命令

add:加法命令

call:函数调用

1. 压入返回地址 2. 转入目标函数执行

ret:恢复返回地址,压入eip。

知道以上寄存器和指令(术语)才能帮助我们更好理解空间的每一步运行过程。

四.创建和销毁全过程

4.1预备知识

        在这之前,我们需要了解一些前提或者是认知才能理解函数栈帧空间的创建和销毁,ebp和esp是维护当前esp和ebp所在空间的栈顶和栈底,这是两个寄存器的“使命”,每次调用函数,都会为这次函数调用开辟空间,为函数栈帧的空间,如图所示

4.1.1调用堆栈

用一个典型的代码案例来演示

#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 3;
int b = 5;
int ret = 0;
ret = Add(a, b);
printf("%d\n", ret);
return 0;
}

调试进入Add函数后,我们就可以观察到函数的调用堆栈

是不是发现main函数的调用也是由其他函数__tmainCRTStartup调用的呢

_tmainCRTStartup也是被下面那个mainCRTStartup调用的。

在调用add函数之前,首先调用的main函数,main也是有属于自己的栈帧空间才对

4.2打开反汇编

图片演示

鼠标右击空白处,转到反汇编

去掉符号名,然后图中箭头所指的【a】就会不见,我们需要的是地址,不是字符

4.3函数栈帧创建

int main()
{
00BE1820 push ebp
00BE1821 mov ebp,esp
00BE1823 sub esp,0E4h
00BE1829 push ebx
00BE182A push esi
00BE182B push edi
00BE182C lea edi,[ebp-24h]
00BE182F mov ecx,9
00BE1834 mov eax,0CCCCCCCCh
00BE1839 rep stos dword ptr es:[edi]
int a = 3;
00BE183B mov dword ptr [ebp-8],3
int b = 5;
00BE1842 mov dword ptr [ebp-14h],5
int ret = 0;
00BE1849 mov dword ptr [ebp-20h],0
ret = Add(a, b);
00BE1850 mov eax,dword ptr [ebp-14h]
00BE1853 push eax
00BE1854 mov ecx,dword ptr [ebp-8]
00BE1857 push ecx
00BE1858 call 00BE10B4
00BE185D add esp,8
00BE1860 mov dword ptr [ebp-20h],eax
printf("%d\n", ret);
00BE1863 mov eax,dword ptr [ebp-20h]
00BE1866 push eax
00BE1867 push 0BE7B30h
00BE186C call 00BE10D2
00BE1871 add esp,8
return 0;
00BE1874 xor eax,eax
}

以上是main函数转化为汇编的所有代码,那么接下来干嘛?拆

注:vs每个编译器执行f10调试的时候会重新分配内存,所以每次汇编代码地址是有差异的。这个是没有什么影响的。

在没有执行进入main函数的第一个push压栈指令之前,esp和ebp的地址是这样的

esp:0x008ffba8

ebp:0x008ffbf4

push  ebp,此时esp的地址应该是往上走,因为你把ebp压进去了,而地址从上往下是由低到高,所以往上是低地址,是减。

a8-a4,是不是减了4

而在内存中,可以看到ebp确实被压进去了(监视器里看内存变化)

看之前的代码,下一个指令是mov  ebp,esp,把esp的值转移给ebp

把栈顶的值给栈底,即ebp要往上走代替esp的值(为main函数开辟空间做准备了)

sub  esp,0E4h,给esp减去0E4h,转化为16进制是228,就是把esp减去那么多个字节,减就是往上走

地址变化了

图例:

这个时候esp在最上面

三个push ebx  esi   edi每压栈一次,esp就会往上走一次,共走三次

注意 ,这时esp的值应该是-12,压栈一次减4个字节

lea edi,[ebp-0E4h],加载有效地址,将edi的地址变成ebp-0E4h

mov ecx  ,39h

mov eax,0CCCCCCCCh

rep stos dword ptr es:[edi]

39h存放在ecx里,0CCCCCCCCh存放在eax寄存器里,dword是一个word两个字节,double倍

这前三个行指令意思把edi位置往下的ecx次(转化为10进制就是57),57次,每一次都是double word,就是一次传4个字节,全部变成CCCCCCCC给到eax,到ebp往上的所有内容57*4个字节

以上都是为main函数栈帧的开辟

变量a的创建,在ebp-8的位置放入3,一个格子代表一个字节

变量b的创建,在ebp-14h地址处放入5

要是没有把a,b变量初始化,就是随机值cccccccccc,“烫烫烫烫烫烫烫烫烫烫”

变量c创建同理

整体效果是这样的:

接下来调用add函数,传参就是把实参数的值push到栈帧空间

 mov   eax ,dword ptr [ebp-14h],这个ebp-14h是b,把b的值放到eax寄存器里

push!在栈顶上压栈,压的是eax(实参b),之前esp的值是0x008ffaB4,把14压在了esp上

同理,mov  ecx,dword ptr [ebp-8],[ebp-8]是a,把a的值放入ecx寄存器里

push!在eax上压栈,压的是ecx(实参a),esp同步往上移位 

call,函数调用,记住这个call之前的00BE1858,后面有用

call指令的作用是将call指令下一条指令的地址入栈,是因为call执行时会进入Add指令的内部,在Add指令走完之后会回到主函数中,按F11走进Add

红线标的那条地址入栈,压在ecx上,这个地址是需要记住主函数中指令走到哪里的位置,因此需要将下一步指令的地址入栈,才能回来,这里逻辑是很严密的。

F11走进去,来到Add函数中

int Add(int x, int y)
{
00BE1760 push ebp 
00BE1761 mov ebp,esp 
00BE1763 sub esp,0CCh 
00BE1769 push ebx 
00BE176A push esi 
00BE176B push edi 
int z = 0;
00BE176C mov dword ptr [ebp-8],0 
z = x + y;
00BE1773 mov eax,dword ptr [ebp+8] 
00BE1776 add eax,dword ptr [ebp+0Ch] 
00BE1779 mov dword ptr [ebp-8],eax 
return z;
00BE177C mov eax,dword ptr [ebp-8] 
}
00BE177F pop edi
00BE1780 pop esi
00BE1781 pop ebx
00BE1782 mov esp,ebp
00BE1784 pop ebp
00BE1785 ret

这里其实和main函数的栈帧空间创建原理一样

push ebp 压栈

原本的ebp是在维护main函数的,现在要压main的ebp上来,esp地址变小,然后mov esp 给ebp,赋值给add的ebp

下一步,同样是sub,把esp减去0CCh个值,给add函数创造栈帧空间

这里是重新计算add里esp和ebp的位置

执行3个push,将ebx,esi,edi入栈,esp继续向上走,最后形成空间

z临时变量的创建,把0放入ebp-8的位置上

注意下一步的操作,

eax,dword ptr [ebp+8],是把ebp+8的值放在eax里,是在找回原来压栈a的参数

eax,dword ptr [ebp+0Ch],把ebp+0Ch加给eax,就是把原来压栈b的参数给eax,形成a+b=8

最后,命令eax的值放入ebp-8,意思是把a+b的值赋值给z!

我们在调用add函数的时候,就已经铜鼓传参的形式把a,b的值传到add里并且push入栈了,所以说形参是实参的一份临时拷贝是完全正确的。

接下来return z

这里很奇妙,因为返回就意味着销毁,所以是把z的值ebp-8暂时放在寄存器里来进行返回,add销毁但不意味着寄存器eax销毁

4.4函数栈帧销毁

接下来执行三个连续的pop

pop edi    pop esi pop ebx,弹出,此时esp会加12,每次弹出加4

mov esp,ebp 再将ebp的值给esp,esp继续往下,回收add栈帧空间

pop ebp  弹出ebp,此时栈顶刚好指的是main函数的ebp,弹出后ebp就会回到原来的main函数里。(ebp重新开始维护main的栈底,esp重新开始维护栈顶)

ret指令的执行,之前我们就已经把call指令的下一条指令入栈了,pop ebp后此时栈顶的值刚好是指令的地址,然后直接跳转到主函数call指令下一条指令的地址处,继续往下执行。

在ret执行结束之后弹出栈顶元素,Add函数栈帧销毁了。

回到main函数的时候,可以看到:

add esp,8,add函数都调用完了,地址已经弹出了,esp+8,继续往下

        把eax移动到ebp-20h,就是把之前存放在eax的值给ebp-20h,等于存放到main的ret,eax保存的数据是add函数x+y的值,它是由寄存器带回来的,所以从里面读取出来。

        最后,我们需要销毁main函数的栈帧,道理和Add函数栈帧的销毁是一样的,这里就不过多解释了。

那么来回答一下二的问题

1.局部变量是怎么创建的?

答:是函数分配好栈帧空间后,初始化了我们的空间内部cccccc值,然后给我们的变量对应的分配内存

2.函数的实参是怎么传给形参的?

答:是在没调用Add函数时(没有call),通过实参传给寄存器然后从右到左push到Add的栈帧空间,在Add函数里,是通过指针偏移量ebp+8,ebp+12找到了原本已经压栈的a,b值。

3.传参的顺序是和函数实参顺序一致吗?

答:不一致,本案例是先b再a push进Add函数

4.函数运行后返回的最终值是怎么传回到main函数的?

答:通过寄存器eax返回

5.为什么没初始化的局部变量是随机的?

答:没初始化前,变量都是随机放入的,不初始化,就没有给局部变量分配内存,当然是随机的。

6.形参和实参是什么关系?

答:形参是在压入栈中的一片空间,他只是拷贝了实参的值,空间是独立的,改变形参不会影响实参。

7.函数调用结束怎么返回的?

答:把call指令的下一条指令记录入栈了,然后跳转到函数中去,先将实参拷贝压入栈中,再将调用这个函数的ebp压入栈中,给函数开辟空间。
函数调用结束,弹出ebp,这个ebp刚好为栈顶main函数的epb,弹出,ret指令首先从栈顶弹出一个值,此时栈顶的值就是是call指令的下一个地址,弹出后可以直接跳转到主函数call指令的下一个地址,可以继续执行。

ok,结束,感谢观看!

标签:销毁,函数,esp,mov,eax,详解,ebp,全过程,push
From: https://blog.csdn.net/2301_76684563/article/details/143623827

相关文章

  • KCP详解
    1.介绍        KCP是一种在应用层的旨在优化网络传输性能的快速的可靠的协议,KCP本身并不会直接处理底层网络通信,而是作为一个中间层协议,其通常基于UDP,这意味着用户要自己定义底层的发送方式,并且通过回调传递给KCP。2.KCP原理    2.1网络传输如何做到可靠 ......
  • Nuxt.js 应用中的 listen 事件钩子详解
    title:Nuxt.js应用中的listen事件钩子详解date:2024/11/9updated:2024/11/9author:cmdragonexcerpt:它为开发者提供了一个自由的空间可以在开发服务器启动时插入自定义逻辑。通过合理利用这个钩子,开发者能够提升代码的可维护性和调试能力。注意处理性能、错误和环......
  • YOLO系列基础(一)卷积神经网络原理详解与基础层级结构说明
    系列文章地址YOLO系列基础(一)卷积神经网络原理详解与基础层级结构说明-CSDN博客YOLO系列基础(二)Bottleneck瓶颈层原理详解-CSDN博客目录卷积神经网络的原理及卷积核详解一、卷积神经网络的原理二、卷积层与卷积核详解卷积核的作用卷积核的设计卷积样例与代码说明:卷积核......
  • Python内置函数1详解案例
    1.列表的最值运算描述牛牛给了牛妹一个一串无规则的数字,牛妹将其转换成列表后,使用max和min函数快速的找到了这些数字的最值,你能用Python代码实现一下吗?输入描述:输入一行多个整数,数字之间以空格间隔。输出描述:输出这些数字的zuizhi示例1输入:35691062输出:10......
  • 鸿蒙next5.0版开发:ArkTS组件点击事件详解
    在HarmonyOS5.0中,ArkTS提供了一套完整的组件和事件处理机制,使得开发者能够创建交互性强的应用程序。本文将详细解读如何使用ArkTS组件处理点击事件,包括事件的注册、回调函数的编写以及事件对象的使用。点击事件基础点击事件是用户与应用交互的基本方式之一。在ArkTS中,点击......
  • 微信小程序scroll-view详解及案例
    需求:实现类似美团外卖。1.点击左侧菜单右侧滚动到对应内容。2.滚动右侧内容左侧对应菜单高亮。一、首先介绍下scroll-view可滚动视图区域。案例用到如下属性(如需了解更多请访问官网https://developers.weixin.qq.com/miniprogram/dev/component/scroll-view.html):以下属性1.0.0版......
  • GoLang协程Goroutiney原理与GMP模型详解
    本文原文地址:GoLang协程Goroutiney原理与GMP模型详解什么是goroutineGoroutine是Go语言中的一种轻量级线程,也成为协程,由Go运行时管理。它是Go语言并发编程的核心概念之一。Goroutine的设计使得在Go中实现并发编程变得非常简单和高效。以下是一些关于Goroutine的关键特性:轻量......
  • CSS中 特性查询(@supports)详解及使用
    1.简介CSS中的@supports用于检测浏览器是否支持CSS的某个属性。其实就是条件判断,如果支持某个属性可以写一套样式,如果不支持某个属性,可以提供另外一套样式作为替补。可以放在代码的顶层,也可以嵌套在任何其他条件组规则中。语法@supports规则由一组样式声明和一条支持条件构......
  • RT DETR v2 TensorRT C++ 部署详解
    RT-DETRv2TensorRTC++部署详解概述随着深度学习技术的发展,目标检测算法在各种应用场景下展现出了卓越的表现。RT-DETRv2(Real-TimeDetectionTransformerv2)作为一款高效的实时目标检测模型,其结合了Transformer架构的优势与传统卷积神经网络(CNNs)的速度,为开发者提供了在......
  • HarmonyOs DevEco Studio小技巧28--部分鸿蒙生命周期详解
    目录前言 页面和自定义组件生命周期页面生命周期onPageShow--- 表示页面已经显示 onPageHide--- 表示页面已经隐藏onBackPress--- 表示用户点击了返回键组件生命周期aboutToAppear---表示组件即将出现onDidBuild--- 表示组件已经构建完成aboutToDisappe......