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


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


    前言

    仅自学笔记
    原文链接:https://blog.csdn.net/m0_64280701/article/details/127160994
    原文链接:https://blog.csdn.net/qq_61635026/article/details/124384367

    一、基础知识

    栈区

    C/C++程序内存分配的几个区域:

    • 栈区(stack):
      在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
    • 堆区(heap):
      一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
    • 数据段(静态区)(static):
      存放全局变量、静态数据。程序结束后由系统释放。
    • 代码段:
      存放函数体(类成员函数和全局函数)的二进制代码。

    认识相关寄存器和汇编指令

    相关寄存器

    • eax:通用寄存器,保留临时数据,常用于返回值
    • ebx:通用寄存器,保留临时数据
    • ecx:计数器,是重复(REP)前缀指令和LOOP指令的内定计数器。
    • edx:总是被用来放整数除法产生的余数。
    • ebp:栈底寄存器,存放地址用来维护函数栈帧
    • esp:栈顶寄存器,存放地址用来维护函数栈帧
    • esi :源索引寄存器
    • edi :目标索引寄存器
    • eip :指令寄存器,保存当前指令的下一条指令的地址

    相关汇编命令

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

    栈区的使用是从高地址到低地址
    栈区的使用遵循先进后出,后进先出


    二、函数栈帧的创建和销毁过程

    提示:推荐使用版本较低的VS编译系统,较高版本的编译系统会优化,越高版本的编译器约不容易观察,本次使用的是2013版本的VS

    以以下代码为例

    #define _CRT_SECURE_NO_WARNINGS  1
    #pragma warning(disable:6031)
    #include
    
    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;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    调用堆栈,__tmainCRTStartup()函数

    1. 按 F10 或 F11 进入调试模式,F10是逐过程,F11是逐语句,打开堆栈
    2. 点击窗口中的调用堆栈,观察main函数是由谁调用的
    3. 按F10,按到return 0 时再按一次,调用栈堆会出现以下内容

    在这里插入图片描述
    这时看到调用堆栈这个窗口
    在这里插入图片描述

    我们可以发现main 函数被 __tmainCRTStartup() 调用
    在这里插入图片描述

    而 __tmainCRTStartup() 又被 mainCRTStartup() 调用
    在这里插入图片描述
    因此此时的栈区大约如下图所示
    在这里插入图片描述

    main函数栈帧的创建

    然后接下来我们要观察C语言代码所对应的汇编代码,在调试状态下,右击鼠标转到反汇编
    在这里插入图片描述
    以下为反汇编代码
    在这里插入图片描述

    第一部分代码解析如下

     push         ebp  //在栈顶开辟ebp寄存器对应的空间
     mov         ebp,esp  //将esp的值传入ebp中(即将ebp指针移动到原本esp指向的位置)
     sub         esp,0E4h  //将esp的内容减去0E4h(将esp移动到原esp-0E4h的位置)
     push        ebx  //在栈顶放入ebx
     push        esi  //在栈顶放入esi
     push        edi  //在栈顶放入edi
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    此时进入main函数(也就是程序调试开始),首先要 push ebp 进行压栈,ebp 在 __tmainCRTStartup() 上面压栈

    观察esp ebp 地址的变化,在调试的监视里面查看,push ebp 之后,esp 指向的位置也随之改变 (地址减小)

    push ebp前
    在这里插入图片描述
    push ebp后
    在这里插入图片描述
    我们可以很明显发现esp的地址值变小了4个字节,这是因为我们在栈顶开辟ebp寄存器对应的空间,原来esp在__tmainCRTStartup()的栈顶,在上面压入一个ebp就导致esp的地址向上了一个单位,也就变小了4个字节
    在这里插入图片描述
    接下来是 mov ebp,esp ,将esp的值传入ebp中(即将ebp指针移动到esp指向的位置)

    move之前
    在这里插入图片描述
    move之后
    在这里插入图片描述
    此时栈区图示(move)
    在这里插入图片描述

    接下来 sub esp,0E4h,将esp的内容减去0E4h(将esp移动到原esp-0E4h的位置,esp-0E4h地址减小)
    0E4h是个16进制数,转换为10进制为228,此举相当于为main函数开辟了以一个228个字节的空间
    在这里插入图片描述
    此时图示(sub)
    在这里插入图片描述
    接下来 push ebx ,在栈顶放入ebx,地址依旧减小
    接下来 push esi ,在栈顶放入 esi,地址依旧减小
    接下来 push edi ,在栈顶放入edi,地址依旧减小
    在这里插入图片描述
    在这里插入图片描述

    第二部分代码解析如下

    lea       edi,[ebp-0E4h]//将ebp-0E4h的地址放入edi
    mov   	  ecx,39h//将39h放入ecx
    mov       eax,0CCCCCCCCh//将0CCCCCCCCh放入eax
    rep stos  dword ptr es:[edi]//将edi往下ecx个地址的数据全部初始化为0CCCCCCCCh(eax)
    
    • 1
    • 2
    • 3
    • 4
    • 这里的数据全部是十六进制数字,数据后面的 h 直接忽略掉即可,它只是编译器十六进制的一种表示形式(word是2个字节,dword是4个字节)
    • 0E4h 转换为十进制为 228, 39h转换为十进制为57,每行4个字节,所以一共就是 57 * 4 = 228 个字节
    • 也就是说明上述几句表示了,将整个刚创建的main函数里的数据全部转换为0CCCCCCCCh,即我们有时编译会产生的结果 烫烫烫烫烫…
    • 调试里打开内存监控,内存监控中的内存地址也是向上减小的

    在这里插入图片描述

    在这里插入图片描述
    此时图示(第二部分)
    在这里插入图片描述

    第三部分函数解析如下

    	int a = 10;
    mov         dword ptr [ebp-8],0Ah  //将ebp-8的位置变成04h
    	int b = 20;
    mov         dword ptr [ebp-14h],14h  //将ebp-24h得到位置变成14h
    	int c = 0;
    mov         dword ptr [ebp-20h],0  //将ebp-20h的位置变成0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    此时图示
    在这里插入图片描述

     c = Add(a, b);
     mov         eax,dword ptr [ebp-14h]//[ebp-14h]是b=20的地址,所以赋值eax为20
     push        eax  //压栈
     mov         ecx,dword ptr [ebp-8]//[ebp-8]是a=10的地址,所以赋值ecx为10
     push        ecx //压栈
    
    • 1
    • 2
    • 3
    • 4
    • 5

    此时图示

    在这里插入图片描述

    接下来为call 指令,按下F11,此时就正式进入Add函数内部 并为其开辟栈帧

    第四部分函数解析如下(进入Add部分)

    按 F11,进入到 Add 函数 ,该add 函数地址不一定与main 函数地址相连,但是add 函数的地址一定在main 函数地址上面
    call 指令调用 Add 函数,这里逐语句(F11)执行,发现这里竟然存储着下一条指令的地址,事实上 call 指令把下一条指令的地址压栈了(为了 Add 函数结束后能找回来),esp 地址也跟着变化

    call     00C210E1
    
    • 1

    在这里插入图片描述

    在这里插入图片描述

    push    ebp//将ebp上移
    mov     ebp,esp//将esp内容放入ebp(移动ebp)
    sub     esp,0CCh//esp-0CCh(为Add开辟空间)
    push    ebx//在栈顶放入ebx
    push    esi//在栈顶放入esi
    push    edi//在栈顶放入edi
    lea      edi,[ebp+FFFFFF34h]//ebp+FFFFFF34h的空间  
    mov      ecx,33h//33存入ecx  
    mov      eax,0CCCCCCCCh//存入eax  
    rep stos  dword ptr es:[edi]//esp往下0ch的空间进行赋值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    具体过程与mian函数的过程基本一致
    此时图示
    在这里插入图片描述
    此时的ebp地址
    在这里插入图片描述

    第五部分函数解析如下(执行Add部分)

    int z = 0;
     mov         dword ptr [ebp-8],0
    z = x + y;
     mov         eax,dword ptr [ebp+8]  //把 ebp+8 的值 10 放到 eax 里
     add         eax,dword ptr [ebp+0Ch]  //把 ebp+0ch 的值 20 和 eax 的值 10 相加
     mov         dword ptr [ebp-8],eax  //把 eax 的值 30 放到 ebp-8(z) 里去
    	return z;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    此时图示
    在这里插入图片描述

    第六部分函数解析如下(Add函数销毁)

    
    	return z;
     mov         eax,dword ptr [ebp-8]  //把ebp-8的值(30)放到eax里头去
    }
     pop         edi  //出栈,释放为edi创建的栈区
     pop         esi  //出栈,释放为esi创建的栈区
     pop         ebx  //出栈,释放为exb创建的栈区//esp地址向下3个单位
     mov         esp,ebp  //ebp的值赋给esp,此时esp和ebp相同
     pop         ebp  //弹出ebp  
     ret  //返回
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    pop edi //出栈,释放为edi创建的栈区
    pop esi //出栈,释放为esi创建的栈区
    pop ebx //出栈,释放为exb创建的栈区//esp地址向下3个单位
    每出栈一次,esp向下一次
    在这里插入图片描述
    ebp的值赋给esp,此时esp和ebp相同,然后再弹出ebp
    此时图示
    在这里插入图片描述
    ret:恢复返回地址,压入eip,类似pop eip命令,使重新回到原来的地址

    此时图示
    在这里插入图片描述

    第七部分函数解析如下(重新回到main函数)

     add         esp,8  //是往esp里加8,即向高位移动,销毁形参
     mov         dword ptr [ebp-20h],eax  //赋值eax(30)的值给c
    	printf("%d\n", c);
     mov         eax,dword ptr [ebp-20h]  
     push        eax  
     push        0EC7B30h  
     call        00EC10D2  
     add         esp,8  
    	return 0;
     xor         eax,eax  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    add esp,8 ,而这一条指令的意思,是往esp里加8,即向高位移动,实际上这条指令就是在销毁我们的形参
    此时图示
    在这里插入图片描述
    接下来就是打印值和 main函数函数栈帧销毁,都与上面类似,这里不多做赘述

  • 相关阅读:
    李开复:未来AI或助力中国成为科技“火车头”
    周四见 | 物流人的一周资讯
    第一章:最新版零基础学习 PYTHON 教程(第三节 - 下载并安装Python最新版本)
    多台的UPS该如何实现系统化的集中监控呢?
    关于log4net的详细使用教程
    七夕的简易代码表白合集
    【JQuery】扩展BootStrap入门——知识点讲解(一)
    RabbitMQ(任务模型,交换机(广播,订阅,通配符订阅))
    Java并发编程核心概念
    低代码开发
  • 原文地址:https://blog.csdn.net/2301_77954967/article/details/136072164