首页 > 其他分享 >函数栈帧的创建与销毁(简单易懂超详细~)

函数栈帧的创建与销毁(简单易懂超详细~)

时间:2024-09-16 18:49:00浏览次数:3  
标签:销毁 函数 Add 指令 ebp 易懂 main 栈帧

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


前言

在这里插入图片描述

在学习函数栈帧之前,我们有没有在学习的过程之中遇到这些疑问?

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

通过学习函数栈帧,我们可以从底层逻辑上了解这些知识~
学习函数栈帧不可避免地需要使用到汇编语言,可能有点生涩,各位大大要仔细看J桑的图一点一点学习,是可以学懂的~


一、什么是函数栈帧

1.函数栈帧的创建与销毁

函数栈帧是在函数调用时在内存的栈区为该函数分配的临时空间,用于存储函数的局部变量、参数以及返回调用点的地址。当函数开始执行时,栈帧被创建,函数的这些信息被压入栈中;当函数执行结束时,栈帧会被回顾,释放这部分内存。这种利用机制栈的后进先出(LIFO)特性,可以保证函数调用按照顺序进行,并且每个其次调用资源的独立性,防止内存冲突或浪费。

2.寄存器

常见的寄存器总结:

  1. 通用寄存器:
    EAX(累加器寄存器):通用寄存器,主要用于存储器函数的返回值,或者在执行运算技术和逻辑寄存器时保存临时数据。
    EBX(基址寄存器):通用寄存器,可用于作为仓库内存地址,或数据存储的基址寄存器。
    EBP(栈底指针):用于指向当前栈帧的基地址,帮助访问函数调用时的局部变量或参数。
    ESP(栈顶指针):指向当前栈顶位置,随着数据的入栈和出栈,ESP的值会动态变化。
    EIP(指令指针地址):保存当前指令的地址,并且自动更新为下一条要执行的指令地址。
  2. 常用的汇编命令
    mov:将数据从一个寄存器或内存位置传输到另一个寄存器或内存位置。
    push:将数据压入栈,同时ESP注册的值会减小,指向新的栈顶。
    pop:将栈顶的数据弹出到指定的注册或内存位置,ESP注册的值增加。
    sub:执行减法侵犯,将两个操作数相减。
    add:执行加法攻击,将两个操作数相加。
    call:调用函数。首先,将返回地址压入栈中,然后跳转到目标函数执行。
    Jump(jmp):直接跳转到指定的地址,修改EIP注册的值。
    ret:从函数返回,恢复的返回地址,并将其加载到EIP发票中,相似pop eip。

想要理解函数栈帧首先要理解两个寄存器(也就是存储数据的单元,通俗来说就是存放地址的)一个是ebp , 另一个是esp。这两个寄存器是用来维护函数栈帧的。

在这里插入图片描述

3.函数调用创建函数栈帧

每一次调用函数都会创建函数栈帧

下面以这段代码为例为大家讲解

#include <stdio.h>

int Add(int x, int y)
{
	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);
	return 0;
}

在这里插入图片描述
在这里插入图片描述
main()函数也是在被调用的,使用函数堆栈,我们可以看到
在这里插入图片描述
在这里插入图片描述

我们可以看到main函数是被__tmainCRTstartup函数调用了,而__tmainCRTstartup函数是被mainCRTstartup函数调用

在这里插入图片描述


二、main函数栈帧的创建与销毁

1.main函数栈帧的开辟

点击F10调试,右键点击转到反汇编
在这里插入图片描述
为了方便观察我们将显示符号名去掉,可以右键点击显示符号名
在这里插入图片描述

这是创建main函数之前先创建好的函数栈帧__tmainCRTStartup

在这里插入图片描述
首先执行push
在这里插入图片描述
在这里插入图片描述
接下来执行mov和sub,将esp的地址赋给ebp,esp地址减去0E4h
在这里插入图片描述

接下来执行push,将ebx,esi,edi入栈
在这里插入图片描述
接下来执行加载lea,将edi的地址变成ebp-0E4h

执行完push ebx后,esp指向的地址减小了4个字节(从0037FCD0变到00F37CCC,前者比后者大4)。此时esp所指向的地址里面存储的就是ebx的值005c200,这说明ebx成功压栈。剩下的两个push esi和push edi也是这样入栈

在这里插入图片描述
接下来执行把main函数里面的空间全初始化执行完lea后,我们来看下面这三行汇编指令,这三行汇编指令放在一起只为了执行一件事情
在这里插入图片描述

指令1:mov ecx, 39h 操作:将十六进制数39h(对应十进制的57)存入寄存器ECX。
目的:ECX寄存器作为计数器,这里设置为57,表示后续指令将重复执行57次。
指令2:mov eax, CCCCCCCC
操作:将CCCCCCCC(这是一个32位的十六进制数)存入EAX寄存器。 目的:准备好即将被重复存储的价值CCCCCCCC。
命令3:rep
stos dword ptr es:[edi] 操作: rep:重复执行接下来的stos指令,重复的次数由ECX中的值决定(57次)。
stos dword:将EAX中的值CCCCCCCC拷贝到EDI中引用指向的内存地址中。一次操作拷贝4个字节(1个dword)。
结果:这条指令执行了 57 次,每次拷贝 4 个字节(即CCCCCCCC)到EDI所指向的内存地址中,因此总共拷贝了57 × 4 =228一个字节。

整体效果:
在执行完这三条指令后,从EDI所指向的完整内存地址(例如0x0037fcd0)到EBP所指向的228个字节之间的地址,都会被赋值为CCCCCCCC。这意味着内存区域将填充为相同的数据。

2.main函数中变量的创建

变量a的创建,在ebp-8的位置放入10
在这里插入图片描述
变量b,c的创建同理,这里一个小格子就代表4个字节
在这里插入图片描述

3.main函数中Add的调用

在这里插入图片描述
先将b传入Add中,也就是将ebp - 14h位置的值20压入栈
在这里插入图片描述
再将a传入Add中,也就是将ebp-8位置的值10压入栈中
在这里插入图片描述
当然,入栈过后,esp地址也会减少

在这里插入图片描述
下面进行call指令,call指令的作用是将call指令下一条指令的地址入栈,是因为call执行时会进入Add指令的内部,在Add指令走完之后会回到主函数中来走,为了记住主函数中指令走到哪里的位置,因此需要将下一步指令的地址入栈
在这里插入图片描述
在这里插入图片描述

4.进入Add中去

按一下F11,编译器跳到进到Add函数中去
在这里插入图片描述
接下来的准备工作和main函数中的一样,先将main函数的ebp压入栈,esp的地址减小,再将esp赋给Add的ebp
在这里插入图片描述
接下来执行sub,给Add函数开辟空间
在这里插入图片描述

在这里插入图片描述
接下来执行3个push,将ebx,esi,edi入栈,esp继续向上走
在这里插入图片描述
在这里插入图片描述

接下来初始化Add函数的栈帧,同理全部初始化为cccccccc
在这里插入图片描述
在这里插入图片描述

接下来储存变量z,创建了临时变量z,将0放入ebp-8的位置
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

我们在调用的准备工作时,就将我们的形参传过来了,在执行加法时,找的是新压入栈中的a,b。找回这两个数据之后相加赋值在栈中。

接下来要进行z的返回
在这里插入图片描述

将ebp-8,也就是z的值放在寄存器eax中,eax是寄存器,Add被销毁了以后,eax不会被销毁。

接下来弹出栈顶元素,三次pop操作,将栈顶的元素pop出去,分别放入edi,esi,ebx中,不过edi,esi,ebx本来就放的是栈顶的数据
在这里插入图片描述

当然,这里的esp会++

在这里插入图片描述

接下来继续回收Add的栈帧,把ebp,赋给esp,中间的空间就都被回收了
在这里插入图片描述
在这里插入图片描述

接下来pop,继续弹出栈顶元素,此时栈顶存的是main函数ebp的地址,pop过后将地址重新赋给ebp,此时ebp就回到main函数中去了
在这里插入图片描述

在这里插入图片描述

最后是ret指令
在这里插入图片描述

还记得call指令,我们在栈顶放的add的地址吗?在我们上一个操作pop出ebp的main函数之后,我们的esp就指向了call指令所存放的地址。

在这里插入图片描述
在ret执行结束之后弹出栈顶元素,esp继续++。
在这里插入图片描述

至此,Add函数栈帧的创建与销毁全部结束~

5.回到main函数

在这里插入图片描述

回到main函数之后首当其冲的就是这两个数值没有用了,因此要销毁掉

让esp+8,就回收了形参的空间。

在这里插入图片描述

在这里插入图片描述

接下来我们将z的值返回储存到c当中去,eax是寄存器,他存储了我们之前保存的z的值,现在将他放到ebp-20h这个位置,这个位置刚好是变量c的储存位置~

在这里插入图片描述
在这里插入图片描述

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


总结

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

首先为我们的函数分配好栈帧空间以后,初始化好我们的栈帧空间为cccccccc,然后为我们的局部变量分配一点栈帧空间。

2.为什么局部变量不初始化的时候值是随机值?

如果我们不初始化的话,局部变量的值是我们随机放上去的,在给函数初始化栈帧空间的时候初始化的,因此是随机值。如果给局部变量初始化,就将随机值覆盖了。

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

在还没有开始进入函数时,就先将形参从左向右一次push近栈顶,进入函数以后通过指针的偏移量回找到我们的形参。

4.形参和实参是怎样的关系?

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

5.函数调用是怎么做的?函数是怎样返回的?

通过call指令记录下来下一条函数的地址压入栈中,然后跳转到函数中去,先将实参拷贝压入栈中,再将调用这个函数的函数的ebp压入栈中,给函数开辟空间。
函数调用结束后通过寄存器储存返回值,ebp回到调用函数的函数的ebp处,pop掉后esp来到call指令记录下来的地址,最后pop掉返回。

结束啦!谢谢大家~

真相永远只有一个!
在这里插入图片描述

标签:销毁,函数,Add,指令,ebp,易懂,main,栈帧
From: https://blog.csdn.net/Jdxxwu/article/details/142302040

相关文章

  • pdf密码怎么解除?简单易懂的8个pdf解密方法分享,2分钟搞定
    pdf密码怎么解除?在工作生活中,为了保护文件的信息隐私安全,我们会选择给pdf文件添加密码。但如果我们经常编辑这份加密pdf文件的话,每次打开之后都需要重新输入密码,就会变得非常麻烦。因此我们需要对加密pdf进行密码解除操作,要怎么解除pdf密码呢?今天小编就来教大家8个简单好用的PD......
  • IIC时序(通俗易懂版,嘎嘎简单)
    介绍简述:IIC总线就是一个两根线的规则(半双工),规定通信双方如何传送数据,至于传送数据,无非就是主机给从机发送数据,或者从机给主机发送数据,其中加了一点发过去的数据有没有回应,也就是应答!或者不应答。还有一点IIC是一个多机通信的协议。话不多说,上才艺!跟着开心哥的小火车发车了!作......
  • 常见的网络攻防技术(通俗易懂)
    前言提示:文章同样适用于非专业的朋友们,全文通俗化表达,一定能找到你亲身经历过的网络攻击(建议大家认真看完,这篇文章会刷新你对网络攻防的认知)前言在世界人口近80亿的地球上,每天尚且发生数以百万计的抢劫打架斗殴事件,网络更是如此,网络攻防战几乎每时每刻都在发生。如果说......
  • 通俗易懂版经典的黑客入门教程
    第一节、黑客的种类和行为以我的理解,“黑客”大体上应该分为“正”、“邪”两类,正派黑客依靠自己掌握的知识帮助系统管理员找出系统中的漏洞并加以完善,而邪派黑客则是通过各种黑客技能对系统进行攻击、入侵或者做其他一些有害于网络的事情,因为邪派黑客所从事的事情违背了《......
  • 如何通俗易懂的解释TON的智能合约
    文章目录一、小故事一则二、Ton的智能合约在小故事中三、python代码模拟一、小故事一则在一个遥远的国度里,有一个被魔法笼罩的小镇,这个小镇每年都会举办一场盛大的戏剧节。这个戏剧节不仅是演员们展示才华的舞台,更是他们交流心得、共同创作新剧目的盛会。今年的戏......
  • 【C++】简单易懂的vector迭代器
    一、迭代器的本质vector的迭代器本质上就是一个被封装的指针。迭代器的用法和指针的用法十分相似,就是一个像指针的假指针,我们不妨把迭代器看作一个伪指针。二、迭代器的使用句式(可以看到迭代器和指针很像):迭代器有四种:1、正向迭代器:容器名<类型>::iterator迭代器名2、常......
  • 推荐一款:简单、易懂、功能强大的Vue3可拖拽插件
    第一步:安装npm使用以下命令安装npminstallvue-grid-layout--saveyarn使用以下命令安装yarnaddvue-grid-layout第二步:配置全局变量import{createApp}from'vue'importAppfrom'./App.vue'importVueGridLayoutfrom'vue-grid-layout'//引入layout......
  • 进程间通信——消息队列(通俗易懂)
    消息队列概念消息队列是消息的链表,存放在内核中并由消息队列标识符标识,消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺陷。消息队列包括POSIX消息队列和SystemV消息队列。消息队列是UNIX下不同进程之间实现共享资源的一种机制,UNIX......
  • linux进程间通信——信号量(通俗易懂,看这一篇就够了)
    信号量概念特点信号量实际是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。很多进程会访问同一资源,或者向共享内存写入一些东西,为防止争夺资源混乱。可以给一些进程上锁,让其排队等待工作原理P(sv):如果sv的值大于零,就给它减1;如果它的值为......
  • MVCC详解,深入浅出简单易懂
    转载自https://blog.csdn.net/lans_g/article/details/124232192一、什么是MVCC?mvcc,也就是多版本并发控制,是为了在读取数据时不加锁来提高读取效率和并发性的一种手段。数据库并发有以下几种场景:读-读:不存在任何问题。读-写:有线程安全问题,可能出现脏读、幻读、不可重复读......