【C语言】函数栈帧的创建和销毁


1.为什么要了解函数栈帧的创建和销毁

  • 函数是如何创建的
  • 局部变量是如何创建的
  • 为什么局部变量不初始化的值是随机的
  • 函数参数是如何传递的
  • 参数有多个时,传参的顺序是怎样的
  • 函数返回值是如何带回的
  • 为什么函数形参是实参的一份临时拷贝

等等这些问题,在我们学完函数栈帧的创建和销毁后就可以迎刃而解了。

2.什么是函数栈帧

函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间。就是函数在栈中所占空间。

3. 函数栈帧的作用

函数栈帧可以用来存放:

  • 函数参数和函数返回值
  • 临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
  • 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)

4.函数栈帧的创建和销毁过程解析

本文例子

#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;
}

4.1什么是栈?

定义栈是一个动态内存区域。它是一个特殊容器,可以容纳局部变量,函数和函数参数。它是基础,我们对函数进行的操作都是在栈上完成。
栈的特点

  1. 先入栈的数据后出栈,后入栈的数据先出栈。就像我们把薯片放入圆柱桶,但我们吃的时候总是先吃到后放入的,最后吃到先放入的。在这里插入图片描述
  2. 数据入栈和出栈是通过push(压栈/入栈)和pop(出栈)来完成,程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。
  3. 在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。在这里插入图片描述

4.2寄存器和汇编指令的类型和作用

相关寄存器

  • eax:通用寄存器,保留临时数据,常用于返回值
  • ebx:通用寄存器,保留临时数据
  • ebp:栈底寄存器
  • esp:栈顶寄存器
  • eip:指令寄存器,保存当前指令的下一条指令的地址

函数栈帧就是当程序调用函数时,为这个函数开辟的空间。其中ebp和esp是维护函数栈帧的寄存器,ebp记录的是栈底的地址,esp记录的是栈顶的地址。在这里插入图片描述
相关汇编指令

  • mov:数据转移指令
01111821  mov         ebp,esp //将esp中的值移(赋)给ebp
  • push:数据入栈,同时esp栈顶寄存器也要发生改变
01111829  push        ebx  
0111182A  push        esi  
0111182B  push        edi

在这里插入图片描述

  • pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
    当我们调用其它函数时,我们把ebp这个记录上一个函数栈底的寄存器压入栈中,当函数调用结束,我们要回到原来函数,需要ebp回到原来函数的栈底,这时pop起作用,它将记录原来函数栈底的ebp弹出,使其回到原来的位置。

在这里插入图片描述

黑色是pop前的操作,红色是pop后的操作。pop后,ebp原来那块空间不属于函数,esp向下移动四个字节,一个内存单位等于四个字节。

  • sub:减法命令
  • add:加法命令
00E618B3  sub         esp,0E4h//表示esp减少0E4h个字节,
					  也就是向上移动57个内存单位。其中0E4h是
					  16进制数字,一共228个字节。
……
00E618F7  add         esp,8//表示esp增加8个字节,
                      向下移动两个内存单位。
  • call:函数调用,1. 压入返回地址 2. 转入目标函数
  • jump:通过修改eip,转入目标函数,进行调用
  • ret:恢复返回地址,压入eip,类似pop eip命令
    这几个后边讲

4.3函数的调用堆栈

我们在分析函数栈帧的创建之前,需要知道函数的调用过程。在这里插入图片描述

我们可以看到main函数是invoke_main调用的,而invoke_main又是由栈区下面的函数调用。已知函数由esp和ebp维护,既然如此,那么调用main的函数也有自己的esp和ebp,否则它的函数栈帧就会被销毁。同理在我们的例子中Add函数也有自己的esp和ebp维护自己的栈帧。

4.4环境准备

我使用的VS2019,在转到反汇编后希望没有编译器附加的代码干扰,我们可以这么做在这里插入图片描述

4.5转到反汇编

我们通过汇编语言来了解函数栈帧,从而解决开头提到的问题。在这里插入图片描述
VS每次调试都会为代码重新分配内存空间,所以我们每次的调试都略有差异。
接下来,我们就看到这段代码的汇编语言。

4.6函数栈帧的创建

  1. 我们发现在变量创建之前有一段汇编代码,它的意义是创建main函数的函数栈帧。我们主要研究main和Add函数的函数栈帧。
int main()
{
00821820  push        ebp  //把ebp压入栈中,它记录的是调用main的函数的栈底的地址,esp-4
00821821  mov         ebp,esp //将esp的记录的地址赋给新的ebp,此时的ebp记录的是main函数的栈底的地址 
00821823  sub         esp,0E4h//esp往上(低地址)移动0E4h(228)个字节  ,此时esp形成main函数的栈顶,在esp和ebp之间创建的这块空间就是函数栈帧
00821829  push        ebx  //压入ebx,esp-4
0082182A  push        esi  //压入esi,esp-4
0082182B  push        edi  //压入edi,esp-4
//上面3条指令保存了3个寄存器的值在栈区,这3个寄存器的在函数随后执行中可能会被修改,所以先保存寄存器原来的值,以便在退出函数时恢复
0082182C  lea         edi,[ebp-24h]  //将ebp-24h的地址放在edi中
0082182F  mov         ecx,9  //将9赋给ecx,9就是9块内存单位
00821834  mov         eax,0CCCCCCCCh  //将0xCCCCCCCCh赋给eax  
00821839  rep stos    dword ptr es:[edi]  //将edi记录地址往下的每块(9块)空间都初始化为0CCCCCCCC,刚好到函数的栈底。

在这里插入图片描述
注意每次压入栈区中,esp都会将它纳入函数的栈帧,就像别人来投靠你,你收纳他了。
问题1为什么局部变量不初始化的值是随机的?
当我们创建一个局部变量时,我们完全有可能申请到这些放有0xCCCCCCCC的空间,所以当你未初始化而使用它时,比如打印,就会出现有趣的一幕。这个数组刚好创建在这部分内存。在这里插入图片描述
所以一定要初始化,即使这个值你暂时用不到。
问题2这段代码同时也解释了函数是如何创建的。
2.接下来就是局部变量的创建。

	int a = 3;
0082183B  mov         dword ptr [ebp-8],3  //将3赋给ebp-8地址的变量a
	int b = 5;
00821842  mov         dword ptr [ebp-14],5  //将5赋给ebp-12的地址的变量b
	int ret = 0;
00821849  mov         dword ptr [ebp-20h],0  //将0赋给ebp-20的地址的变量ret

在这里插入图片描述
这里的8、14、20都是十六进制数。
问题3局部变量是如何创建的
局部变量在函数栈帧内创建的。创建过程如上。
3. 函数调用以及传参

	ret = Add(a, b);
//传参
00821850  mov         eax,dword ptr [ebp-14h]  //将b的值赋给eax,我们把这个值叫做a1
00821853  push        eax  //将eax压入栈中,esp-4
00821854  mov         ecx,dword ptr [ebp-8]  //将a的值赋给ecx,我们把这个值叫做b1
00821857  push        ecx  //将ecx压入栈中,esp-4
//调用函数
00821858  call        _Add (08213B6h)  //将下一条指令的地址压入栈中,同时进行被调函数
0082185D  add         esp,8  
00821860  mov         dword ptr [ebp-20h],eax

在这里插入图片描述

问题4参数有多个时,传参的顺序是怎样的?
当参数有多个时,从右往左传参。如本例中,先b,再传a。
4. 进入函数

int Add(int x, int y)
{
//为Add函数创建函数栈帧,过程和main函数类似
00821890  push        ebp  //此时压入栈中的是记录main栈底的ebp,esp-4
00821891  mov         ebp,esp  //将记录main函数栈顶的esp赋给ebp,维护Add函数的栈底
00821893  sub         esp,0CCh   //将esp往上移动0CCh个字节,esp-0cch
00821899  push        ebx  //将ebx压栈
0082189A  push        esi  //将esi压栈
0082189B  push        edi  //将edi压栈
	int z = 0;
0082189C  mov         dword ptr [ebp-8],0  //在ebp-8的位置创建一个变量z
	z = x + y;
008218A3  mov         eax,dword ptr [ebp+8]  //ebp+8是a1的地址,将a1放在eax寄存器
008218A6  add         eax,dword ptr [ebp+0ch]  //ebp+0ch是b1的地址,将b1放在寄存器中
008218A9  mov         dword ptr [ebp-8],eax  //将寄存器中的值放到z中,z就是a和b的值
	return z;
008218AC  mov         eax,dword ptr [ebp-8]  //将z的值放在eax,让eax带回返回值
}

在这里插入图片描述

问题5函数参数是如何传递的?
函数参数并不是在被调函数的栈帧中创建的,而是在main函数的栈帧中创建的,详细请看上图。
问题6为什么函数形参是实参的一份临时拷贝?
我们发现被调函数中并没有创建x和y,而是直接将a1和b1相加赋给z。而a1和b1是我们创建的变量,它们是a和b的拷贝,对它们进行修改,不影响a和b的值。

4.7 函数栈帧的销毁

当函数结束时,函数栈帧就要销毁。

008218AF  pop         edi//在栈顶弹出一个值,把值放在edi中,esp+4  
008218B0  pop         esi//在栈顶弹出一个值,把值放在esi中,esp+4  
008218B1  pop         ebx//在栈顶弹出一个值,把值放在ebx中,esp+4  
008218B2  mov         esp,ebp//将ebp的值赋给esp,相当于回收了Add函数的栈帧空间
008218B4  pop         ebp//将记录main函数栈底的ebp从栈中弹出,使ebp指向main函数的栈底,esp+4
008218B5  ret//弹出call留下的地址,返回call的下一条指令,继续执行,esp+4

在这里插入图片描述
问题7 函数返回值是如何带回的?
函数的返回值是放在eax寄存器中带回的,详细请看上面代码。

 0082185D  add         esp,8  //将8赋给esp,直接跳过a1和b1,esp+8
00821860  mov         dword ptr [ebp-20h],eax  //eax寄存器中的值放在ret中
	printf("%d\n", ret);
00821863  mov         eax,dword ptr [ebp-20h]  //将ret的值赋给eax  
00821866  push        eax  //将eax压栈,esp-4
00821867  push        offset string "%d\n" (0827BD8h)  
0082186C  call        _printf (08210D2h)  
00821871  add         esp,8  
接下来三个代码就是把ret打印出来,这里就不讲了,有兴趣的大家可以去了解下
	return 0;
00821874  xor         eax,eax  
}

5.总结

到这里,我们了解函数栈帧的创建,解决了局部变量的创建、传值调用、传参顺序、返回值等问题,也了解了部分汇编指令和部分寄存器的作用。
写这章时写的浑浑噩噩,写到这,忘了那,所以大家如果发现有什么问题,请一定要提出,我一定会尽快改正。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值