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+8return 0;
004C1874  xor         eax,eax  }

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于RTOS的STM32游戏机

1.游戏机的主要功能 所有游戏都来着B站JL单片机博主开源 这款游戏机具备存档与继续游戏功能&#xff0c;允许玩家在任何时候退出当前游戏并保存进度&#xff0c;以便日后随时并继续之前的冒险。不仅如此&#xff0c;游戏机还支持多任务处理&#xff0c;玩家可以在退出当前游戏…

ONLYOFFICE 文档 8.3 已发布:PDF 图章、合并形状、更多格式支持等

ONLYOFFICE 最新版本的在线编辑器已发布&#xff0c;包含约 30 项新功能和多个错误修复。阅读本文&#xff0c;了解所有更新内容。 关于 ONLYOFFICE 文档 ONLYOFFICE 是一个开源项目&#xff0c;专注于高级和安全的文档处理。坐拥全球超过 1500 万用户&#xff0c;ONLYOFFICE …

第二次连接k8s平台注意事项

第二次重新打开集群平台 1.三台机子要在VMware打开 2.MobaBXterm连接Session 3.三个机子docker重启 systemctl restart docker4.主节点进行平台链接 docker pull kubeoperator/kubepi-server[rootnode1 home]# docker pull kubeoperator/kubepi-server [rootnode1 home]# # 运…

通过多层混合MTL结构提升股票市场预测的准确性,R²最高为0.98

“Boosting the Accuracy of Stock Market Prediction via Multi-Layer Hybrid MTL Structure” 论文地址&#xff1a;https://arxiv.org/pdf/2501.09760 ​​​​​​​ 摘要 本研究引入了一种创新的多层次混合多任务学习架构&#xff0c;致力于提升股市预测的效能。此架构融…

结合深度学习、自然语言处理(NLP)与多准则决策的三阶段技术框架,旨在实现从消费者情感分析到个性化决策

针对电商个性化推荐场景的集成机器学习和稳健优化三阶段方案。 第一阶段:在线评论数据处理&#xff0c;利用深度学习和自然语言处理技术进行特征挖掘&#xff0c;进而进行消费者情感分析&#xff0c;得到消费者偏好 在第一阶段&#xff0c;我们主要关注如何通过深度学习和自然语…

【React】受控组件和非受控组件

目录 受控组件非受控组件基于ref获取DOM元素1、在标签中使用2、在组件中使用 受控组件 表单元素的状态&#xff08;值&#xff09;由 React 组件的 state 完全控制。组件的 state 保存了表单元素的值&#xff0c;并且每次用户输入时&#xff0c;React 通过事件处理程序来更新 …

嵌入式八股文面试题(一)C语言部分

1. 变量/函数的声明和定义的区别&#xff1f; &#xff08;1&#xff09;变量 定义不仅告知编译器变量的类型和名字&#xff0c;还会分配内存空间。 int x 10; // 定义并初始化x int x; //同样是定义 声明只是告诉编译器变量的名字和类型&#xff0c;但并不为它分配内存空间…

【Android】jni开发之导入opencv和libyuv来进行图像处理

做视频图像处理时需要对其进行水印的添加&#xff0c;放在应用层调用工具性能方面不太满意&#xff0c;于是当下采用opencvlibyuv方法进行处理。 对于Android的jni开发不是很懂&#xff0c;我的需求是导入opencv方便在cpp中调用&#xff0c;但目前找到的教程都是把opencv作为模…

HTML应用指南:利用GET请求获取全国盒马门店位置信息

随着新零售业态的发展&#xff0c;门店位置信息的获取变得至关重要。作为新零售领域的先锋&#xff0c;盒马鲜生不仅在商业模式创新上持续领先&#xff0c;还积极构建广泛的门店网络&#xff0c;以支持其不断增长的用户群体。本篇文章&#xff0c;我们将继续探究GET请求的实际应…

20240206 adb 连不上手机解决办法

Step 1: lsusb 确认电脑 usb 端口能识别设备 lsusb不知道设备有没有连上&#xff0c;就插拔一下&#xff0c;对比观察多了/少了哪个设备。 Step 2: 重启 adb server sudo adb kill-serversudo adb start-serveradb devices基本上就可以了&#xff5e; Reference https://b…

【BUUCTF逆向题】[MRCTF2020]Transform

一.[MRCTF2020]Transform 64位无壳&#xff0c;IDA打开发现main函数进入反编译 阅读程序 先输入33位code再加密处理然后验证是否相等的题型 逆向看&#xff0c;验证数组byte_40F0E0已知 再往上看加密处理方式 就是将Str&#xff08;我们输入的flag&#xff09;的每一个索引处…

寒假2.5

题解 web:[网鼎杯 2020 朱雀组]phpweb 打开网址&#xff0c;一直在刷新&#xff0c;并有一段警告 翻译一下 查看源码 每隔五秒钟将会提交一次form1&#xff0c;index.php用post方式提交了两个参数func和p&#xff0c;func的值为date&#xff0c;p的值为Y-m-d h:i:s a 执行fu…

【正点原子K210连载】第六十七章 音频FFT实验 摘自【正点原子】DNK210使用指南-CanMV版指南

第六十七章 音频FFT实验 本章将介绍CanMV下FFT的应用&#xff0c;通过将时域采集到的音频数据通过FFT为频域。通过本章的学习&#xff0c;读者将学习到CanMV下控制FFT加速器进行FFT的使用。 本章分为如下几个小节&#xff1a; 32.1 maix.FFT模块介绍 32.2 硬件设计 32.3 程序设…

【Prometheus】如何通过golang生成prometheus格式数据

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

从零开始:OpenCV 图像处理快速入门教程

文章大纲 第1章 OpenCV 概述 1.1 OpenCV的模块与功能  1.2 OpenCV的发展 1.3 OpenCV的应用 第2章 基本数据类型 2.1 cv::Vec类 2.2 cv&#xff1a;&#xff1a;Point类 2.3 cv&#xff1a;&#xff1a;Rng类 2.4 cv&#xff1a;&#xff1a;Size类 2.5 cv&#xff1a;&…

Vim跳转文件及文件行结束符EOL

跳转文件 gf 从当前窗口打开那个文件的内容&#xff0c;操作方式&#xff1a;让光标停在文件名上&#xff0c;输入gf。 Ctrlo 从打开的文件返回之前的窗口 Ctrlwf 可以在分割的窗口打开跳转的文件&#xff0c;不过在我的实验不是次次都成功。 统一行尾格式 文本文件里存放的…

MLA 架构

注&#xff1a;本文为 “MLA 架构” 相关文章合辑。 未整理去重。 DeepSeek 的 MLA 架构 原创 老彭坚持 产品经理修炼之道 2025 年 01 月 28 日 10:15 江西 DeepSeek 的 MLA&#xff08;Multi-head Latent Attention&#xff0c;多头潜在注意力&#xff09;架构 是一种优化…

变压器-000000

最近一个项目是木田12V的充电器&#xff0c;要设计变压器&#xff0c;输出是12V,电压大于1.5A12.6*1.518.9W. 也就是可以将变压器当成初级输入的一个负载。输入端18.9W. 那么功率UI 。因为变压器的输入是线性上升的&#xff0c;所以电压为二份之一&#xff0c;也就是1/2*功率…

【DeepSeek】私有化本地部署图文(Win+Mac)

目录 一、DeepSeek本地部署【Windows】 1、安装Ollama 2、配置环境变量 3、下载模型 4、使用示例 a、直接访问 b、chatbox网页访问 二、DeepSeek本地部署【Mac】 1、安装Ollama 2、配置环境变量 3、下载模型 4、使用示例 5、删除已下载的模型 三、DeepSeek其他 …

02vue3实战-----项目目录详解

02vue3实战-----项目目录详解 1.目录完整结构2.extensions.json文件3.node_modules文件夹4.public文件夹5.src文件夹6.文件.gitignore7.文件env.d.ts8.文件index.html9.文件package-lock.json和文件package.json10.文件README.md11.文件vite.config.ts12.文件tsconfig.json和文…