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语言…...
VSCode便捷开发
一、常用插件 Vue 3 Snippets、Vetur、Vue - Official 二、常用开发者工具 三、Vue中使用Element-UI 安装步骤: 1、在VSCode的终端执行如下指令: npm i element-ui -S 2、在main.js中全局引入: import Vue from vue; import ElementUI from …...
二、tsp学习笔记——LINUX SDK编译
开发环境:window11 wsl ubuntu24.04 lypwslDESKTOP-39T8VTC:~$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 24.04.1 LTS Release: 24.04 Codename: noble linux_sdk同步 tspi_linux_sdk_repo_202…...
langchain教程-2.prompt
前言 该系列教程的代码: https://github.com/shar-pen/Langchain-MiniTutorial 我主要参考 langchain 官方教程, 有选择性的记录了一下学习内容 这是教程清单 1.初试langchain2.prompt3.OutputParser/输出解析4.model/vllm模型部署和langchain调用5.DocumentLoader/多种文档…...
分析用户请求K8S里ingress-nginx提供的ingress流量路径
前言 本文是个人的小小见解,欢迎大佬指出我文章的问题,一起讨论进步~ 我个人的疑问点 进入的流量是如何自动判断进入iptables的四表?k8s nodeport模式的原理? 一 本机环境介绍 节点名节点IPK8S版本CNI插件Master192.168.44.1…...
初阶数据结构:树---堆
目录 一、树的概念 二、树的构成 (一)、树的基本组成成分 (二)、树的实现方法 三、树的特殊结构------二叉树 (一)、二叉树的概念 (二)、二叉树的性质 (三&#…...
Vue WebSocket简单应用 ws
webSocket应用 <template><div></div> </template><script> import { getToken } from "/utils/auth"; export default {data() {return {url: "",Socket: null, //socket对象lockReconnect: false, //锁定拒绝重连close: …...
feign 远程调用详解
在平常的开发工作中,我们经常需要跟其他系统交互,比如调用用户系统的用户信息接口、调用支付系统的支付接口等。那么,我们应该通过什么方式进行系统之间的交互呢?今天,简单来总结下 feign 的用法。 1:引入依…...
Sentinel的安装和做限流的使用
一、安装 Release v1.8.3 alibaba/Sentinel GitHubA powerful flow control component enabling reliability, resilience and monitoring for microservices. (面向云原生微服务的高可用流控防护组件) - Release v1.8.3 alibaba/Sentinelhttps://github.com/alibaba/Senti…...
讯飞智作 AI 配音技术浅析(三):自然语言处理
自然语言处理(NLP)是讯飞智作 AI 配音技术的重要组成部分,负责将输入的文本转换为机器可理解的格式,并提取出文本的语义和情感信息,以便生成自然、富有表现力的语音。 一、基本原理 讯飞智作 AI 配音的 NLP 技术主要包…...
wxWidgets生成HTML文件,带图片转base64数据
编译环境大家可以看我之前的文章,CodeBlocks + msys2 + wx3.2,win10 这里功能就是生成HTML文件,没用HTML库,因为是自己固定的格式,图片是一个vector,可以动态改变数量的。 效果如下: #include <wx/string.h> #include <wx/file.h> #include <wx/ima…...
python开发:爬虫示例——GET和POST请求处理
一、Get请求 import json import requests#输入示例:urlhttps://www.baidu.com #RequestHeader:F12标头-请求标头-原始-复制到这(忽略第一句) def GetRequest(url,RequestHeader""):try:dic{}RequestHeaderList RequestHeader.s…...
webrtc协议详细解释
### 一、概述与背景 WebRTC(Web Real-Time Communication)最早由 Google 在 2011 年开源,旨在为浏览器与移动端应用提供客户端直连(点对点)方式进行实时音视频及数据传输的能力。传统的网络应用在进行高实时性音视频通…...
leetcode——组合总和(回溯算法详细讲解)
在面试或刷题过程中,回溯算法是一个绕不开的核心算法之一。今天,我们来详细解析 LeetCode 39「组合总和」 问题,并用 Java 回溯 剪枝优化 来高效解决它!这篇文章不仅适合初学者,也适合希望提高回溯算法的朋友们。 给你…...
深入浅出DeepSeek LLM 以长远主义拓展开源语言模型
深入浅出地讲解DeepSeek LLM 以长远主义拓展开源语言模型 🌟 1. 什么是 DeepSeek LLM? 大家想象一下,你在游戏里要打造一个超级英雄角色,选择最强的装备、技能点和升级策略。那么,DeepSeek LLM 就是 AI 界的“超级英雄…...
【matlab基本使用笔记】
ctrl a i 代码格式化 fzero求非线性函数的根 arrayfun将函数应用于每个数组元素 format long长格式输出 format long g取消科学计数法 linspace logspace 一、界面使用 1.创建matlab脚本 利用.m后缀的脚本文件(又称为m文件)编程: 点击…...
实名制-网络平台集成身份证实名认证接口/身份证查询-PHP
在当今数字化快速发展的时代,线上平台的安全性和用户体验成为了衡量其成功与否的关键因素。其中,身份证实名认证接口的集成显得尤为重要,它不仅为用户提供了更加安全、可靠的网络环境,同时也增强了平台的信任度和合规性。 对于任…...
机器学习--python基础库之Matplotlib (1) 超级详细!!!
机器学习--python基础库Matplotlib 机器学习--python基础库Matplotlib0 介绍1 实现基础绘图-某城市温度变化图1.1绘制基本图像1.2实现一些其他功能 2 再一个坐标系中绘制多个图像3 多个坐标系显示-plt.subplots(面向对象的画图方法)4 折线图的应用场景 机器学习–python基础库M…...
Android 中实现 PDF 预览三种方式
目录 1. 使用第三方库 PdfRenderer(适用于 Android 5.0 及以上) 步骤:2. 使用第三方库 MuPDF步骤:3. 使用第三方库 PdfiumAndroid步骤: 1. 使用第三方库 PdfRenderer(适用于 Android 5.0 及以上)…...
10. k8s二进制集群之Kube Scheduler部署
在开始之前需要准备什么?创建kube-scheduler证书请求文件【即证书的生成⓵】根据上面证书配置文件生成kube-scheduler证书【即证书的生成⓶】创建与关联kube-scheduler配置文件(为后面生成系统服务做准备)创建kube-scheduler服务配置文件【准备系统服务⓵】创建kube-schedul…...
bat脚本实现自动化漏洞挖掘
bat脚本 BAT脚本是一种批处理文件,可以在Windows操作系统中自动执行一系列命令。它们可以简化许多日常任务,如文件操作、系统配置等。 bat脚本执行命令 echo off#下面写要执行的命令 httpx 自动存活探测 echo off httpx.exe -l url.txt -o 0.txt nuc…...
一文解释nn、nn.Module与nn.functional的用法与区别
🌈 个人主页:十二月的猫-CSDN博客 🔥 系列专栏: 🏀零基础入门PyTorch框架_十二月的猫的博客-CSDN博客 💪🏻 十二月的寒冬阻挡不了春天的脚步,十二点的黑夜遮蔽不住黎明的曙光 目录 …...
Unity VideoPlayer播放视屏不清晰的一种情况
VideoPlayer的Rnder Texture可以设置Size,如果你的视屏是1920*1080那么就设置成1920*1080。 如果设置成其他分辨率比如800*600会导致视屏不清晰。...
Docker 数据卷(Volume)详细介绍
Docker 数据卷(Volume)详细介绍 1. 什么是 Docker 数据卷? Docker 数据卷(Volume)是一种用于 持久化数据 和 容器间数据共享 的机制。由于容器的存储是临时的,容器删除后其中的数据会丢失,因此…...
【玩转全栈】--创建一个自己的vue项目
目录 vue介绍 创建vue项目 vue页面介绍 element-plus组件库 启动项目 vue介绍 Vue.js 是一款轻量级、易于上手的前端 JavaScript 框架,旨在简化用户界面的开发。它采用了响应式数据绑定和组件化的设计理念,使得开发者可以通过声明式的方式轻松管理数据和…...
揭秘区块链隐私黑科技:零知识证明如何改变未来
文章目录 1. 引言:什么是零知识证明?2. 零知识证明的核心概念与三大属性2.1 完备性(Completeness)2.2 可靠性(Soundness)2.3 零知识性(Zero-Knowledge) 3. 零知识证明的工作原理4. 零…...
力扣 239.滑动窗口最大值
思路 滑动窗口 遍历 解题思路 基本思路:使用滑动窗口法遍历数组,动态维护当前窗口的最大值。 特殊情况:该方法有一个缺陷,如果出窗口的元素是当前窗口的最大值max时,接下来的窗口中的最大值就无法确定了,所…...
堆的实现——堆的应用(堆排序)
文章目录 1.堆的实现2.堆的应用--堆排序 大家在学堆的时候,需要有二叉树的基础知识,大家可以看我的二叉树文章:二叉树 1.堆的实现 如果有⼀个关键码的集合 K {k0 , k1 , k2 , …,kn−1 } ,把它的所有元素按完全⼆叉树…...
【3】高并发导出场景下,服务器性能瓶颈优化方案-文件压缩
使用EasyExcel导出并压缩文件是一种高效且常见的解决方案,尤其适用于需要处理大量数据的场景。 1. 导出多个Excel文件并压缩成ZIP文件的基本流程 (1)数据准备:从数据库或其他数据源获取需要导出的数据,并将其存储在Ja…...
Ubuntu20.04 本地部署 DeepSeek-R1
一、下载ollama 打开 ollama链接,直接终端运行提供的命令即可。如获取的命令如下: curl -fsSL https://ollama.com/install.sh | sh确保是否安装成功可在终端输入如下命令: ollama -v注意: 如遇到Failed to connect to github.…...
