【深入解释函数栈帧的创建和销毁】||详细解析+图文分析

本文详细介绍了函数调用时栈帧的创建和销毁过程,包括保存上一个函数的栈帧、初始化本函数的栈帧、为局部变量开辟空间、函数传参及调用和返回。通过实例分析了`Add`和`main`函数的栈帧操作,揭示了参数传递、局部变量初始化和栈空间回收的原理。
摘要由CSDN通过智能技术生成

目录

前言

函数的调用栈帧过程的常用指令

实例讲解函数栈帧的创建和销毁

1.保存上一个函数(__tmainCRTSARTUP)的栈帧

2.初始化本函数(main)的栈帧,通过esp来初始化ebp

3.对本函数预留的栈空间(包括本函数的临时变量空间)进行初始化为随机值

4.为局部变量开辟空间

5.函数传参(从右向左)

6.调用函数(call Add)

7.保存上一个函数(main)的栈帧

8.初始化本函数(Add)的栈帧

9.对本函数预留的栈空间(包括本函数的临时变量空间)进行初始化

10.为局部变量开辟空间

11.函数栈帧的销毁

总结


前言

每一个函数调用都要在栈区调用一个空间。在函数调用时,用来保存函数内部的临时数据如参数、返回地址等的内存单元就是栈帧。

栈是由高地址低地址生长的,且栈有栈顶和栈底之分,为了更方便的使用函数内部的临时数据,引入了ebp和esp,寄存器ebp保存的是栈底地址(基地址),esp保存的是栈顶地址

在函数调用后进入函数内部代码后通过esp来初始化ebp,这样以ebp为基地址的数据,向上为函数的返回值,参数等,向下为调用函数的局部变量等。(总结会讲到)使用ebp方便地将本函数中的栈分成了两部分,我们把ebp和esp之间的数据就称为函数栈帧。这两个地址是用来维护栈帧的,且一次只能维护一个函数。

由于栈帧是与函数的调用位置有关系,并且是用完即还,并且同一个函数在不同的位置调用所形成的栈帧并不相同,故当函数调用完毕,需要回收它使用的栈空间,否则引起栈平衡错误,引发程序故障。

栈在使用上一般会通过push或pop指令来进行入栈和出栈,另外一些特殊的指令call和ret,也会进行入栈和出栈操作。


函数的调用栈帧过程的常用指令

1.push:把一个32位的操作数压入堆栈中,这个操作在32位机中会使得esp被减4(字节)

push ecx 等价于 esp=esp-4

2.pop:与push相反,esp每次加4(字节),一个数据出栈。pop的参数一般是一个寄存器,栈顶的数据被弹出到这个寄存器中;

pop eax 等价于 esp=esp+4

  3. call:调用函数,先压栈call指令的下一条指令,然后跳转到Add函数的地方

  4.ret:跳转到调用函数的地方。对应于call,返回到对应的call调用的下一条指令,若有返回       值,则放入eax中

  5.xor:异或指令,这本身是一个逻辑运算指令,但在汇编指令中通常会见到它被用来实现清零         功能。用 xor eax,eax这种操作来实 现 mov eax,0,可以使速度更快,占用字节数更少。        6.lea:取得第二个参数地址后放入到前面的寄存器(第一个参数)中。

   然而lea也同样可以实现mov的操作,例如: lea edi,[ebx-0ch]

  7.add:加法指令,第一个是目标操作数,第二个是源操作数,格式为:目标操作数 = 目标操作数 + 源操作数;

  8.sub:减法指令,第一个是目标操作数,第二个是源操作数,格式为:目标操作数 = 目标操作数- 源操作数;

  9.mov:数据传送。第一个参数是目的操作数,第二个参数是源操作数,就是把源操作数拷贝到目的操作数一份。


实例讲解函数栈帧的创建和销毁

首先,我们以Add函数为例进行讲解。

int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}
int main()
{
    int a = 10;
    int b = 10;
    int ret=Add(a,b);
    return 0;
}

   109: int Add(int x, int y)
   110: {
003D3CD0  push        ebp  //保留上一个函数(main)的栈帧
003D3CD1  mov         ebp,esp // 通过esp,更新ebp
  //对本函数预留的栈空间进行初始化
003D3CD3  sub         esp,0CCh
003D3CD9  push        ebx  
003D3CDA  push        esi  
003D3CDB  push        edi  
003D3CDC  lea         edi,[ebp-0CCh]  
003D3CE2  mov         ecx,33h  
003D3CE7  mov         eax,0CCCCCCCCh  
003D3CEC  rep stos    dword ptr es:[edi] 
 
   111:     int z = 0;
003D3CEE  mov         dword ptr [z],0  //创建临时变量
   112:     z = x + y;
003D3CF5  mov         eax,dword ptr [x] // 将x保存在eax中
003D3CF8  add         eax,dword ptr [y] //将x+y相加
003D3CFB  mov         dword ptr [z],eax  //将相加的值放入Z中 
   113:     return z;
003D3CFE  mov         eax,dword ptr [z]  //把z存储在eax中
   
   114: }
003D3D01  pop         edi  //edi出栈,esp+4;
003D3D02  pop         esi //esi出栈,esp+4; 
003D3D03  pop         ebx  //ebx出栈,esp+4;
003D3D04  mov         esp,ebp  //回收Add的战争
003D3D06  pop         ebp  //ebp出栈,将出栈的内容保存到ebp
003D3D07  ret               //ret指令会使得出栈一次,跳转到之前函数保留的返回地址
​
   
   
   
   115: int main()
   116: {
003D1AE0  push        ebp  //保存上一个函数的栈帧,将上一个函数栈底的地址压栈
003D1AE1  mov         ebp,esp //通过esp,更新ebp
003D1AE3  sub         esp,0E4h  //开辟0E4h局部变量的空间
003D1AE9  push        ebx       //保存ebx寄存器的值
003D1AEA  push        esi       //保存esi寄存器的值
003D1AEB  push        edi       //保存edi寄存器的值
003D1AEC  lea         edi,[ebp-0E4h] 
 //将ebp-0E4h的值,赋给edi,是所开辟局部变量空间的最低地址
003D1AF2  mov         ecx,39h          //将39h放ecx寄存器
003D1AF7  mov         eax,0CCCCCCCCh  //将0xcccccccc的值放进eax中
003D1AFC  rep stos    dword ptr es:[edi]  //初始化本函数(main)的栈帧
   117:     int a = 10;
003D1AFE  mov         dword ptr [ebp-8],0Ah  //为局部变量a开辟空间
   118:     int b = 10;
003D1B05  mov         dword ptr [ebp-14h],0Ah // 为局部变量b开辟空间
   119:     int ret=Add(a,b);
003D1B0C  mov         eax,dword ptr [b]  //参数b放入寄存器eax中
003D1B0F  push        eax               //对eax进行压栈
003D1B10  mov         ecx,dword ptr [a]  //参数a放入寄存器eax中
003D1B13  push        ecx               //对ecx进行压栈
003D1B14  call        _Add (03D11E0h)  //调用函数Add,并且对下一条指令进行压栈
003D1B19  add         esp,8         //回收形参
003D1B1C  mov         dword ptr [ebp-20h],eax  //将之前Z的值保存到[ebp-20h]
   120:     return 0;
003D1B1F  xor         eax,eax  //相当于 mov eax,0,实现清零功能
//采取相同方法对main函数回收
   121: }
003D1B21  pop         edi  
003D1B22  pop         esi  
003D1B23  pop         ebx  
003D1B24  add         esp,0E4h  
   121: }
003D1B2A  cmp         ebp,esp  
003D1B2C  call        __RTC_CheckEsp (03D1136h)  
003D1B31  mov         esp,ebp  
003D1B33  pop         ebp  
003D1B34  ret 


1.保存上一个函数(__tmainCRTSARTUP)的栈帧

其实main()函数是在_tmainCRTStartup函数中调用的,第一步压栈保存了调用main函数的__tmainCRTSARTUP的栈帧。

2.初始化本函数(main)的栈帧,通过esp来初始化ebp

将当前esp指向的地址(即存储old ebp值的地址)赋给ebp,这样就能通过esp来初始化ebp,使得ebp的值得以更新,

3.对本函数预留的栈空间(包括本函数的临时变量空间)进行初始化为随机值

 

1)

 

 

2)

 

3)lea 加载有效地址,把[ebp-0E4h] 这个地址加载到edi中,相当于给edi 放了个地址,这四步相当于 把从edi加载的地址开始向下的39h个dword(四个字节)初始化为0cccccccch。

换句话说就是用eax中的值初始化地址为edi的内存,dword为双字型,一个字为两个字节,双字为4个字节,每执行一次,edi+4,ecx-1,直到ecx=0为止.

 

 

4.为局部变量开辟空间

 

 

5.函数传参(从右向左)

 

 

6.调用函数(call Add)

 

call操作,相当于push call的下一条指令,然后esp-4

call指令先将函数返回地址压入栈中,再跳转到相应地址执行,此处的返回地址应为call指令的下一条指令地址.

 


 

 

7—10步骤和1—4相同,都是调用函数时的操作,不再赘述

7.保存上一个函数(main)的栈帧

8.初始化本函数(Add)的栈帧

9.对本函数预留的栈空间(包括本函数的临时变量空间)进行初始化

10.为局部变量开辟空间

 

 

 

11.函数栈帧的销毁

回收Add函数内存空间。

 

 

 

 

 

 

 

采用相同的方法回收mian和__tmainCRTSARTUP


 

总结:

通过以上的学习,我们对函数栈帧的创建和销毁的理解就更加的透彻,也可以解决之前很多的疑惑,我们通过回答以下几个问题来巩固一下知识。

  • 局部变量是怎么创建的?

我们对函数预留的栈空间进行初始化后会为局部变量开辟空间,由此创建了局部变量。

  • 为什么我们如果不初始化局部变量,局部变量的值为随机值呢?

    我们在创建函数栈帧时,对edi到ebp之间的空间进行了初始化,值为随机值。如果你对局部变量初始化了,那就把随机值覆盖了。

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

    我们在调用函数之前,就已经把实参放进寄存器中,并且从右向左开始压栈,当我们真正调入函数时,我们可以通过ebp的偏移量来找到形参。

    如图所示:ebp+4=函数的返回地址;ebp+8=函数的第一个参数;ebp+12=函数的第二个参数。我在前言也说过,以ebp为基地址的数据,向上为函数的返回地址,参数等。向下为调用函数的局部变量等。所以,可通过基地址方便的寻址函数的局部变量,函数的返回值,函数参数等。

     

  • 形参和实参的关系是什么?

    形参是实参的一份临时拷贝,它们值是相等的,但是空间是完全独立的。

  • 函数调用是怎么做的?

    函数调用通过call指令,先压栈call指令的下一条指令,然后跳转到你所调用的函数的地方。

  • 函数调用结束后是如何返回的?

    我们在调用函数之前就为自己找好了后路,我们对call指令的下一条指令和上一个函数的ebp进行了压栈保存了下来,当函数调用结束后我们pop了ebp找到了上个函数的栈底,并且ret出栈,返回到对应的call调用的下一条指令,若有返回值,则放入eax中带回。


点个赞再走呗~~~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值