函数栈帧的创建与销毁

本文详细解析了函数栈帧的概念,介绍了栈的工作原理、寄存器在函数调用中的作用以及汇编指令在栈帧创建和销毁中的应用。通过实例展示了函数调用过程中的栈帧操作,帮助读者掌握函数调用堆栈管理和内存管理的关键概念。

本文是在vs2022,x86环境下进行的

一.何为函数栈帧

函数栈帧(Function Stack Frame),也称为活动记录(Activation Record)或堆栈帧(Stack Frame),是在函数调用过程中在程序的执行堆栈上分配的一块内存区域。它用于存储函数的局部变量、参数、返回地址和其他与函数执行相关的信息。

每当一个函数被调用时,一个新的函数栈帧就会被创建,并被添加到当前的执行堆栈的顶部。这个新的函数栈帧包含了函数的局部变量以及其他必要的上下文信息。函数栈帧的创建和销毁是通过栈指针的移动来完成的。
函数栈帧通常包括以下内容:

  • 局部变量:函数内部定义的变量,在函数栈帧中分配内存空间来存储它们的值。
  • 参数:函数调用时传递给函数的参数值被保存在函数栈帧中,供函数使用。
  • 返回地址:调用函数之前的代码执行到函数调用语句时,将当前代码的返回地址压入堆栈,函数执行完后,根据该返回地址返回到正确的位置。
  • 帧指针(Frame Pointer):也称为基址指针(Base Pointer),用于快速访问函数栈帧中的局部变量和其他上下文信息。
  • 函数调用时需要保留的寄存器值:例如,有些寄存器可能需要在函数调用期间被暂时保存,并在函数返回时恢复到原始状态。

我们要注意,函数栈帧的创建和销毁是由编译器或者解释器负责的,它们根据函数的参数、局部变量以及其他上下文信息生成相应的汇编指令来分配和释放堆栈空间。函数栈帧的创建和销毁过程确保了函数调用的正确性和数据的隔离,使得程序能够有效地管理函数的执行和嵌套调用。

所以搞明白函数栈帧的创建与销毁可以让我们解决平常不太明白的问题,而且还可以提高编程的准确性和效率,并避免因不正确的函数调用而引发的错误和异常情况。

二.基本知识

在了解函数栈帧的创建与销毁之前,我们需要先了解一些知识

1.栈

栈(Stack)是一种常见的数据结构,用于存储和管理数据。它是一个特殊的线性数据结构,遵循“后进先出”(Last-In-First-Out,LIFO)的原则,意味着最后加入的元素首先被取出。

栈通常由一个连续的内存区域组成,可以想象成一摞叠在一起的盘子。新的元素(如数据项或指针)通过压栈(Push)操作添加到栈顶,而从栈中取出元素则通过弹栈(Pop)操作从栈顶移除。因此,只能访问或操作栈顶元素,而不能直接访问或修改其他位置上的元素。

在x86架构及类似的架构中,栈通常位于高地址区域。
在这里插入图片描述

2.寄存器

寄存器(Register)是计算机中用于临时存储和处理数据的一组内部存储器。它们是位于中央处理器(CPU)内部的一种高速存储器,与主内存相比,寄存器具有更快的读写速度和更低的访问延迟。
在x86架构中,常见的寄存器在函数调用中扮演重要角色,包括以下几个:

  1. ebp(BasePointer):ebp寄存器是基址指针寄存器,用于指向当前函数的栈帧的基地址。在函数调用时,ebp通常会保存上一个函数的ebp值,并将其设置为当前栈帧的基地址。通过ebp,可以方便地访问局部变量和函数参数。
  2. esp(Stack Pointer):esp寄存器是栈指针寄存器,用于指示当前栈顶位置。栈是用来存储函数调用过程中的局部变量、返回地址和其他临时数据的重要数据结构。esp随着栈帧的创建和销毁而进行调整。
  3. ebx(Base Register X):ebx寄存器是通用寄存器之一,用于存储临时数据或者作为内存地址的基址寄存器。
  4. esi(Source Index):esi寄存器是源索引寄存器,通常用于存储源数据的基址或者作为循环计数器的一部分。
  5. edi(Destination Index):edi寄存器是目的地索引寄存器,通常用于存储目标数据的基址或者作为循环计数器的一部分。
  6. ecx(Counter Register X):ecx寄存器是计数器寄存器,用于存储循环的计数值或者作为一般性目的寄存器。
  7. eax()Extended Accumulator:寄存器 eax 是 x86 架构中的通用寄存器之一,用于存储函数返回值、操作数和临时数据等。

3.汇编指令

  1. mov(Move):mov指令用于将数据从一个位置复制到另一个位置。例如,mov ax, bx会将BX寄存器的值复制到AX寄存器中。
  2. push(Push onto Stack):push指令用于将数据推入栈中。它将给定的值或寄存器中的值压入栈顶,并相应地调整栈指针。例如,push ax会将ax寄存器中的值压入栈中。
  3. call(Call Procedure):call指令用于调用子程序或函数。它将当前的程序执行地址压入栈中,并跳转到指定的子程序开始执行。在子程序执行完成后,使用 ret 指令返回到 call 指令之后的下一条指令。
  4. ret(Return):用于从子程序中返回到调用点,恢复程序控制流并进行栈帧的清理
  5. add(Addition):add指令用于执行加法操作。它可以将给定的值添加到目标操作数中,然后将结果存储在目标操作数中。例如,add ax,10会将AX寄存器的值增加10。
  6. sub(Subtraction):sub指令用于执行减法操作。它可以将给定的值从目标操作数中减去,然后将结果存储在目标操作数中。例如,sub ax, 5会将AX寄存器的值减去5。
  7. rep stos(Repeat Store String):rep stos指令用于将一个字节或字的值重复存储到一段内存区域中
  8. lea(load effecitve address):在x86汇编中用于计算有效地址,并将结果存储在目标寄存器中。

三.函数栈帧的创建与销毁详解

展示所用代码,我们这里简单的定义了一个加法函数,并调用其进行运算

1.代码展示

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

}

2.函数调用堆栈

我们先按F11开启调试,然后打开调用堆栈

在这里插入图片描述
打开调用堆栈之后记得勾选显示外部代码
在这里插入图片描述

我们可以清楚的看到main()函数其被 invoke_main()函数调用的在这里插入图片描述

然后 invoke_main()函数被__scrt_common_main_seh()函数调用,而__scrt_common_main_seh()函数又被__scrt_common_main()函数调用,最后__scrt_common_main()被mainCRTStartup()调用,每个函数栈帧都有自己的 ebp 和 esp 来维护栈帧空间

本文为了更加清晰的展示,就只展示到invoke_main()函数,之前的调用暂且忽略

3.反汇编

在使用之前我们要先做一些事前工作,右击解决发难点击属性,然后打开C/C++一栏的常规,将支持仅我的代码调试改为否,这样可以让我们观察汇编指令的时候更加清晰。
在这里插入图片描述
在这里插入图片描述

F11开启调试,右击背景板点击转到反汇编
在这里插入图片描述
右击之后勾选我所选择的几个选项,此举是为了更方便观察

在这里插入图片描述

4.函数栈帧的创建

int main()
{
00F01840  push        ebp  
00F01841  mov         ebp,esp  
00F01843  sub         esp,0E4h  
00F01849  push        ebx  
00F0184A  push        esi  
00F0184B  push        edi  
00F0184C  lea         edi,[ebp-24h]  
00F0184F  mov         ecx,9  
00F01854  mov         eax,0CCCCCCCCh  
00F01859  rep stos    dword ptr es:[edi]  
	int a = 10;
00F0185B  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
00F01862  mov         dword ptr [ebp-14h],14h  
	int c = 0;
00F01869  mov         dword ptr [ebp-20h],0  

	c = Add(a, b);
00F01870  mov         eax,dword ptr [ebp-14h]  
00F01873  push        eax  
00F01874  mov         ecx,dword ptr [ebp-8]  
00F01877  push        ecx  
00F01878  call        00F010B9  
00F0187D  add         esp,8  
00F01880  mov         dword ptr [ebp-20h],eax  

	printf("%d\n", c);
00F01883  mov         eax,dword ptr [ebp-20h]  
00F01886  push        eax  
00F01887  push        0F07B30h  
00F0188C  call        00F010D7  
00F01891  add         esp,8  

}

调用main之前,invoke_main()函数的栈帧就已经创建好了。
在这里插入图片描述

执行以下代码

int main()
{
00F01840  push        ebp   ; 将当前函数调用的基址指针(ebp)保存到堆栈上
00F01841  mov         ebp,esp   ; 将当前堆栈指针(esp)的值赋给 ebp,以建立新的基址指针
00F01843  sub         esp,0E4h   ; 通过将0E4h(228个字节)的大小从堆栈指针减去来为局部变量和临时数据分配空间
00F01849  push        ebx   ; 将 ebx 寄存器的值保存到堆栈中,以备将来使用
00F0184A  push        esi   ; 将 esi 寄存器的值保存到堆栈中,以备将来使用
00F0184B  push        edi   ; 将 edi 寄存器的值保存到堆栈中,以备将来使用
00F0184C  lea         edi,[ebp-24h]   ; 使用 `lea` 指令计算出一个相对于 ebp 的地址,该地址是一个缓冲区([ebp-24h])用于存储数据
00F0184F  mov         ecx,9   ; 设置 ecx 的值为 9,即要重复操作的次数
00F01854  mov         eax,0CCCCCCCCh   ; 将 eax 的值设置为 0CCCCCCCCh,这是内存填充模式(常用于调试目的)
00F01859  rep stos    dword ptr es:[edi]   ; 使用 `rep stos` 指令将 eax 中的值重复存储到 [edi] 所指向的位置,即将缓冲区填充满

在这里插入图片描述

变量的创建

    int a = 10;
00F0185B  mov         dword ptr [ebp-8],0Ah   ; 将值 10 存储到位于 ebp-8 的地址处,即变量 a 的内存空间
    int b = 20;
00F01862  mov         dword ptr [ebp-14h],14h   ; 将值 20 存储到位于 ebp-14 的地址处,即变量 b 的内存空间
    int c = 0;
00F01869  mov         dword ptr [ebp-20h],0   ; 将值 0 存储到位于 ebp-20 的地址处,即变量 c 的内存空间

在这里插入图片描述

Add函数传参

    c = Add(a, b);
00F01870  mov         eax,dword ptr [ebp-14h]   ; 将变量 b 的值加载到寄存器 eax 中
00F01873  push        eax   ; 将 eax 寄存器中的值压入堆栈,作为第二个参数传递给函数 Add
00F01874  mov         ecx,dword ptr [ebp-8]   ; 将变量 a 的值加载到寄存器 ecx 中
00F01877  push        ecx   ; 将 ecx 寄存器中的值压入堆栈,作为第一个参数传递给函数 Add

在这里插入图片描述

Add函数的调用

00F01878  call        00F010B9   ; 调用函数 Add
00F0187D  add         esp,8   ; 调整堆栈指针,清除参数空间
00F01880  mov         dword ptr [ebp-20h],eax   ; 将函数返回值存储到变量 c 的内存空间

执行call语句后会调用Add函数,但他会在之前先将下一条指令进行压栈。
Add函数的反汇编代码如下

int Add(int x, int y)
{
00F01780  push        ebp   ; 将当前函数调用的基址指针(ebp)保存到堆栈上
00F01781  mov         ebp,esp   ; 将当前堆栈指针(esp)的值赋给 ebp,以建立新的基址指针
00F01783  sub         esp,0CCh   ; 通过将0CCh(204个字节)的大小从堆栈指针减去来为局部变量和临时数据分配空间
00F01789  push        ebx   ; 将 ebx 寄存器的值保存到堆栈中,以备将来使用
00F0178A  push        esi   ; 将 esi 寄存器的值保存到堆栈中,以备将来使用
00F0178B  push        edi   ; 将 edi 寄存器的值保存到堆栈中,以备将来使用
    int z = 0;
00F0178C  mov         dword ptr [ebp-8],0   ; 将值 0 存储到位于 ebp-8 的地址处,即变量 z 的内存空间
    z = x + y;
00F01793  mov         eax,dword ptr [ebp+8]   ; 将第一个参数 x 的值加载到寄存器 eax 中
00F01796  add         eax,dword ptr [ebp+0Ch]   ; 将第二个参数 y 的值加到寄存器 eax 中
00F01799  mov         dword ptr [ebp-8],eax   ; 将寄存器 eax 中的结果存储到变量 z 的内存空间
    return z;
00F0179C  mov         eax,dword ptr [ebp-8]   ; 将变量 z 的值加载到寄存器 eax 中
}

我们在这里可以看到通过ebp+8和ebp+12的地址访问到了由a和b传到寄存器ecx和eax的值,他们就是我们Add函数中的两个形参x和y
在这里插入图片描述

5.函数的销毁

Add函数的销毁

00F0179F  pop         edi   ; 从堆栈中弹出值并恢复到 edi 寄存器
00F017A0  pop         esi   ; 从堆栈中弹出值并恢复到 esi 寄存器
00F017A1  pop         ebx   ; 从堆栈中弹出值并恢复到 ebx 寄存器
00F017A2  mov         esp,ebp   ; 将基址指针 ebp 的值赋给栈指针 esp,恢复原来的堆栈位置
00F017A4  pop         ebp   ; 从堆栈中弹出值并恢复到 ebp 寄存器
00F017A5  ret   ; 从当前函数返回,将控制权交还给调用方

在这里插入图片描述

main函数的销毁

call指令回到这里

00F0187D  add         esp,8   ; 调整堆栈指针,清除参数空间,将形参消除
00F01880  mov         dword ptr [ebp-20h],eax   ; 将函数返回值存储到变量 c 的内存空间

下面和上面所讲同理,这里就不再赘述了

printf("%d\n", c);
00F01883  mov         eax,dword ptr [ebp-20h]   ; 将变量 c 的值加载到寄存器 eax 中
00F01886  push        eax   ; 将寄存器 eax 的值压入堆栈中(作为参数传递给 printf 函数)
00F01887  push        0F07B30h   ; 将字符串格式化控制符 "%d\n" 的地址压入堆栈中(作为参数传递给 printf 函数)
00F0188C  call        00F010D7   ; 调用 printf 函数
00F01891  add         esp,8   ; 调整堆栈指针,清除参数空间

}
00F01894  xor         eax,eax   ; 将寄存器 eax 的值与自身进行异或操作,相当于将其置零
00F01896  pop         edi   ; 从堆栈中弹出值并恢复到 edi 寄存器
00F01897  pop         esi   ; 从堆栈中弹出值并恢复到 esi 寄存器
00F01898  pop         ebx   ; 从堆栈中弹出值并恢复到 ebx 寄存器
00F01899  add         esp,0E4h   ; 调整堆栈指针,回收局部变量和临时数据所占用的空间
00F0189F  cmp         ebp,esp   ; 比较基址指针 ebp 和堆栈指针 esp 的值
00F018A1  call        00F01253   ; 调用某个函数(未给出具体代码)
00F018A6  mov         esp,ebp   ; 将基址指针 ebp 的值赋给栈指针 esp,恢复原来的堆栈位置
00F018A8  pop         ebp   ; 从堆栈中弹出值并恢复到 ebp 寄存器
00F018A9  ret   ; 从当前函数返回,将控制权交还给调用方

四.尾声

以上就是本文章的全部内容,相信大家理解后可以解决许多平常疑惑的问题,如果文章有什么问题,请大家及时提出,我会进一步修改和完善的。

评论 6
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值