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

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函数的整个过程后便能明白,上述提出的问题也可以直接回答出

相关文章:

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

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

NLP_[2]_文本预处理-文本数据分析

文章目录 4 文本数据分析1 文件数据分析介绍2 数据集说明3 获取标签数量分布4 获取句子长度分布5 获取正负样本长度散点分布6 获取不同词汇总数统计7 获取训练集高频形容词词云8 小结 4 文本数据分析 学习目标 了解文本数据分析的作用.掌握常用的几种文本数据分析方法. 1 文…...

【工具篇】深度揭秘 Midjourney:开启 AI 图像创作新时代

家人们,今天咱必须好好唠唠 Midjourney 这个在 AI 图像生成领域超火的工具!现在 AI 技术发展得那叫一个快,各种工具层出不穷,Midjourney 绝对是其中的明星产品。不管你是专业的设计师、插画师,还是像咱这种对艺术创作有点小兴趣的小白,Midjourney 都能给你带来超多惊喜,…...

从O(k*n)到O(1):如何用哈希表终结多层if判断的性能困局

【前言】   本文将以哈希表重构实战为核心&#xff0c;完整展示如何将传统条件匹配逻辑(上千层if-else判断)转化为O(1)的哈希表高效实现。通过指纹验证场景的代码级解剖&#xff0c;您将深入理解&#xff1a;   1.哈希函数设计如何规避冲突陷阱   2.链式寻址法的工程实现…...

视频采集卡接口

采集卡的正面有MIC IN、LINE IN以及AUDIO OUT三个接口&#xff0c; MIC IN为麦克风输入&#xff0c;我们如果要给采集到的视频实时配音或者是在直播的时候进行讲解&#xff0c;就可以在这里插入一个麦克风&#xff0c; LINE IN为音频线路输入&#xff0c;可以外接播放背景音乐…...

蓝桥杯真题 - 像素放置 - 题解

题目链接&#xff1a;https://www.lanqiao.cn/problems/3508/learning/ 个人评价&#xff1a;难度 3 星&#xff08;满星&#xff1a;5&#xff09; 前置知识&#xff1a;深度优先搜索 整体思路 深搜&#xff0c;在搜索过程中进行剪枝&#xff0c;剪枝有以下限制条件&#xf…...

vue基础(三)

常用指令 1. v-bind 固定绑定与动态绑定&#xff1a; 语法&#xff1a; 标准语法&#xff1a;v-bind:属性"动态数据" 简写语法&#xff1a;:属性"动态数拓" <!DOCTYPE html> <html lang"en"><head><me…...

使用Python开发PPTX压缩工具

引言 在日常办公中&#xff0c;PPT文件往往因为图片过大而导致文件体积过大&#xff0c;不便于传输和存储。为了应对这一问题&#xff0c;我们可以使用Python的wxPython图形界面库结合python-pptx和Pillow&#xff0c;开发一个简单的PPTX压缩工具。本文将详细介绍如何实现这一…...

ubuntu24.04安装布置ros

最近换电脑布置机器人环境&#xff0c;下了24.04&#xff0c;但是网上的都不太合适&#xff0c;于是自己试着布置好了&#xff0c;留作有需要的人一起看看。 文章目录 目录 前言 一、确认 ROS 发行版名称 二、检查你的 Ubuntu 版本 三、安装正确的 ROS 发行版 四、对于Ubuntu24…...

SQL 秒变 ER 图 sql转er图

&#x1f680;SQL 秒变 ER 图&#xff0c;校园小助手神了&#xff01; 学数据库的宝子们集合&#x1f64b;‍♀️ 是不是每次碰到 SQL 转 ER 图就头皮发麻&#xff1f;看着密密麻麻的代码&#xff0c;脑子直接死机&#xff0c;好不容易理清一点头绪&#xff0c;又被复杂的表关…...

【AI知识点】如何判断数据集是否噪声过大?

【AI论文解读】【AI知识点】【AI小项目】【AI战略思考】【AI日记】【读书与思考】【AI应用】 判断数据集是否 噪声过大 是数据分析和机器学习建模过程中至关重要的一步。噪声数据会导致模型难以学习数据的真实模式&#xff0c;从而影响预测效果。以下是一些常见的方法来判断数据…...

网络安全治理架构图 网络安全管理架构

网站安全攻防战 XSS攻击 防御手段&#xff1a; - 消毒。 因为恶意脚本中有一些特殊字符&#xff0c;可以通过转义的方式来进行防范 - HttpOnly 对cookie添加httpOnly属性则脚本不能修改cookie。就能防止恶意脚本篡改cookie 注入攻击 SQL注入攻击需要攻击者对数据库结构有所…...

如何写出优秀的单元测试?

写出优秀的单元测试需要考虑以下几个方面&#xff1a; 1. 测试用例设计 测试用例应该覆盖被测试代码的不同场景和边界情况&#xff0c;以尽可能发现潜在的问题。在设计测试用例时需要关注以下几点&#xff1a; 输入输出数据&#xff1a;要测试的函数或方法可能有多个输入参数…...

数据留痕的方法

在项目中&#xff0c;数据变更时&#xff0c;经常需要记录上次的数据&#xff0c;以便查看对比&#xff0c;专业术语叫做数据留痕。数据变更留痕&#xff08;即记录数据的变更历史&#xff09;是一个常见的需求&#xff0c;例如在审计、追踪数据变化或满足合规性要求的场景中。…...

机器学习数学基础:19.线性相关与线性无关

一、线性相关与线性无关的定义 &#xff08;一&#xff09;线性相关 想象我们有一组向量&#xff0c;就好比是一群有着不同“力量”和“方向”的小伙伴。给定的向量组 α ⃗ 1 , α ⃗ 2 , ⋯ , α ⃗ m \vec{\alpha}_1, \vec{\alpha}_2, \cdots, \vec{\alpha}_m α 1​,α 2…...

ArgoCD实战指南:GitOps驱动下的Kubernetes自动化部署与Helm/Kustomize集成

摘要 ArgoCD 是一种 GitOps 持续交付工具,专为 Kubernetes 设计。它能够自动同步 Git 仓库中的声明性配置,并将其应用到 Kubernetes 集群中。本文将介绍 ArgoCD 的架构、安装步骤,以及如何结合 Helm 和 Kustomize 进行 Kubernetes 自动化部署。 引言 为什么选择 ArgoCD?…...

JVM虚拟机以及跨平台原理

相信大家已经了解到Java具有跨平台的特性&#xff0c;即“一次编译&#xff0c;到处运行”&#xff0c;例如在Windows下编写的程序&#xff0c;无需任何修改就可以在Linux下运行&#xff0c;这是C和C很难做到的。 那么&#xff0c;跨平台是怎样实现的呢&#xff1f;这就要谈及…...

【AIGC提示词系统】基于 DeepSeek R1 + ClaudeAI 易经占卜系统

上篇因为是VIP&#xff0c;这篇来一个免费的 提示词在最下方&#xff0c;喜欢的点个关注吧 引言 在人工智能与传统文化交融的今天&#xff0c;如何让AI系统能够传递传统易经文化的智慧&#xff0c;同时保持易经本身的神秘感和权威性&#xff0c;是一个极具挑战性的课题。本文将…...

电路笔记 : opa 运放失调电压失调电流输入偏置电流 + 反向放大器的平衡电阻 R3 = R1 // R2 以减小输出直流噪声

目录 定义影响和解决失调电压输入偏置电流平衡电阻R3推导公式&#xff1a; 失调电流 实际的运算放大器&#xff08;Op-Amp&#xff09;存在一些非理想特性&#xff0c;如失调电压&#xff08;VIO&#xff09;、失调电流&#xff08;IIO&#xff09;和输入偏置电流&#xff08;I…...

ScrapeGraphAI颠覆传统网络爬虫技术

ScrapeGraphAI颠覆传统网络爬虫技术&#xff01; 引言 在互联网时代&#xff0c;数据如同油田&#xff0c;丰富而深邃。但如何有效地提取这些数据&#xff0c;仍然是许多开发者面临的艰巨任务。你有没有想过&#xff0c;传统的网络爬虫技术是否已经过时&#xff1f;如今&…...

C++_核心编程_多态案例二-制作饮品

#include <iostream> #include <string> using namespace std;/*制作饮品的大致流程为&#xff1a;煮水 - 冲泡 - 倒入杯中 - 加入辅料 利用多态技术实现本案例&#xff0c;提供抽象制作饮品基类&#xff0c;提供子类制作咖啡和茶叶*//*基类*/ class AbstractDr…...

深入剖析AI大模型:大模型时代的 Prompt 工程全解析

今天聊的内容&#xff0c;我认为是AI开发里面非常重要的内容。它在AI开发里无处不在&#xff0c;当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗"&#xff0c;或者让翻译模型 "将这段合同翻译成商务日语" 时&#xff0c;输入的这句话就是 Prompt。…...

在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能

下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能&#xff0c;包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...

前端倒计时误差!

提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

高频面试之3Zookeeper

高频面试之3Zookeeper 文章目录 高频面试之3Zookeeper3.1 常用命令3.2 选举机制3.3 Zookeeper符合法则中哪两个&#xff1f;3.4 Zookeeper脑裂3.5 Zookeeper用来干嘛了 3.1 常用命令 ls、get、create、delete、deleteall3.2 选举机制 半数机制&#xff08;过半机制&#xff0…...

连锁超市冷库节能解决方案:如何实现超市降本增效

在连锁超市冷库运营中&#xff0c;高能耗、设备损耗快、人工管理低效等问题长期困扰企业。御控冷库节能解决方案通过智能控制化霜、按需化霜、实时监控、故障诊断、自动预警、远程控制开关六大核心技术&#xff0c;实现年省电费15%-60%&#xff0c;且不改动原有装备、安装快捷、…...

关于 WASM:1. WASM 基础原理

一、WASM 简介 1.1 WebAssembly 是什么&#xff1f; WebAssembly&#xff08;WASM&#xff09; 是一种能在现代浏览器中高效运行的二进制指令格式&#xff0c;它不是传统的编程语言&#xff0c;而是一种 低级字节码格式&#xff0c;可由高级语言&#xff08;如 C、C、Rust&am…...

C# 求圆面积的程序(Program to find area of a circle)

给定半径r&#xff0c;求圆的面积。圆的面积应精确到小数点后5位。 例子&#xff1a; 输入&#xff1a;r 5 输出&#xff1a;78.53982 解释&#xff1a;由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982&#xff0c;因为我们只保留小数点后 5 位数字。 输…...

处理vxe-table 表尾数据是单独一个接口,表格tableData数据更新后,需要点击两下,表尾才是正确的

修改bug思路&#xff1a; 分别把 tabledata 和 表尾相关数据 console.log() 发现 更新数据先后顺序不对 settimeout延迟查询表格接口 ——测试可行 升级↑&#xff1a;async await 等接口返回后再开始下一个接口查询 ________________________________________________________…...

08. C#入门系列【类的基本概念】:开启编程世界的奇妙冒险

C#入门系列【类的基本概念】&#xff1a;开启编程世界的奇妙冒险 嘿&#xff0c;各位编程小白探险家&#xff01;欢迎来到 C# 的奇幻大陆&#xff01;今天咱们要深入探索这片大陆上至关重要的 “建筑”—— 类&#xff01;别害怕&#xff0c;跟着我&#xff0c;保准让你轻松搞…...