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.mzph.cn/news/894856.shtml

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

相关文章

25/2/6 <机器人基础> 运动学中各连杆的变换矩阵求法

变换矩阵 机器人通常包含多个关节和连杆&#xff0c;每个关节和连杆都有自己的局部坐标系。变换矩阵能够将一个点或向量从一个坐标系转换到另一个坐标系&#xff0c;从而实现对机器人各个部件位置和姿态的统一描述 变换矩阵能够将复杂的运动分解为旋转和平移的组合。通过矩阵乘…

AllData数据中台核心菜单十二:数据同步平台

&#x1f525;&#x1f525; AllData大数据产品是可定义数据中台&#xff0c;以数据平台为底座&#xff0c;以数据中台为桥梁&#xff0c;以机器学习平台为中层框架&#xff0c;以大模型应用为上游产品&#xff0c;提供全链路数字化解决方案。 ✨奥零数据科技官网&#xff1a;…

【FPGA】 MIPS 12条整数指令 【3】

实现乘除 修改框架 EX&#xff1a;实现带符号乘除法和无符号乘除法 HiLo寄存器&#xff1a;用于存放乘法和除法的运算结果。Hi、Lo为32bit寄存器。电路描述与实现RegFile思想一致 仿真 代码 DataMem.v include "define.v"; module DataMem(input wire clk,input…

文件基础IO

理解"文件" 1-1 狭义理解 文件在磁盘里磁盘是永久性存储介质&#xff0c;因此文件在磁盘上的存储是永久性的磁盘是外设&#xff08;即是输出设备也是输入设备&#xff09;磁盘上的文件 本质是对文件的所有操作&#xff0c;都是对外设的输入和输出简称IO 1-2 广义理…

Unity 简易的UI框架

核心内容 UIType.cs namespace MYTOOL.UI {/// <summary>/// UI层级/// </summary>public enum UILayer{/// <summary>/// 主界面层/// </summary>MainUI 0,/// <summary>/// 普通界面层/// </summary>NormalUI 1,/// <summary>/…

VUE2双向绑定的原理

文章目录 VUE2双向绑定的原理1. 什么是双向绑定2. 双向绑定的原理2.1 ViewModel的重要作用2.2 双向绑定的流程 3. 双向绑定的实现3.1 data响应化处理3.2 Compile编译3.3 依赖收集 VUE2双向绑定的原理 1. 什么是双向绑定 讲双向绑定先讲单项绑定&#xff0c;啥叫单项绑定&…

4G核心网的演变与创新:从传统到虚拟化的跨越

4G核心网 随着移动通信技术的不断发展&#xff0c;4G核心网已经经历了从传统的硬件密集型架构到现代化、虚拟化网络架构的重大转型。这一演变不仅提升了网络的灵活性和可扩展性&#xff0c;也为未来的5G、物联网&#xff08;LOT&#xff09;和边缘计算等技术的发展奠定了基础。…

HTML排版标签、语义化标签、块级和行内元素详解

目录 前言 一、HTML中的排版标签 1. 文本相关标签 1.1 标题标签 ~ 1.2 段落标签 1.3 强调和加粗 1.4 换行标签 1.5 水平线标签 二、HTML中的语义化标签 2.1 语义化标签概述 2.2 常见的语义化标签 示例&#xff08;核心代码部分&#xff09;&#xff1a; 三、HTM…

【字节青训营-7】:初探 Kitex 字节微服务框架(使用ETCD进行服务注册与发现)

本文目录 一、Kitex概述二、第一个Kitex应用三、IDL四、服务注册与发现 一、Kitex概述 长话短说&#xff0c;就是字节跳动内部的 Golang 微服务 RPC 框架&#xff0c;具有高性能、强可扩展的特点&#xff0c;在字节内部已广泛使用。 如果对微服务性能有要求&#xff0c;又希望…

【数学】矩阵、向量(内含矩阵乘法C++)

目录 一、前置知识&#xff1a;向量&#xff08;一列或一行的矩阵&#xff09;、矩阵1. 行向量2. 列向量3. 向量其余基本概念4. 矩阵基本概念5. 关于它们的细节 二、运算1. 转置&#xff08;1&#xff09;定义&#xff08;2&#xff09;性质 2. 矩阵&#xff08;向量&#xff0…

浅尝yolo11全程记录1-准备环境+官网模型推理(个人备份)

准备工作&#xff08;虚拟环境、导入项目&#xff09; 安装Anaconda 主要是为了创建和管理虚拟环境&#xff0c;在pycharm里按照项目里的requirments.txt安装依赖的时候&#xff0c;使用虚拟环境会好很多&#xff08;我记得不用Anaconda也可以直接在pycharm的terminal里头创建…

5.攻防世界 fileinclude

进入题目页面如下 提示flag在flag.php ctrlu&#xff0c;查看源码 给出了一段PHP代码&#xff0c;进行代码审计 <?php // 检查是否开启了错误显示功能 if( !ini_get(display_errors) ) {// 如果没有开启&#xff0c;则将错误显示功能设置为开启状态ini_set(display_error…

红包雨项目前端部分

创建项目 pnpm i -g vue/cli vue create red_pakage pnpm i sass sass-locader -D pnpm i --save normalize.css pnpm i --save-dev postcss-px-to-viewportpnpm i vantlatest-v2 -S pnpm i babel-plugin-import -Dhttps://vant.pro/vant/v2/#/zh-CN/<van-button click&…

蓝桥杯嵌入式备赛(三)—— LED +按键 + LCD

目录 一、LED1、原理图介绍2、程序代码 二、按键1、原理图介绍2、程序代码 三、LCD1、原理图介绍2、程序代码 一、LED 1、原理图介绍 如果所示&#xff0c;STM32G431RBT6中有八个LED&#xff0c;由八个GPIO控制&#xff0c;分别为PC8-15&#xff0c;当输出为低电平时点亮。其中…

[Java基础]函数式编程

Lambda函数 JDK8新增的语法形式, 使用Lambda函数替代某些匿名内部类对象&#xff0c;从而让程序代码更简洁&#xff0c;可读性更好。 基本使用 lambda表达式只能简化函数式接口的匿名内部类写法 // 1.定义抽象类 abstract class Animal {public abstract void crt(); }publi…

Vim 多窗口编辑及文件对比

水平分割 :split 默认使用水平分割的方式。 :split :sp 垂直分割 :vsplit :vs 带文件的分割 :split 文件名 :sp 文件名 在光标所在的窗口&#xff0c;输入分割窗口命令就会对那个窗口进行分割。 切换窗口 Ctrlw 切换正在编辑的窗口 快速分割窗口 Ctrlwn 快速分割当前…

二级C语言题解:十进制转其他进制、非素数求和、重复数统计

目录 一、程序填空&#x1f4dd; --- 十进制转其他进制 题目&#x1f4c3; 分析&#x1f9d0; 二、程序修改&#x1f6e0;️ --- 非素数求和 题目&#x1f4c3; 分析&#x1f9d0; 三、程序设计&#x1f4bb; --- 重复数统计 题目&#x1f4c3; 分析&#x1f9d0; 前言…

使用服务器部署DeepSeek-R1模型【详细版】

文章目录 引言deepseek-r1IDE或者终端工具算力平台体验deepseek-r1模型总结 引言 在现代的机器学习和深度学习应用中&#xff0c;模型部署和服务化是每个开发者面临的重要任务。无论是用于智能推荐、自然语言处理还是图像识别&#xff0c;如何高效、稳定地将深度学习模型部署到…

央行发布《贸易金融分布式账本技术要求》,参考架构包括5部分

《银行科技研究社》(作者 木子剑):2024年12月11日,中国人民银行发布金融行业标准《贸易金融分布式账本技术要求》(JR/T 0308-2024)(以下简称“《要求》”),当日实施。据悉,该文件的起草单位包括6大行和多家股份制银行等。 《要求》规定了分布式账本技术在贸易金融领域…

Python aiortc API

本研究的主要目的是基于Python aiortc api实现抓取本地设备媒体流&#xff08;摄像机、麦克风&#xff09;并与Web端实现P2P通话。本文章仅仅描述实现思路&#xff0c;索要源码请私信我。 1 demo-server解耦 1.1 原始代码解析 1.1.1 http服务器端 import argparse import …