当前位置: 首页 > news >正文

【C语言进阶】函数栈帧的创建和销毁(内功修炼)

目录

前言 

一、基础知识

1.1 什么是栈区?

1.2 寄存器

1.3 测试代码和一些其它的

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

2.1 _tmainCRTStartup函数(调用main函数)栈帧的创建

2.2 main函数栈帧的创建

2.3 main函数内执行有效代码

2.4 Add函数栈帧的创建

2.5 Add函数内执行有效代码 

2.6 Add函数栈帧的销毁

2.7 main函数代码继续执行

 三、所需反汇编代码总览

四、总结


前言 

 在前期的学习过程中,我们可能会有很多的困惑:

  1. 局部变量是怎么创建的?
  2. 为什么未初始化的局部变量的值是随机值?
  3. 函数是如何传参的?以及传参的顺序是怎样的?
  4. 形参和实参是什么关系?
  5. 函数调用是怎么做的?
  6. 函数调用结束后是怎么返回的?

这里使用的环境是 Visual Studio 2019(原本想用 Visual Studio 2013 的,但是没有安装有),提示不要使用太过高级的编译器,因为越高级的编译器越不容易观察。同时这里需要注意的是在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,不是完全相同的,具体细节取决于编译器 

一、基础知识

1.1 什么是栈区?

C/C++程序内存分配的几个区域:
1. 栈区(stack):

  • 在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

2. 堆区(heap):

  • 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。

3. 数据段(静态区)(static):

  • 存放全局变量、静态数据。程序结束后由系统释放。

4. 代码段:

  • 存放函数体(类成员函数和全局函数)的二进制代码

今天文章的内容是关于栈区的,其他简单了解即可 

接下来,补充一下栈的知识,了解到这就可以了,足够使用了

  • 栈区的使用是从高地址到低地址
  • 栈区的使用遵循先进后出,后进先出
  • 栈区的放置是从高地址往低地址放置:push 是压栈
  • 删除是从低往高删除:pop 是出栈

接下来还要了解一个重要的东西,寄存器,寄存器整篇文章都在使用。

 -------------------我是分割线------------------

1.2 寄存器

这里简单介绍一些寄存器,其它的先不要过多理解

常见寄存器有eax、ebx、ecx、edx,这四个都当做通用寄存器,保留临时数据,ebp和esp较为特殊

eax"累加器"  它是很多加法乘法指令的缺省寄存器。
ebx"基地址"寄存器, 在内存寻址时存放基地址。
ecx计数器,是重复(REP)前缀指令和LOOP指令的内定计数器。
edx总是被用来放整数除法产生的余数。
esi源索引寄存器
edi

目标索引寄存器

ebp(栈底指针)"基址指针",存放的是地址,用来维护函数栈帧
esp(栈顶指针)专门用作堆栈指针,存放的是地址,用来维护函数栈帧

1.3 测试代码和一些其它的

使用的测试代码(足够简单才好演示):

#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);
	return 0;
}

接下来还有一些汇编代码的含义:

  • mov:mov是数据传送指令(move的缩写),用于将一个数据从源地址传送到目标地址

  • sub:减法,subtraction的缩写

  • lea:Load effective address的缩写,取有效地址

  • call:用于调用其他函数

  • add:加法

  • pop:出栈

  • push:入栈或压栈

测试编译器使用的是 VS2019,以调试一步步进行演示 

准备工作已经做好,接下来开始演示。

 -------------------我是分割线------------------

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

每一个函数调用,都要在栈区创建一个空间

2.1 _tmainCRTStartup函数(调用main函数)栈帧的创建

先了解 main 函数是被谁调用的,按 F10 或 F11 进入调试模式,F10是逐过程,F11是逐语句,打开堆栈

这时看到调用堆栈这个窗口

按F10,按到return 0 时再按一次,调用栈堆会出现以下内容(我使用的VS2019 没有出现,可能编译器版本太高,优化掉了) 

这时再看堆栈窗口发现 main 函数被 __tmainCRTStartup() 调用

而 __tmainCRTStartup() 又被 mainCRTStartup() 调用

观察C语言代码所对应的汇编代码,在调试状态下,右击鼠标转到反汇编

 转到汇编后,右键 取消符号名,方便查看阅读汇编代码

先看以下汇编代码 

 

 编译器会先在栈区处开辟一部分空间给  __tmainCRTStartup()  和 _mainCRTStartup()  函数,并用 esp 和 ebp 维护,先看下这两个函数栈帧开辟情况:

2.2 main函数栈帧的创建

先看第一部分汇编代码,逐条语句解释

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

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

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

  接下来是 mov  ebp,esp  ,将esp的值传入ebp中(即将ebp指针移动到esp指向的位置)

 接下来 sub  esp,0E4h,将esp的内容减去0E4h(将esp移动到原esp-0E4h的位置,esp-0E4h地址减小)

 

 

  接下来 push  ebx  ,在栈顶放入ebx,地址依旧减小

 接下来 push  esi  ,在栈顶放入 esi,地址依旧减小

  接下来 push  edi  ,在栈顶放入edi,地址依旧减小

步骤演示图:

 接下来: 

lea  edi,[ebp-24h]//将ebp-24h的地址放入edi
mov  ecx,9//将9放入ecx
mov  eax,0CCCCCCCCh//将0CCCCCCCCh放入eax
rep stos  dword ptr es:[edi]//将edi往下ecx个地址的数据全部初始化为0CCCCCCCCh

 lea  edi,[ebp-24h],把 ebp - 0E4h 这个地址加载到 edi 里

顺便看一下,ebp-24h 的地址 

接下来

mov  ecx,9,将9放入ecx
mov  eax,0CCCCCCCCh ,将0CCCCCCCCh放入eax
rep stos  dword ptr es:[edi],将edi往下ecx个地址的数据全部初始化为0CCCCCCCCh

说明:这里的数据全部是十六进制数字,数据后面的 h 直接忽略掉即可,它只是编译器十六进制的一种表示形式 

 这三步执行完,把 edi 这个位置开始向下的 9 行 dword 数据全部改为 0xcccccccc (word是2个字节,dword是4个字节),一共36个字节,一行四字节,共9 行

 调试里打开内存监控,内存监控中的内存地址也是向上减小的

 步骤演示图:

到这main函数的函数栈帧已经创建好了

2.3 main函数内执行有效代码

接下来开始初始化 a、b、c 局部变量,直接看mov初始化,上面两行不用理

mov         ecx,0ECC003h  
call        00EC131B  
	int a = 10;
mov         dword ptr [ebp-8],0Ah  
	int b = 20;
mov         dword ptr [ebp-14h],14h  
	int c = 0;
mov         dword ptr [ebp-20h],0  

mov     dword ptr [ebp-8],0Ah  ,把 0Ah(十进制为10) 放到 ebp-8 的位置

 mov  dword ptr [ebp-14h],14h  ,把 14h(20) 放到 ebp-14h的位置

mov  dword ptr [ebp-20h],0  ,把 0 放到 ebp-20h的位置 

到这里a,b,c 已经初始化完成了

步骤演示图:

接下来: 

 mov         eax,dword ptr [ebp-14h]  
 push        eax  
 mov         ecx,dword ptr [ebp-8]  
 push        ecx  

  mov  eax,dword ptr [ebp-14h]  ,把 ebp-14h 的值0000 0014(十进制是20)放到 eax 里去

 push  eax  ,压栈 eax(20),esp指向的位置也随之改变 (地址减小) 

  mov   ecx,dword ptr [ebp-8]  ,把 ebp-8 的值0000000a(十进制是10) 放到 ecx 里去

 push  ecx  ,压栈 ecx(10),esp指向的位置也随之改变 (地址减小) 

 步骤演示图:(截一部分,用不到的先不截)

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

2.4 Add函数栈帧的创建

按 F11,进入到 Add 函数 ,该add 函数地址不一定与main 函数地址相连,但是add 函数的地址一定在main 函数地址上面

 call        00EC131B  

 call 指令调用 Add 函数,这里逐语句(F11)执行,发现这里竟然存储着下一条指令的地址,事实上 call 指令把下一条指令的地址压栈了(为了 Add 函数结束后能找回来),esp 地址也跟着变化

 进入 Add 函数前,会先为 Add 函数开辟函数栈帧,这这些操作跟先前main函数开辟函数栈帧操作一样,所以这里就不细谈了

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-0Ch]//ebp-0Ch的空间  
mov      ecx,3//3存入ecx  
mov      eax,0CCCCCCCCh//存入eax  
rep stos  dword ptr es:[edi]//esp往下0ch的空间进行赋值

首先,push ebp把ebp压栈到栈顶,再mov把esp赋给ebp,再sub,把esp-去0CCh,此步骤就是在为Add函数开辟空间,接着进行三次push,同main函数那样,同理,依旧是赋值为CCCCCCCC,详细过程不再赘述,跟上文main函数一样,如图所示:

详细:

简图:

2.5 Add函数内执行有效代码 

 mov         ecx,0ECC003h  
 call        00EC131B  
	int z = 0;
 mov         dword ptr [ebp-8],0  //把 0 初始化到 ebp-8 的位置

	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;
 mov         eax,dword ptr [ebp-8]  //把 ebp-8 的值 30 放到 eax 里去

首先,把0放到ebp-8的位置上,接着mov把ebp+8的值放到eax里头去,此时eax就是10。再add给eax加上ebp+0ch,就是把20加进去,此时eax就是30,加完后再把eax(30)放到ebp-8里头去,最终的结果(30)放到z里头去。

 接下来就要进行返回了,也就是Add函数栈帧的销毁,见下文

2.6 Add函数栈帧的销毁

	return z;
 mov         eax,dword ptr [ebp-8]  //把ebp-8的值(30)放到eax里头去
}
 pop         edi  //出栈,释放为edi创建的栈区
 pop         esi  //出栈,释放为esi创建的栈区
 pop         ebx  //出栈,释放为exb创建的栈区
 add         esp,0CCh //为esp地址+0CCh,即退出Add程序的栈区空间  
 cmp         ebp,esp  不理会
 call        00EC1244  不理会
 mov         esp,ebp  //ebp的值赋给esp,此时esp和ebp相同
 pop         ebp  //弹出ebp  
 ret  //返回

 mov   eax,dword ptr [ebp-8]  ,把ebp-8的值(30)放到eax里头去

 pop edi  ,出栈,释放为edi创建的栈区,地址开始增大

 pop esi  ,出栈,释放为esi创建的栈区,地址继续增大

 

  pop ebx  ,出栈,释放为exb创建的栈区,地址继续增大

 步骤演示图:

add     esp,0CCh   ,为esp地址+0CCh,即退出Add程序的栈区空间  ,此时esp和ebp相同

 步骤演示图: 

 mov  esp,ebp  ,ebp的值赋给esp,此时esp和ebp依旧相同

 pop    ebp  ,弹出ebp,并将ebp所指向的main函数的起始地址赋值给了ebp指针,esp指针向高位移动,esp和ebp重新开始维护main函数的栈区空间

  ret  ,返回到main函数,在执行 ret 指令时,esp指针就指向了栈顶存放的call指令的下一条指令的地址,

  步骤演示图:

 此时Add函数的栈帧算是真正销毁

2.7 main函数代码继续执行

 add         esp,8  
 mov         dword ptr [ebp-20h],eax  
	printf("%d\n", c);
 mov         eax,dword ptr [ebp-20h]  
 push        eax  
 push        0EC7B30h  
 call        00EC10D2  
 add         esp,8  
	return 0;
 xor         eax,eax  

 add    esp,8  ,而这一条指令的意思,是往esp里加8,即向高位移动,实际上这条指令就是在销毁我们的形参

  步骤演示图:

  mov      dword ptr [ebp-20h],eax  ,把eax的值放到ebp-20h上,而eax就是我们出Add函数时计算的和

 接下来就是打印值和 main函数函数栈帧销毁,都与上面类似,这里不多做赘述

   -------------------我是分割线------------------  

 三、所需反汇编代码总览

add 函数

int Add(int x, int y)
{
 push        ebp  
 mov         ebp,esp  
 sub         esp,0CCh  
 push        ebx  
 push        esi  
 push        edi  
 lea         edi,[ebp-0Ch]  
 mov         ecx,3  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 mov         ecx,0ECC003h  
 call        00EC131B  
	int z = 0;
 mov         dword ptr [ebp-8],0  
	z = x + y;
 mov         eax,dword ptr [ebp+8]  
 add         eax,dword ptr [ebp+0Ch]  
 mov         dword ptr [ebp-8],eax  
	return z;
 mov         eax,dword ptr [ebp-8]  
}
 pop         edi  
 pop         esi  
 pop         ebx  
 add         esp,0CCh  
 cmp         ebp,esp  
 call        00EC1244  
 mov         esp,ebp  
 pop         ebp  
 ret  

main函数 

int main()
{
 push        ebp  
 mov         ebp,esp  
 sub         esp,0E4h  
 push        ebx  
 push        esi  
 push        edi  
 lea         edi,[ebp-24h]  
 mov         ecx,9  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 mov         ecx,0ECC003h  
 call        00EC131B  
	int a = 10;
 mov         dword ptr [ebp-8],0Ah  
	int b = 20;
 mov         dword ptr [ebp-14h],14h  
	int c = 0;
 mov         dword ptr [ebp-20h],0  
	c = Add(a, b);
 mov         eax,dword ptr [ebp-14h]  
 push        eax  
 mov         ecx,dword ptr [ebp-8]  
 push        ecx  
 call        00EC10B4  
 add         esp,8  
 mov         dword ptr [ebp-20h],eax  
	printf("%d\n", c);
 mov         eax,dword ptr [ebp-20h]  
 push        eax  
 push        0EC7B30h  
 call        00EC10D2  
 add         esp,8  
	return 0;
 xor         eax,eax  
}
 pop         edi  
 pop         esi  
 pop         ebx  
 add         esp,0E4h  
 cmp         ebp,esp  
 call        00EC1244  
 mov         esp,ebp  
 pop         ebp  
 ret  

四、总结

 1、局部变量是怎么创建的?

首先为这个函数分配好栈帧空间,并初始化一部分空间为0xcccccccc,再为局部变量分配空间并初始化

2、为什么未初始化的局部变量的值是随机值?

在开辟好栈帧空间后,会初始化 0xcccccccc 这样的随机值,而局部变量的初始化操作就会将随机值覆盖

3、函数是如何传参的?以及传参的顺序是怎样的?

在调用函数前,会先将函数参数从后向前依次压栈,而进入函数后,它会通过指针的偏移量找到形参

4、形参和实参是什么关系?

形参是在压栈时开辟的空间,实参和形参只是值相同,空间是独立的。所以形参是实参的一份临时拷贝,改变形参不会改变实参

5、函数调用是怎么做的?

函数调用前,它会记住下一条指令的地址,这样做是为了函数结束后能回的来

6、函数调用结束后是怎么返回的?

函数调用结束会通过下一条指令的地址返回,这也是为什么要压栈下一条指令的地址。在返回前它会将计算好的值放在 eax 里

    -------------------我是分割线------------------  

这里只是对函数栈帧的创建和销毁简单描述,需要更详细的百度即可

写完这篇文章给我的感觉就是图真难画....

  -------------------我是分割线------------------  

文章就先到这

相关文章:

  • 结构体数组与结构体指针
  • PTA 3+2 转段考试 数据库 mysql(3篇)
  • 技术创新助力港口自动化与智能化
  • 【Day22】力扣LeetCode算法刷题[811. 子域名访问计数]
  • LinkedList - 链表
  • 【MySQL从入门到精通】【高级篇】(二十四)EXPLAIN中select_type,partition,type,key,key_len字段的剖析
  • C | 函数指针数组妙用
  • springboot:生成excel并且通过邮件发送
  • 【自然语言处理】【文本生成】使用Transformers中的BART进行文本摘要
  • 【Vue】Vue中的计算属性computed
  • 一文步入python大门,基础教程大全(25分钟)
  • 计算机学院第二周语法组及算法组作业
  • 13、设计模式总结
  • [LeetCode][面试算法]逻辑闭环的二分查找代码思路
  • 无卷积步长或池化:用于低分辨率图像和小物体的新 CNN 模块SPD-Conv
  • 「译」Node.js Streams 基础
  • CentOS7简单部署NFS
  • k个最大的数及变种小结
  • redis学习笔记(三):列表、集合、有序集合
  • sublime配置文件
  • vue的全局变量和全局拦截请求器
  • vue总结
  • 技术胖1-4季视频复习— (看视频笔记)
  • 来,膜拜下android roadmap,强大的执行力
  • 前端
  • 前端代码风格自动化系列(二)之Commitlint
  • 融云开发漫谈:你是否了解Go语言并发编程的第一要义?
  • 使用阿里云发布分布式网站,开发时候应该注意什么?
  • 正则表达式
  • 如何用纯 CSS 创作一个菱形 loader 动画
  • #调用传感器数据_Flink使用函数之监控传感器温度上升提醒
  • $con= MySQL有关填空题_2015年计算机二级考试《MySQL》提高练习题(10)
  • (cljs/run-at (JSVM. :browser) 搭建刚好可用的开发环境!)
  • (Oracle)SQL优化技巧(一):分页查询
  • (补)B+树一些思想
  • (附源码)计算机毕业设计SSM保险客户管理系统
  • (切换多语言)vantUI+vue-i18n进行国际化配置及新增没有的语言包
  • (实战篇)如何缓存数据
  • (四)图像的%2线性拉伸
  • (小白学Java)Java简介和基本配置
  • (一)80c52学习之旅-起始篇
  • (原創) 博客園正式支援VHDL語法著色功能 (SOC) (VHDL)
  • * CIL library *(* CIL module *) : error LNK2005: _DllMain@12 already defined in mfcs120u.lib(dllmodu
  • . ./ bash dash source 这五种执行shell脚本方式 区别
  • .NET / MSBuild 扩展编译时什么时候用 BeforeTargets / AfterTargets 什么时候用 DependsOnTargets?
  • .NET “底层”异步编程模式——异步编程模型(Asynchronous Programming Model,APM)...
  • .NET Core Web APi类库如何内嵌运行?
  • .NET MVC第三章、三种传值方式
  • .Net 知识杂记
  • .NET(C#) Internals: as a developer, .net framework in my eyes
  • .netcore 6.0/7.0项目迁移至.netcore 8.0 注意事项
  • .NET开发人员必知的八个网站
  • @ComponentScan比较
  • []利用定点式具实现:文件读取,完成不同进制之间的
  • [2021ICPC济南 L] Strange Series (Bell 数 多项式exp)