精简函数栈帧:优化创建和销毁过程的完全解析(建议收藏,提升内功)

请添加图片描述
请添加图片描述
Alt

🌈个人主页:是店小二呀
🌈C语言笔记专栏:C语言笔记
🌈C++笔记专栏: C++笔记

🌈喜欢的诗句:无人扶我青云志 我自踏雪至山巅
请添加图片描述

文章目录

  • 【前文】
  • 一、函数栈帧的概念(stack frame)
  • 二、函数栈帧的作用
  • 三、函数栈帧的创建和销毁解析
    • 3.1 铺垫知识:
    • 3.2 认识相关寄存器和汇编指令
    • 3.3 解析函数栈帧的创建和销毁
  • 【正文】(逐步分析)
    • 一、创建main函数的函数栈帧
    • 二、main函数变量的创建
    • 三、传参操作(在函数调用前)
    • 四、函数栈帧的销毁


【前文】

一、函数栈帧的概念(stack frame)

写代码的时候,常会把一个独立实现的功能抽象为函数,所以C语言是以函数为基础单位的。

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

  • 函数参数和返回值
  • 临时变量(包括函数的非静态的局部变量以及编译器自动产出的其他临时变量)
  • 保存上下信息(包括在函数调用前后需要保持不变的寄存器)

二、函数栈帧的作用

主要解决下面问题:

  1. 局部变量如何创建的?

  2. 为什么局部变量不初始化内容是随机的?

  3. 函数调用时参数时如何传递的?

  4. 传参的顺序是怎么样的?

  5. 函数的形参和实参是分别怎么实例化的?

  6. 函数的返回值如何带会的?

三、函数栈帧的创建和销毁解析

3.1 铺垫知识:

栈(stack):是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。

这里的栈,不是数据结构中栈,而是动态内存区域中的栈。在计算机系统中,栈是一个具有先进后出(First In Last Out,FIFO)的属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。(压栈操作使得栈增大,而弹出操作使得栈减小)

在经典的操作系统中,栈总是向下增长(由高地址向低地址)

在这里插入图片描述

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

相关寄存器:

  • eax:通用寄存器,保留临时数据,常用于返回值
  • ebx:通用寄存器,保留临时数据
  • ebp:栈底寄存器
  • esp:栈顶寄存器
  • eip:指令寄存器,保存当前指令的下一条指令的地址(函数调用可以看到)
  • exc: 计数寄存器

相关汇编命令:

  • mov:数据转移指令
  • push:数据入栈,同时esp栈顶寄存器也要发生改变
  • pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
  • sub:减法命令
  • add:加法命令
  • call:函数调用,1. 压入返回地址 2. 转入目标函数
  • jump:通过修改eip(指令寄存器),转入目标函数,进行调用
  • ret:恢复返回地址,压入eip,类似pop eip命令
  • lea:装入有效地址,操作数就是地址

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

预备知识:

  • 每一次函数调用,都需要在栈中开辟空间(函数栈帧)

  • 这块空间需要使用两个寄存器维护:esp(栈顶)和ebp(栈底),ebp记录栈底的地址,esp记录栈顶的地址

  • 函数栈帧的创建和销毁过程,在不同的编译器上实现的方法大同小异(本次在vs2019)

在这里插入图片描述

函数调用堆栈是反馈函数调用逻辑的,那我们可以清晰的观察到, main 函数调用之前,是由invoke_main 函数来调用main函数,那么内存中以invoke_main 函数开辟好了函数栈帧。

准备环境(便于函数栈帧的过程足够清晰)

在这里插入图片描述

转到反汇编(调试到main函数开始执行的第一行,右击鼠标转到反汇编 )

注意】:VS编译器每次调试都会为程序重新分配内存,对于每次反汇编代码是一次调试代码过程中数据,每次调试略有差异。


【正文】(逐步分析)

一、创建main函数的函数栈帧

00E718B0  push        ebp  //ebp寄存器中的值压栈,存放invoke_main函数栈帧的ebp,esp-4
00E718B1  mov         ebp,esp  //mov指令会把move指令会把esp的值存放到ebp中,相当于产生了main函数的ebp,这个值就是invoke_main函数栈帧的esp
00E718B3  sub         esp,0E4h  ///sub会让esp中的地址减去一个16进制数字0xe4,产生新的esp,此时的esp是main函数栈帧的esp,此时结合上一条指令的ebp和当前的esp,ebp和esp之间维护了一个块栈空间,这块栈空间就是为main函数开辟的,就是main函数的栈帧空间,这一段空间中将存储main函数中的局部变量,临时数据已经调试信息等。
00E718B9  push        ebx  //将寄存器ebx的值压栈,esp-4
00E718BA  push        esi  //将寄存器esi的值压栈,esp-4
00E718BB  push        edi  //将寄存器edi的值压栈,esp-4
 //上面3条指令保存了3个寄存器的值在栈区,这3个寄存器的在函数随后执行中可能会被修改,所以先保存寄存器原来的值,以便在退出函数时恢复。
00E718BC  lea         edi,[ebp-24h]//ebp-24h的地址,放在edi
00E718BF  mov         ecx,9  //次数
00E718C4  mov         eax,0CCCCCCCCh//0CCCCCCCCh数值存放在eax寄存器
00E718C9  rep stos    dword ptr es:[edi] // 将从edp-0x2h到ebp这一段的内存的每个字节都初始化为0xCCCCCCCh(0xCCCC(两个连续排列的0xCC)的汉字编码就是“烫”,所以0xCCCC被当作文本就是“烫”)

注:最后四条代码等价下面的伪代码
edi = ebp-0x24;
ecx = 9;
eax = 0xCCCCCCCC;
for(; ecx = 0; --ecx,edi+=4)
{
*(int*)edi = eax;
}

请添加图片描述

二、main函数变量的创建

int a = 3;
00BE183B mov dword ptr [ebp-8],3 //将3存储到ebp-8的地址处,ebp-8的位置其实就是a变量
int b = 5;
00BE1842 mov dword ptr [ebp-14h],5 //将5存储到ebp-14h的地址处,ebp-14h的位置其实是b变量
int ret = 0;
00BE1849 mov dword ptr [ebp-20h],0 //将0存储到ebp-20h的地址处,ebp-20h的位置其实是ret变量

说明】:

以上汇编代码表示的变量a、b、ret的创建和初始化,这就是局部的变量的创建和初始化。其实是局部变量的创建时在局部变量所在函数的栈帧空间中创建的

请添加图片描述

三、传参操作(在函数调用前)

//调用Add函数
ret = Add(a, b);
//调用Add函数时的传参
//其实传参就是把参数push到栈帧空间中
00BE1850 mov eax,dword ptr [ebp-14h] //传递b,将ebp-14h处放的5放在eax寄存器00BE1853 push eax //将eax的值压栈,esp-4
00BE1854 mov ecx,dword ptr [ebp-8] //传递a,将ebp-8处放的3放在ecx寄存器中
00BE1857 push ecx //将ecx的值压栈,esp-4
00BE1858 call 00BE10B4//(函数的目标地址)    
 

call 指令是要执行函数调用逻辑的,在执行call指令之前先会把call指令的下一条指令的地址进行压栈操作,这个操作是为了解决当函数调用结束后要回到call指令的下一条指令的地方,继续往后执行。

请添加图片描述

代码执行到Add函数的时候,就要开始创建Add函数的栈帧空间了。
在Add函数中创建栈帧的方法和在main函数中是相似的,在栈帧空间的大小上略有差异而已。

  1. 将main函数的 ebp 压栈

  2. 计算新的 ebp 和 esp

  3. 将 ebx , esi , edi 寄存器的值保存

  4. 计算求和,在计算求和的时候,我们是通过 ebp 中的地址进行偏移访问到了函数调用前压栈进去的
    参数,这就是形参访问。

  5. 将求出的和放在 eax 寄存器尊准备带回

int Add(int x, int y)
{
00BE1760 push ebp //将main函数栈帧的ebp保存,esp-4
00BE1761 mov ebp,esp //将main函数的esp赋值给新的ebp,ebp现在是Add函数的ebp
00BE1763 sub esp,0CCh //给esp-0xCC,求出Add函数的esp
00BE1769 push ebx //将ebx的值压栈,esp-4
00BE176A push esi //将esi的值压栈,esp-4
00BE176B push edi //将edi的值压栈,esp-4
int z = 0;
00BE176C mov dword ptr [ebp-8],0 //将0放在ebp-8的地址处,其实就是创建z
z = x + y;
//接下来计算的是x+y,结果保存到z中
00BE1773 mov eax,dword ptr [ebp+8] //将ebp+8地址处的数字存储到eax中
00BE1776 add eax,dword ptr [ebp+0Ch] //将ebp+12地址处的数字加到eax寄存中
00BE1779 mov dword ptr [ebp-8],eax //将eax的结果保存到ebp-8的地址处,其实就是放到z中
return z;
00BE177C mov eax,dword ptr [ebp-8] //将ebp-8地址处的值放在eax中,其实就是把z的值存储到eax寄存器中,这里是想通过eax寄存器带回计算的结果,做函数的返回值。
}

请添加图片描述

图片中的 a’ 和 b’ 其实就是 Add 函数的形参 x , y 。这里的分析很好的说明了函数的传参过程,以及函数
在进行值传递调用的时候,形参其实是实参的一份拷贝。对形参的修改不会影响实参。

四、函数栈帧的销毁

00BE177F pop edi //在栈顶弹出一个值,存放到edi中,esp+4
00BE1780 pop esi //在栈顶弹出一个值,存放到esi中,esp+4
00BE1781 pop ebx //在栈顶弹出一个值,存放到ebx中,esp+4
00BE1782 mov esp,ebp //再将Add函数的ebp的值赋值给esp,相当于回收了Add函数的栈
帧空间
00BE1784 pop ebp //弹出栈顶的值存放到ebp,栈顶此时的值恰好就是main函数的ebp,esp+4,此时恢复了main函数的栈帧维护,esp指向main函数栈帧的栈顶,ebp指向了main函数栈帧的栈
底。
00BE1785 ret //ret指令的执行,首先是从栈顶弹出一个值,此时栈顶的值就是call指令下一条指令的地址,此时esp+4,然后直接跳转到call指令下一条指令的地址处,继续往下执行

请添加图片描述

回到了call指令的下一条指令的地方:

00BE185D add esp,8 
 //esp直接+8,相当于跳过了main函数中压栈的a'和b'
00BE1860 mov dword ptr [ebp-20h],eax 
 //将eax中值,存档到ebp-0x20的地址处,其实就是存储到main函数中ret变量中,而此时eax中就是Add函数中计算的x和y的和,可以看出来,本次函数的返回值是由eax寄存器带回来的。程序是在函数调用返回之后,在eax中去读取返回值的.

注意】:其实返回对象时内置类型时,一般都是通过寄存器来带回返回值的,返回对象如果时较大的对象时,一般会在主调函数的栈帧中开辟一块空间,然后把这块空间的地址,隐式传递给被调函数,在被调函数中通过地址找到主调函数中预留的空间,将返回值直接保存到主调函数的。

具体可以参考《程序员的自我修养》一书的第10章。


请添加图片描述
以上就是本篇文章的所有内容,在此感谢大家的观看!这里是店小二C语言笔记,希望对你在学习C语言中有所帮助!

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

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

相关文章

VS(Visual Studio)中查找项目里的中文字符

目录 正则表达式查找中文字符 正则表达式查找中文字符 在Visual Studio (VS) 中查找所有的中文字符,你可以使用其强大的查找和替换功能。不过,由于中文字符的范围非常广泛(包括简体中文、繁体中文、日本汉字、韩国汉字等)&#xf…

【Docker第一课】docker的基本命令和试启动容器(详细图解)

目录 知识梗概 docker的初步了解 了解docker常用命令 试开启容器(这里演示nginx、python3和mysql) 1、nginx容器的启动 2、python3容器的启动 docker的作用 虚拟机与容器的区别 写在前面: 本专栏你将了解docker一些入门知识&#xff…

拼多多新店和老店哪个好做

拼多多新店和老店哪个好做 拼多多推广可以使用3an推客。3an推客(CPS模式)给商家提供的营销工具,由商家自主设置佣金比例,激励推广者去帮助商家推广商品链接,按最终有效交易金额支付佣金,不成交不扣费。是商…

【书生·浦语大模型实战营第二期】Lagent AgentLego 智能体应用搭建——学习笔记6

文章目录 概述Lagent: 轻量级智能体框架Lagent Web Demo用Lagent自定义工具 AgentLego:组装智能体“乐高”直接使用AgentLego作为智能体工具使用AgentLego用AgentLego自定义工具 参考资料 概述 Lagent是什么 一个轻量级开源智能体框架,提供了一些典型工…

计算机毕业设计python在线交友系统django+vue

Flask 是一个轻量级的 Web 框架,使用 Python 语言编写,较其他同类型框架更为灵活、轻便且容易上手,小型团队在短时间内就可以完成功能丰富的中小型网站或 Web 服务的实现。 本在线交友系统管理员功能有个人中心,用户管理&#xff…

面试经典150题——Z 字形变换

面试经典150题 day22 题目来源我的题解方法一 使用StringBuilder数组模拟矩阵方法二 找规律直接构造 题目来源 力扣每日一题;题序:6 我的题解 方法一 使用StringBuilder数组模拟矩阵 如果numRows是1,则直接返回s。 否则,创建nu…

【数据结构】算法的效率(时间复杂度和空间复杂度)

目录 一.算法的效率 二.时间复杂度 1.概念 2.大O的渐进表示法 3.常见时间复杂度计算举例 三.空间复杂度 四.常见复杂度对比 五. 复杂度的oj练习 1.消失的数字 2.轮转数字: 一.算法的效率 算法在编写成可执行程序后,运行时需要耗费时间资源和空…

区块链 | IPFS:Merkle DAG(进阶版)

🦊原文:Merkle DAGs: Structuring Data for the Distributed Web 🦊写在前面:本文属于搬运博客,自己留存学习。 1 Merkle DAG 当我们在计算机上表示图时,必须通过提供节点和边的具体表示来编码我们的数据…

黑马点评项目个人笔记+项目优化调整

博客须知 本篇博客内容来源与黑马点评项目实战篇-16.用户签到-实现签到功能_哔哩哔哩_bilibili,作者对视频内容进行了整合,由于记笔记时图片使用的是本地路径,所以导致博客的图片无法正常显示,如果有图片需求可以下载上方的pdf须…

给thinkpadP15V加装固态硬盘的TIPS

O(∩_∩)O首先要先恭喜小编,终于舍得给咱们的小伙伴加固态硬盘了 1、确定固态硬盘的类型: M.2接口NVMe协议, Pcie通道( Peripheral company interconnect express)(3.0第三代1gb每秒,4.0第四代2 gb每秒)x4…

PDF中伪代码、原理示意图等导出为矢量图

需求:将 LaTeX 中生成的伪代码 PDF 转换成 svg 或 emf 格式的矢量图,然后插入 word 或 ppt 中。 1 伪代码PDF导出为矢量图 1.1 通过 Adobe Illustrator 处理将 先新建一个空白的PDF,然后文件-->置入导入PDF; 2.选中这个图片…

k8s部署maven项目

failed to verify certificate: x509: certificate signed by unknown authority 今天在执行kubectl get nodes的时候报的证书验证问题,看了一圈首次搭建k8s的都是高频出现的问题。 couldn’t get current server API group list: Get “https://kubernetes.docker…

SwiftUI 5.0(iOS 17.0,macOS 14.0+)新 Inspector 辅助视图之趣味漫谈

概览 在 SwiftUI 开发中,苹果为我们提供了多种辅助视图用来显示额外信息从而极大丰富了应用的表现力,比如:Alert、Sheet、ContextMenu 等等。 从 SwiftUI 5.0(iOS 17+)开始, 又增加了一种全新的辅助视图:Inspector。 在本篇博文中,您将学到如下内容: 概览1. Inspe…

[笔试训练](十一)

目录 031:游游的水果大礼包 032:买卖股票的最好时机(二) 033:倒置字符串 031:游游的水果大礼包 游游的水果大礼包 (nowcoder.com) 题目: 题解: 枚举:依次枚举1号礼…

连接一个 IP 不存在的主机时,会发生什么?(面试)

一、IP 不存在时 如果 IP 在局域网内,会发送 N 次 ARP 请求获得目的主机的 MAC 地址,同时不能发出 TCP 握手消息。 如果 IP 在局域网外,会将消息通过路由器发出,但因为最终找不到目的地,触发 TCP 重试流程。 二、IP…

React 第十五章 Ref

React ref 是 React 中一个用于访问组件中 DOM 元素或者类实例的方式。它允许我们直接操作 DOM,而不需要通过 state 或 props 来更新组件。 过时 API:String 类型的 Refs 在最最早期的时候,React 中 Ref 的用法非常简单,类似于 …

设计模式: 工厂模式

工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 工厂模式提供了一种创建对象的方式,而无需指定要创建的具体类。 工厂模式属于创建型…

如何用 Redis 实现延迟队列?

延迟队列是一种常见的消息队列模式,用于处理需要延迟执行的任务或消息。Redis 是一种快速、开源的键值对存储数据库,具有高性能、持久性和丰富的数据结构,因此很适合用于实现延迟队列。在这篇文章中,我们将详细讨论如何使用 Redis…

数据库(MySQL)基础:多表查询(一)

一、多表关系 概述 项目开发中,在进行数据库表结构设计时,会根据业务需求及业务模块之间的关系,分析并设计表结构,由于业务之间相互关联,所以各个表结构之间也存在着各种联系,基本上分为三种:…