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

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


前言

在这里插入图片描述

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

  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掉返回。

结束啦!谢谢大家~

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值