C语言:函数栈帧的创建和销毁

目录

  • 1.什么是函数栈帧
  • 2.理解函数栈帧能解决什么问题
  • 3.函数栈帧的创建和销毁的过程解析
    • 3.1 什么是栈
    • 3.2 认识相关寄存器和汇编指令
    • 3.3 解析函数栈帧的创建和销毁过程
      • 3.3.1 准备环境
      • 3.3.2 函数的调用堆栈
      • 3.3.3 转到反汇编
      • 3.3.4 函数栈帧的创建和销毁

1.什么是函数栈帧

在写C语言代码的时候,我们经常会把一个独立的功能抽象成函数,C程序是以函数为基本单位的,那么函数又是如何调用的呢?函数的参数是怎样传递的呢?这些答案都可以在函数栈帧中寻找

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

  • 函数参数和函数返回值
  • 临时变量(包括函数的非静态的局部变量以及编译器自动产生的其他临时变量)
  • 保存上下文信息(包括用来维护函数调用前后的寄存器)

2.理解函数栈帧能解决什么问题

只要理解好函数栈帧就可以对一下问题有额外的理解:

  • 局部变量是如何创建的?
  • 为什么局部变量不初始化时为随机值?
  • 函数调用时形参的传递的顺序是怎样的?
  • 函数的形参和实参的联系是怎样的?
  • 函数的返回值是如何带回来的?

3.函数栈帧的创建和销毁的过程解析

3.1 什么是栈

栈(stack)是现代计算机程序中最为重要的概念之一,几乎每一个程序都要用到栈,没有栈就没有函数,没有局部变量,更没有更正语言的桥接

在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(该操作被称为压栈:push),也可以将已经压入栈中的数据弹出(出栈:pop),但是栈这个容器遵守一条规则:先进后出

在计算机系统中,栈则是一个具有以上属性的动态内存区域,程序可以将数据压入栈中,也可以将栈弹出,在经典的操作系统中,栈总是向下增长(由高地址到低地址)的,在我们常见的i386或者x86-64下,栈顶由esp的寄存器定位

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

<1.相关寄存器

  • eax:通用寄存器,保留临时数据,常用于返回值
  • ebx:同样寄存器,保留临时数据
  • ebp:栈底寄存器
  • esp:栈顶寄存器,与ebp共同维护当前的函数栈帧
  • eip:指令寄存器,保存当前指令的下一条指令的地址

<2.相关汇编命令

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

3.3 解析函数栈帧的创建和销毁过程

3.3.1 准备环境

为了更好地观察函数栈帧的整个过程,需要先关闭一些选项以免受到干扰:
在这里插入图片描述
在这里插入图片描述

3.3.2 函数的调用堆栈

这里我们写一段简单的代码,并将代码一条一条拆解处理足够好观察内部的细节

注意:函数栈帧的创建和销毁过程,在不同的编译器的实现方法大同小异,但大体的逻辑层次是不会差很多的,本次演示用的是VS2019环境

演示代码:

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

在VS2019环境下,按F10进入调试,打开窗口的调用堆栈:
在这里插入图片描述
调用堆栈是用来反馈函数调用逻辑的

然后继续按F10(按完整个主函数),进入界面:
在这里插入图片描述

我们会发现main函数也是被调用的,这里可以清晰地观察到:invoke_main函数调用了main函数,至于是什么函数调用了invoke_main就不再考虑了

3.3.3 转到反汇编

按F10调试到main函数的第一行,右击鼠标转到反汇编
注意:这里调试出来的地址是由系统自动分配的,所以每一次进去调试出来的地址都是不同的


int main()
{
//main函数的函数栈帧的创建
004C1820  push        ebp  
004C1821  mov         ebp,esp  
004C1823  sub         esp,0E4h  
004C1829  push        ebx  
004C182A  push        esi  
004C182B  push        edi  
004C182C  lea         edi,[ebp-24h]  
004C182F  mov         ecx,9  
004C1834  mov         eax,0CCCCCCCCh  
004C1839  rep stos    dword ptr es:[edi]  

//main函数中的核心代码
	int a = 10;
004C183B  mov         dword ptr [ebp-8],0Ah  
	int b = 20;
004C1842  mov         dword ptr [ebp-14h],14h  
	int c = 0;
004C1849  mov         dword ptr [ebp-20h],0  

	c = Add(a, b);
004C1850  mov         eax,dword ptr [ebp-14h]  
004C1853  push        eax  
004C1854  mov         ecx,dword ptr [ebp-8]  
004C1857  push        ecx
  
//执行call指令会跳转到Add函数内部
004C1858  call        004C10B4  
004C185D  add         esp,8  
004C1860  mov         dword ptr [ebp-20h],eax  

	printf("%d\n", c);
004C1863  mov         eax,dword ptr [ebp-20h]  
004C1866  push        eax  
004C1867  push        4C7B30h  
004C186C  call        004C10D2  
004C1871  add         esp,8  

	return 0;
004C1874  xor         eax,eax  

}

调试至call指令,按住F11:

//Add函数的函数栈帧
int Add(int x, int y)
{
004C1760  push        ebp  
004C1761  mov         ebp,esp  
004C1763  sub         esp,0CCh  
004C1769  push        ebx  
004C176A  push        esi  
004C176B  push        edi  
	int z = 0;
004C176C  mov         dword ptr [ebp-8],0  
	z = x + y;
004C1773  mov         eax,dword ptr [ebp+8]  
004C1776  add         eax,dword ptr [ebp+0Ch]  
004C1779  mov         dword ptr [ebp-8],eax  
	return z;
004C177C  mov         eax,dword ptr [ebp-8]  
}
004C177F  pop         edi  
004C1780  pop         esi  
004C1781  pop         ebx  
004C1782  mov         esp,ebp  
004C1784  pop         ebp  
004C1785  ret  

3.3.4 函数栈帧的创建和销毁

这里我们将拆解每一行汇编代码:

//main函数栈帧的创建,在创建之前esp和ebp维护的是invoke_main的函数栈帧
004C1820  push        ebp  //将ebp寄存器的值进行压栈,此时存放的是invoke_main的函数栈帧的ebp,esp-4
004C1821  mov         ebp,esp  //将esp中的值赋给ebp
004C1823  sub         esp,0E4h //将esp减去0eE4(十六进制的表示),此时的esp已经指向了一个新的区域用来维护main的函数栈帧
004C1829  push        ebx  //把ebx的值进行压栈,esp-4
004C182A  push        esi  //把esi的值进行压栈,esp-4
004C182B  push        edi  //把edi的值进行压栈,esp-4
//上面3条指令保存了3个寄存器的值在栈区,这3个寄存器的在函数随后执行中可能会被修改,所以先保存寄
//存器原来的值,以便在退出函数时恢复。

004C182C  lea         edi,[ebp-24h]  //lea(load effective address)加载有效地址,将ebp-24h的地址放到edi中
004C182F  mov         ecx,9  //将9赋给ecx
004C1834  mov         eax,0CCCCCCCCh  //将0CCCCCCCCh赋给eax
004C1839  rep stos    dword ptr es:[edi]  
//从edi开始将以ecx的存储数值为个数的4个字节的数据全部改为eax存储的值
//dword:double word(双字),一个字为2个字节,双字就是4个字节

该段汇编的内存:
在这里插入图片描述

解释烫烫烫的产生:
在这里插入图片描述
之所以得到了这些奇怪的汉字,是因为这里使用了未初始化的字符数组,就导致buf中存储的就是上面的0CCCCCCCCh的值,而0xCCCC的汉字编码就是“烫”

//main函数中的核心代码
	int a = 10;
004C183B  mov         dword ptr [ebp-8],0Ah  //将0Ah(10)赋给ebp-8的地址处,对变量a初始化
	int b = 20;
004C1842  mov         dword ptr [ebp-14h],14h  //将14h(20)赋给ebp-14h的地址处,对变量b初始化
	int c = 0;
004C1849  mov         dword ptr [ebp-20h],0  //将0赋给ebp-20h的地址处,对变量c初始化

//调用Add函数
	c = Add(a, b);
004C1850  mov         eax,dword ptr [ebp-14h]  //将ebp-14h地址处的值(20)存放在eax中,这里其实就是传递参数b
004C1853  push        eax  //把eax的值进行压栈
004C1854  mov         ecx,dword ptr [ebp-8]  //将ebp-8地址处的值(10)存放在ecx中,这里是传递参数a
004C1857  push        ecx //把ecx的值进行压栈

该段汇编的内存:

在这里插入图片描述

//执行call指令会跳转到Add函数内部,在跳转之前会进行压栈操作
004C1858  call        004C10B4  //把call指令的下一条的地址进行压栈,esp-4,回调函数

//Add函数的函数栈帧的创建
004C1760  push        ebp  //将main函数的ebp的值压栈进行保存,esp-4
004C1761  mov         ebp,esp  //将esp的值赋给ebp
004C1763  sub         esp,0CCh  //将esp减去0CCh,esp开始维护新函数Add的函数栈帧
004C1769  push        ebx  //把ebx的值进行压栈,esp-4
004C176A  push        esi  //将esi的值进行压栈,esp-4
004C176B  push        edi  //将edi的值进行压栈,esp-4
//Add函数中的核心代码
	int z = 0;
004C176C  mov         dword ptr [ebp-8],0 //将0赋给ebp-8的地址处 
	z = x + y;
004C1773  mov         eax,dword ptr [ebp+8]  //将ebp+8地址处的值赋给eax
004C1776  add         eax,dword ptr [ebp+0Ch]  //把ebp+0Ch地址处的值加到eax中
004C1779  mov         dword ptr [ebp-8],eax  //将eax中的值赋到ebp-8的地址处
	return z;
004C177C  mov         eax,dword ptr [ebp-8]  //将ebp-8地址处的值赋给eax
}

该段汇编的内存:
在这里插入图片描述
可以看出形参和实参的关系:形参是实参的一份临时拷贝,对形参的修改并不会改变实参

004C177F  pop         edi  //把edi的值进行出栈,esp+4
004C1780  pop         esi  //把esi的值进行出栈,esp+4
004C1781  pop         ebx  //把ebx的值进行出栈,esp+4
004C1782  mov         esp,ebp  //将ebp的值赋给esp
004C1784  pop         ebp  //把ebp的值进行出栈,ebp此时又回到main函数的ebp,开始维护main函数的函数栈帧,esp+4
004C1785  ret //首先弹出栈顶的值,此时esp+4,并回到call指令的下一条地址处继续执行代码
//Add函数的函数栈帧销毁

回到call指令的下一条指令:

004C185D  add         esp,8 //esp+8  
004C1860  mov         dword ptr [ebp-20h],eax  //将eax的值(存储的就是Add函数的返回值)赋到ebp-20h的地址处

	printf("%d\n", c);
004C1863  mov         eax,dword ptr [ebp-20h]  //将ebp-20h地址处的值赋给eax
004C1866  push        eax  //将eax的值进行压栈
004C1867  push        4C7B30h  //将4C7B30h进行压栈
004C186C  call        004C10D2  //继续回调函数
004C1871  add         esp,8  //esp+8

	return 0;
004C1874  xor         eax,eax  

}

该段汇编的内存():
在这里插入图片描述
小结:对于函数栈帧的创建和销毁过程可以在VS上独自进行反汇编代码解析 + 内存和监视的观察,理解该过程更有助于对代码底层的东西了解的更深,此外,这里对于main函数的函数栈帧的销毁不再解析,相信了解过Add函数的整个过程后便能明白,上述提出的问题也可以直接回答出

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/966406.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

开箱即用的.NET MAUI组件库 V-Control 发布了!

之前写过挺多的MAUI Sample&#xff0c;其中有很多代码可以打包成组件&#xff0c;当组件完善到一定程度&#xff0c;我会把控件封装起来放到控件库中。 今天&#xff0c;在这个仓库建立一年零八个月后&#xff0c;我觉得可以考虑将其作为开源库发布。 有很多网友在观望.NET …

Qt:项目文件解析

目录 QWidget基础项目文件解析 .pro文件解析 widget.h文件解析 widget.cpp文件解析 widget.ui文件解析 main.cpp文件解析 认识对象模型 窗口坐标系 QWidget基础项目文件解析 .pro文件解析 工程新建好之后&#xff0c;在工程目录列表中有⼀个后缀为 ".pro" …

装备库室管控系统|支持国产化、自主研发

装备库室管控系统&#xff08;DW-S306&#xff09;利用现有内部网络&#xff0c;部署综合管理系统&#xff0c;形成一套上下统一、功能完善的管理体系&#xff0c;建设一个功能完善、规范有序为目标&#xff0c;实现可视化监管、数字化军械管理、安全监管于一体的物联网信息化管…

软件测试就业

文章目录 2.6 初识一、软件测试理论二、软件的生产过程三、软件测试概述四、软件测试目的五、软件开发与软件测试的区别&#xff1f;六、学习内容 2.7 理解一、软件测试的定义二、软件测试的生命周期三、软件测试的原则四、软件测试分类五、软件的开发与测试模型1.软件开发模型…

【Java基础】序列化、反序列化和不可变类

Hi~&#xff01;这里是奋斗的明志&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f331;&#x1f331;个人主页&#xff1a;奋斗的明志 &#x1f331;&#x1f331;所属专栏&#xff1a;Java基础面经 &#x1f4da;本系列文章为个…

upx压缩工具使用说明

UPX&#xff08;Ultimate Packer for Executables&#xff09;是一款开源的可执行文件打包工具&#xff0c;能够将可执行文件&#xff08;如Windows的.exe文件或Linux的ELF文件&#xff09;进行压缩&#xff0c;以减少文件大小&#xff0c;并增加反逆向工程的难度。 下载相关安…

DeepSeek-R1 32B Windows+docker本地部署

最近国产大模型DeepSeek兴起&#xff0c;本地部署了一套deepseek同时集成Open WebUI界面,给大家出一期教程。 软件&#xff1a;Ollama、docker、Open WebUI 一、用Ollama下载模型 首先我们需要安装Ollama&#xff0c;它可以在本地运行和管理大模型。 到Ollama官网 https://ol…

活动预告 |【Part 2】Microsoft 安全在线技术公开课:通过扩展检测和响应抵御威胁

课程介绍 通过 Microsoft Learn 免费参加 Microsoft 安全在线技术公开课&#xff0c;掌握创造新机遇所需的技能&#xff0c;加快对 Microsoft Cloud 技术的了解。参加我们举办的“通过扩展检测和响应抵御威胁”技术公开课活动&#xff0c;了解如何更好地在 Microsoft 365 Defen…

(2024|CVPR,MLLM 幻觉)OPERA:通过过度信任惩罚和回顾分配缓解多模态大型语言模型中的幻觉

OPERA: Alleviating Hallucination in Multi-Modal Large Language Models via Over-Trust Penalty and Retrospection-Allocation 目录 1. 引言 2. 相关研究 2.1 多模态大语言模型 2.2 LLM 的幻觉与解决方案 2.3. 语言模型中的解码策略 3. 方法 3.1 MLLM 生成过程 3.2…

激活函数篇 03 —— ReLU、LeakyReLU、ELU

本篇文章收录于专栏【机器学习】 以下是激活函数系列的相关的所有内容: 一文搞懂激活函数在神经网络中的关键作用 逻辑回归&#xff1a;Sigmoid函数在分类问题中的应用 整流线性单位函数&#xff08;Rectified Linear Unit, ReLU&#xff09;&#xff0c;又称修正线性单元&a…

C++20新特性

作者&#xff1a;billy 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 前言 C20 是 C 标准中的一个重要版本&#xff0c;引入了许多新特性和改进&#xff0c;包括模块&#xff08;Modules&#xff09;、协程…

新增md、html压缩文档上传,开放接口访问密钥改为多个,zyplayer-doc 2.4.7 发布啦!

zyplayer-doc是一款适合企业和个人使用的WIKI知识库管理工具&#xff0c;支持在线编辑富文本、Markdown、表格、Office文档、API接口、思维导图、Drawio以及任意的文本文件&#xff0c;专为私有化部署而设计&#xff0c;最大程度上保证企业或个人的数据安全&#xff0c;支持以内…

ES管理器焕新升级:紫色银狼主题来袭!

ES管理器&#xff08;安卓版&#xff09;迎来了一次令人眼前一亮的改头换面&#xff01;此次更新最直观的变化集中在UI界面设计上。开发团队大胆突破&#xff0c;摒弃了以往稍显平庸的风格&#xff0c;引入了极具个性的全新主题——以热门游戏《崩坏&#xff1a;星穹铁道》中的…

SwiftUI 学习 Toggle 遇到的问题

最近学习SwifyUI&#xff0c;心中存疑&#xff0c;于是记录这些问题 List {Toggle(isOn: $showFavoriteOnly) {Text("Favorite only")}ForEach(filterLandmarks) { landmark in// 在 NavigationLink 中&#xff0c;label 是用于指定导航链接显示内容的部分Navigati…

HarmonyOS:电话号码格式化

一、使用场景 不同国家和地区的电话号码在号码位数、组合方式、呈现方式等都存在差异。同时&#xff0c;在不同环境和条件下&#xff0c;电话号码可能存在不同的拨打方式和号码格式。例如&#xff0c;在中国境内跨地区打电话&#xff0c;通常需要先输入“0”&#xff0c;再拨打…

网络分析工具—WireShark的安装及使用

Wireshark 是一个广泛使用的网络协议分析工具&#xff0c;常被网络管理员、开发人员和安全专家用来捕获和分析网络数据包。它支持多种网络协议&#xff0c;能够帮助用户深入理解网络流量、诊断网络问题以及进行安全分析。 Wireshark 的主要功能 数据包捕获与分析&#xff1a; …

优惠券平台(十七):实现用户查询/取消优惠券预约提醒功能

业务背景 当用户预约了一个或多个优惠券抢购提醒后&#xff0c;如果不再需要提醒&#xff0c;可以取消预约通知。不过&#xff0c;虽然用户可以取消提醒&#xff0c;但已经发送到 MQ 的消息不会被撤回&#xff0c;消费者在时间点到达时依然会收到消息。此时&#xff0c;我们不…

10vue3实战-----实现登录的基本功能

10vue3实战-----实现登录的基本功能 1.基本页面的搭建2.账号登录的验证规则配置3.点击登录按钮4.表单的校验5.账号的登录逻辑和登录状态保存6.定义IAccount对象类型 1.基本页面的搭建 大概需要搭建成这样子的页面: 具体的搭建界面就不多讲。各个项目都有自己的登录界面&#…

盘姬工具箱:完全免费的电脑工具箱

今天给大家介绍一个非常好用的系统工具箱&#xff0c;里面内含100多个工具&#xff0c;完全免费使用&#xff0c;而且没有广告&#xff0c;非常的棒。 盘姬工具箱&#xff1a;完全免费的电脑工具箱 盘姬工具箱是一款完全免费的电脑工具箱&#xff0c;功能丰富且实用。软件下载并…

国产编辑器EverEdit - 编辑辅助功能介绍

1 编辑辅助功能 1.1 各编辑辅助选项说明 1.1.1 行号 打开该选项时&#xff0c;在编辑器主窗口左侧显示行号&#xff0c;如下图所示&#xff1a; 1.1.2 文档地图 打开该选项时&#xff0c;在编辑器主窗口右侧靠近垂直滚动条的地方显示代码的缩略图&#xff0c;如下图所示&…