• 提升C内功--函数栈帧的创建和销毁(动画讲解)


    前言
    💖作者龟龟不断向前
    简介宁愿做一只不停跑的慢乌龟,也不想当一只三分钟热度的兔子。
    👻专栏C++初阶知识点

    👻工具分享

    1. 刷题: 牛客网 leetcode
    2. 笔记软件:有道云笔记
    3. 画图软件:Xmind(思维导图) diagrams(流程图)

    在这里插入图片描述

    如果觉得文章对你有帮助的话,还请点赞,关注,收藏支持博主🙊,如有不足还请指点,博主及时改正

    函数栈帧

    🚀1.预备知识点

    🍉1.1 什么是函数栈帧

      我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。

    那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系。

    函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:

    • 函数参数函数返回值

    • 临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)

    • 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。

     

    🍉1.2函数栈帧能解决什么问题呢?
    1. 局部变量是如何创建的?

    2. 为什么局部变量不初始化内容是随机的?

    3. 函数调用时参数时如何传递的?传参的顺序是怎样的?

    4. 函数的形参和实参分别是怎样实例化的?

    5. 函数的返回值是如何带会的?

     

    🍉1.3相关寄存器和汇编指令

    寄存器:

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

     

    指令:

    mov:数据转移指令
    push:数据入栈,同时esp栈顶寄存器也要发生改变
    pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
    sub:减法命令
    add:加法命令
    call:函数调用,1. 压入返回地址 2. 转入目标函数
    jump:通过修改eip,转入目标函数,进行调用
    ret:恢复返回地址,压入eip,类似pop eip命令
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

     这里不理解没有任何关系,我们在下面会逐句指令,进行相关解释。

    🚀2.函数之间的调用关系(谁调用了main函数)

      以下都以该程序代码来讲解内容。环境:visual stdio2013

    #include 
    
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

     

      大家都知道上述代码中,main函数会调用Add函数,那请问main函数会不会也是某个函数调用的呢?确实如此

      按F10将程序调试起来,并且将代码跑到Add函数的内部,查看调用堆栈的窗口。

    在这里插入图片描述

     

    在这里插入图片描述

     其中下面的函数先被调用,我们不难发现,Add是被main函数调用,并且main函数是被__tmainCRTStartup函数所调用的,而且__tmainCRTStartup函数是被mainCRTStarup函数调用的。这就是该程序函数调用的关系。

     

    🚀3.函数调用的过程

      接下来咱们会从汇编语言的角度,带着同学们逐步分析函数调用的过程,以及解释一些对我们理解比较重要的指令和寄存器。

    F10调式起来,进入反汇编。

    在这里插入图片描述

     

    🍉3.1.main函数 函数栈帧的创建

    espebp是维护函数栈帧的两个寄存器,esp–存放栈顶指针,ebp–存放栈底指针

    提示:栈是向低地址处生长的,这也是为什么压栈,越要-esp的值,因为在向低地址处生长。

    00FD1410  push        ebp  //1.将ebp的内容压栈,esp-4
    00FD1411  mov         ebp,esp  //2.将esp的内容赋值给ebp
    00FD1413  sub         esp,0E4h  //3.0E4h--十六进制的0E--十进制的228,即esp-228
    00FD1419  push        ebx  //ebx压栈
    00FD141A  push        esi  //esi压栈
    00FD141B  push        edi  //edi压栈
        
    00FD141C  lea         edi,[ebp-0E4h]  
    00FD1422  mov         ecx,39h  
    00FD1427  mov         eax,0CCCCCCCCh  
    00FD142C  rep stos    dword ptr es:[edi]
    //这四句指令的内容意思:
    //将edi到ebp之间的内容全部修改成十六进制的CCCCCCCC,这也是烫烫烫烫的缘由
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

     

    动画演示

    在这里插入图片描述

     

    🍉3.2.main函数核心代码执行
    	int a = 3;
    00FD142E  mov         dword ptr [ebp-8],3 //ebp-8的内容放上3 ,a
    	int b = 5;
    00FD1435  mov         dword ptr [ebp-14h],5  //ebp-20的内容放上5,b
    	int ret = 0;
    00FD143C  mov         dword ptr [ebp-20h],0  //ebp-32的内容放上0,ret
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

     

    动画演示

    在这里插入图片描述

     

    🍉3.3函数传参,调用函数
    	ret = Add(a, b);
    //传参
    00FD1443  mov         eax,dword ptr [ebp-14h]  //将ebp-20(b)的内容放进eax里面
    00FD1446  push        eax  //将eax压栈
    00FD1447  mov         ecx,dword ptr [ebp-8]  //将ebp-8(a)的内容放进ecx
    00FD144A  push        ecx  //将ecx压栈
    //调用Add函数
    00FD144B  call        00FD10E1  //call指令
    00FD1450  add         esp,8  //call指令的下一句指令
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

      call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。

     

    动画演示

    在这里插入图片描述

     此时我们发现,函数传参的顺序是:从右到左进行传参

     

    我们按F11,会跳转进下面的场景

    _Add:
    00FD10E1  jmp         00FD13C0  //转入Add函数,进行调用
    
    • 1
    • 2

    此时我们再次按F11,即可跳进Add函数当中

     

    🍉3.4.Add函数 函数栈帧的创建
    int Add(int x, int y)
    {
    int Add(int x, int y)
    {
    00FD13C0  push        ebp  
    00FD13C1  mov         ebp,esp  
    00FD13C3  sub         esp,0CCh  
    00FD13C9  push        ebx  
    00FD13CA  push        esi  
    00FD13CB  push        edi  
        
    00FD13CC  lea         edi,[ebp-0CCh]  
    00FD13D2  mov         ecx,33h  
    00FD13D7  mov         eax,0CCCCCCCCh  
    00FD13DC  rep stos    dword ptr es:[edi] 
    //栈帧的建立是类似的,大家直接看动画演示
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

     

    🍉3.5.Add函数核心代码执行
    	int z = 0;
    00FD13DE  mov         dword ptr [ebp-8],0  //将ebp-8(z)的内容赋为0
    	z = x + y;
    00FD13E5  mov         eax,dword ptr [ebp+8]  //将ebp+8的内容(a)放进eax里
    00FD13E8  add         eax,dword ptr [ebp+0Ch]  //将ebp+12的内容(b)加进eax里面
    00FD13EB  mov         dword ptr [ebp-8],eax  //将eax的内容放进ebp-8(z)里面
    	return z;
    00FD13EE  mov         eax,dword ptr [ebp-8]  //将ebp-8的内容(z)放在eax里面
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

     

    动画演示

    在这里插入图片描述

    🍉3.6.Add函数栈帧的销毁

      通过上一步我们发现,在Add函数开始销毁之前,它将返回值先保存在了eax里面

    00FD13F1  pop         edi  //edi出栈
    00FD13F2  pop         esi  //esi出栈
    00FD13F3  pop         ebx  //ebx出栈
    00FD13F4  mov         esp,ebp  //将ebp的值赋值给esp
    00FD13F6  pop         ebp  //ebp出栈
    00FD13F7  ret  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

     

      pop弹出栈顶的值存放到ebp,栈顶此时的值恰好就是main函数的ebp,esp+4,此时恢复了main函数的栈帧维护,esp指向main函数栈帧的栈顶,ebp指向了main函数栈帧的栈底。

       ret:ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指令下一条指令的地址,此时esp+4,然后直接跳转到call指令下一条指令的地址处,继续往下执行

    动画演示

    在这里插入图片描述

     

    🍉3.7.main函数的剩余代码
    00FD1450  add         esp,8  
    	ret = Add(a, b);
    00FD1453  mov         dword ptr [ebp-20h],eax  //将eax的内容放进ebp-32的内容(ret)里面
        
    //以下的功能大家可以不用琢磨,不影响我们理解函数栈帧
    	printf("%d\n", ret);
    00FD1456  mov         esi,esp  
    00FD1458  mov         eax,dword ptr [ebp-20h]  
    00FD145B  push        eax  
    00FD145C  push        0FD5858h  
    00FD1461  call        dword ptr ds:[00FD9114h]  
    00FD1467  add         esp,8  //将两个函数参数出栈
    00FD146A  cmp         esi,esp  
    00FD146C  call        00FD113B  
    	return 0;
    00FD1471  xor         eax,eax  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述

    注意:call指令的下一句指令的地址,其实早已经在上一步的ret指令中就出栈了

     

    🍉3.8.main函数 函数栈帧的销毁

      同样也是与Add函数栈帧销毁类似

    00FD1473  pop         edi  
    00FD1474  pop         esi  
    00FD1475  pop         ebx  
    00FD1476  add         esp,0E4h  
    00FD147C  cmp         ebp,esp  
    00FD147E  call        00FD113B  
    00FD1483  mov         esp,ebp  
    00FD1485  pop         ebp  
    00FD1486  ret
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

     

    动画演示

    在这里插入图片描述

      main函数和Add函数的调用过程结束。

     

    希望大家看了这篇文章对函数的调用能有不一样的理解,咱们下期间!

    在这里插入图片描述

  • 相关阅读:
    《TCP/IP网络编程》阅读笔记--Socket类型及协议设置
    线程池的自定义策略
    阿里云物联网IOT平台使用案例教程(模拟智能设备)
    information_schema过滤与无列名注入
    【echarts】19、echarts+vue2 - 折线图柱状图
    C#参数修饰符params
    FPGA运算
    初识设计模式 - 单例模式
    电商系统对接支付渠道的解决方案
    设计模式之代理模式(Proxy Pattern)
  • 原文地址:https://blog.csdn.net/m0_64361907/article/details/127758318