深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)

news2025/1/11 19:52:36

本章的内容:

  • 什么是函数栈帧?
  • 理解函数栈帧能解决什么问题?
  • 函数栈帧的创建和销毁解析

本文放到 --> 该专栏内:http://t.csdnimg.cn/poMzA

目录

什么是函数栈帧❓

理解函数栈帧能解决什么问题呢?💢

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

预备知识

什么是栈?

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

相关寄存器

相关汇编命令

必备知识

演示代码:

大体思路:

反汇编代码:

1. _tmainCRTStartup函数栈帧的创建(调用main函数的函数)

2.函数栈帧的创建

3.main函数中的核心代码

🧨call指令🚩

4.Add函数栈帧的创建

5.Add函数中的核心代码🎯

6.Add函数栈帧的销毁 

总结:


什么是函数栈帧❓

     我们在写C 语言代码的时候,经常会把一个独立的功能抽象为函数,所以 C程序是以函数为基本
单位的。那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问
题都和函数栈帧有关系。
        
        函数栈帧(stack frame) 就是函数调用过程中在程序的调用栈(call stack)所开辟的空间 这些空间是用来存放:
  • 函数参数和函数返回值
  • 临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
  • 保存上下文信息(包括在函数调用前后需要保持不变的寄存器)

理解函数栈帧能解决什么问题呢?💢

理解函数栈帧有什么用呢?
只要理解了函数栈帧的创建和销毁,以下问题就能够很好的额理解了:
  • 局部变量是如何创建的?
  • 为什么局部变量不初始化内容是随机的?
  • 函数调用时参数时如何传递的?传参的顺序是怎样的?
  • 形参和实参的关系是什么呢?
  • 函数调用结束后是如何返回的?
让我们一起走进函数栈帧的创建和销毁的过程中。

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

预备知识

什么是栈?

        栈(stack )是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。
        在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push ),也可以将已经压入栈中的数据弹出(出栈,pop ),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out FIFO )。就像叠成一叠的术,先叠上去的书在最下面,因此要最后才能取出。
        在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。
        
        在经典的操作系统中, 栈总是向下增长(由高地址向低地址)的
        在我们常见的i386 或者 x86-64 下,栈顶由成为 esp 的寄存器进行定位的。

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

相关寄存器
寄存器名称                                     简介
eax通用寄存器,保留临时数据,常用于返回值
ebx通用寄存器,保留临时数据
ebp栈底寄存器(Stack bottom
esp栈顶寄存器 (stack top
eip指令寄存器,保存当前指令下一条指令的地址
相关汇编命令
汇编命令解释
mov
数据转移指令(赋值)
push
数据入栈,同时 esp栈顶寄存器 也要发生改变
pop
数据弹出至指定位置,同时 esp栈顶寄存器 也要发生改变
sub
减法命令
add
加法命令
call
函数调用, 1 . 压入返回地址 2. 转入目标函数
jump
通过修改 eip ,转入目标函数,进行调用
ret
恢复返回地址,压入 eip ,类似 pop eip 命令

必备知识

  1. 每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间。
  2. 这块空间的维护是使用了2个寄存器: esp ebp ebp 记录的是栈底的地址, esp 记录的是栈顶的地址。

第2点如图所示:

        3.函数栈帧的创建和销毁过程,在不同的编译器下创建和销毁是略有差异的,但是大体逻辑是相差不大的,当编译器越高级的时候,函数栈帧的封装越不容易看,所以编译器的环境采用vs2013

演示代码:

#include <stdio.h>
int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}
int main()
{
    int a = 3;
    int b = 5;
    int ret = 0;
    ret = Add(a, b);
    printf("%d\n", ret);
    return 0;
}

大体思路:

        每一个函数调用,都要在栈区创建一个空间

        由于栈区使用内存的时候,每一次函数调用都要在栈区上分配空间,是先使用高地址,再使用低地址

打开调试窗口,接着打开调用堆栈

从调用堆栈看到,原来main函数也被调用了,那么它是被谁调用呢?

 在VS2013中,main函数也是被其他函数调用的,调用逻辑如下:

反汇编代码:

右击鼠标,打开反汇编 

int main()
{
//函数栈帧的创建
00BE1820 push ebp
00BE1821 mov ebp,esp
00BE1823 sub esp,0E4h
00BE1829 push ebx
00BE182A push esi
00BE182B push edi
00BE182C lea edi,[ebp-24h]
00BE182F mov ecx,9
00BE1834 mov eax,0CCCCCCCCh
00BE1839 rep stos dword ptr es:[edi]
//main函数中的核心代码
    int a = 3;
00BE183B mov dword ptr [ebp-8],3
    int b = 5;
00BE1842 mov dword ptr [ebp-14h],5
    int ret = 0;
00BE1849 mov dword ptr [ebp-20h],0
    ret = Add(a, b);
00BE1850 mov eax,dword ptr [ebp-14h]
00BE1853 push eax
00BE1854 mov ecx,dword ptr [ebp-8]
00BE1857 push ecx
00BE1858 call 00BE10B4
00BE185D add esp,8
00BE1860 mov dword ptr [ebp-20h],eax
    printf("%d\n", ret);
00BE1863 mov eax,dword ptr [ebp-20h]
00BE1866 push eax
00BE1867 push 0BE7B30h
00BE186C call 00BE10D2
00BE1871 add esp,8
    return 0;
00BE1874 xor eax,eax
}

1. _tmainCRTStartup函数栈帧的创建(调用main函数的函数)

  我们已经知道了,main函数也是被调用的,画出函数栈帧图详解一波:

        栈空间的使用是,由高地址到低地址,而main函数是被_tmainCRTStartup的,所以esp与ebp就维护当前的栈帧

        ①执行push操作

        这时候F10按一下, 执行一下push让ebp这个地址压栈

        怎么证明ebp压栈成功?

        所以说,esp这个栈顶指针指向了ebp这个压栈的值:

        ②接下来执行mov指令,就是把esp的值赋值给ebp

如下图: 

        ③然后执行sub指令,让esp减去0E4h,换成二进制就是228,,整体流程下图:

        当①②执行完后,其实_tmainCRTStartup栈帧的空间已经开辟完毕,当③执行完后,调用了main函数,此时esp、ebp就预开辟好了一块空间给main函数,并维护该栈帧,如下图

2.函数栈帧的创建

        接着上文的内容,画出该图:

        接着依次push三个寄存器ebx,esi,edi的值入栈中,esp往低地址处移动

通过监视可以看一看

画出图如下: 

接下来看这四条指令:

①lea edi, [ebp+FFFFFF1Ch]

解析:

[ebp+FFFFFF1Ch]显示符号名去掉,也就是[ebp-0E4h] (也就是和[esp - OE4h]是同一个位置)

lea - 加载有效地址,即将[ebp-0E4h]的地址加载到edi寄存器中,[ebp-0E4h] - 指向ebp(基准指针寄存器)上减去0E4h(232)个字节位置的内存单元

②mov ecx,39h(准确的次数)

解析:

将立即数 39h 复制到 ecx 寄存器中,使 ecx 寄存器的内容变为 39h(十进制的57)。

③mov  eax,0cccccccCh

解析:

这条指令将立即数 0cccccccCh 复制到 eax 寄存器中,使 eax 寄存器的内容变为 0cccccccCh

 ④rep stos dword ptr es : [edi]

解析: 

  • rep 是重复前缀,用于指示指令要重复执行多次,执行的次数由 ecx 寄存器中的计数值决定。
  •  stos 是字符串存储 (Store String) 的缩写,用于将数据存储到字符串中。
  • dword ptr 指明操作数的大小为双字(32位),用于指示要存储的数据的大小。
  • es:[edi] 是目标操作数,表示将数据存储到以 es 寄存器为段地址,edi 寄存器为偏移地址的内存位置。

        第④点整体来看:该指令的作用是将 eax 寄存器中的值重复写入到以 es:[edi] 为起始地址的内存位置。执行次数由 ecx 寄存器中的计数值确定。

       

        整体①②③④来看:

        要把edi这个位置开始(也就是[ebp-0E4h]的地址),向下空间的ecx(次数)放的39h这个值,这么多个dword(4个字节)的数据全部都改成0CCCCCCCCh,图解在下面:

         到这,main函数的开辟已经执行完了。

3.main函数中的核心代码

接下来执行以下三句代码:

以a为例子,观察下图:

        可得出以下图解:

然后接下来执行以下指令:

首先来看前两条指令: 

  • mov 是一个指令,用于将数据从一个位置复制到另一个位置。
  • eax 是一个32位的寄存器,用于存储通用数据。
  • dword ptr 是一个修饰符,用于指示后面的操作数应该被视为32位的双字(即4个字节)。
  • [ebp-14h] 是一个内存引用,它指向位于基址指针 ebp 减去 14h(20个字节)的位置。基址指针 ebp 是一个用于存储局部变量和函数参数的寄存器。

综上所述,这行代码的作用是将位于 ebp-14h 地址处的32位数据加载到 eax 寄存器中

  • push 是一个指令,用于将数据压入堆栈中。
  • eax 是之前加载了数据的寄存器。

综上所述,这行代码的作用是将 eax 寄存器中的值(20)压入堆栈中

所以,后两条指令同理③④

        将位于 ebp-8 地址处的32位数据加载到 ecx 寄存器中,将 ecx 寄存器中的值(10)压入堆栈中

图解:

🧨call指令🚩

函数调用过程

  • call 是一个指令,用于调用一个函数或子程序。它的作用是将当前指令的下一条指令的地址(返回地址)压入堆栈,并跳转到指定的函数或子程序的地址执行。

按f11,通过call指令就会进入Add函数里面去了(并未真正进入,还要再按一次f11)

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

因此,call  的作用是将当前指令的下一条指令的地址压入堆栈(00C21450),并跳转到地址为 00C210E1 的函数或子程序的入口点执行

4.Add函数栈帧的创建

        再按下f11,这时候才是真正来到add函数内,前面那一堆汇编代码跟main函数栈帧创建逻辑是一样的。

反汇编代码:

前提说明 

图解: 

5.Add函数中的核心代码🎯

反汇编代码:

①:将值0(初始化z)存储到位于内存中的地址 ebp-8 处的双字(32位)数据中。

②将位于内存中地址 ebp+8 处的双字(32位)数据(当前位置的值为10)加载到寄存器 eax 中。

③将位于内存中地址 ebp+0Ch 处的双字(32位)数据(当前位置的值为20)与寄存器 eax 中的值相加,并将结果存储(两数相加的结果为30)回 eax 寄存器中。

④位于当前堆栈帧中相对于基址寄存器 ebp 偏移 8 字节的内存位置的值(当前值为30)复制到寄存器 eax 中

图解:

6.Add函数栈帧的销毁 

代码:

 这句代码的意思是: 

        将位于 ebp-8 地址处的32位数据(值为30)加载到寄存器 eax 中,因为函数出去之后,值就销毁了,但是如果放在寄存器eax内就安全了,相当于用了一个全局的寄存器把返回值保存起来,回到主函数main再用。

 然后pop三次,把三个寄存器的地址分别弹出:

接着 mov esp,ebp,就是把ebp当前地址赋值给esp:

接着pop ebp,此时ebp回到main函数函数栈帧的栈底:

        说明此时Add函数已经销毁了。

此时最重要的一条指令来了:

        当pop ebp之后,只是让我们找到了esp和ebp的栈帧空间,但是当我回到main函数的时候,还应该从call指令的下一调指令的地址开始执行,所以此时恰好栈顶上就放着这个地址

        这个ret指令return返回的时候这个指令其实就是从栈顶弹出了call指令下一条指令的地址,然后跳那去了,接着F10走一下,回来main函数内:

        存这个地址(00C21450)就是当函数调用完之后还能回来,从call指令的下一条指令的地址开始执行。

 所以图解是这样的:

关于形参变量空间的释放:

 返回值是怎么带回来:先把值委托到eax寄存器内,接着回到main函数内部赋值

        经过esp+8之后,关于x和y两个形参空间的变量就已经销毁,还给操作系统了。

 关于main函数的销毁跟上述Add函数的销毁逻辑相似,也不累赘地列举了。

总结:

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

        首先为main函数分配栈帧空间,然后在栈帧空间内初始化一部分空间之后,给局部变量在该栈帧空间内分配一点空间

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

        因为随机值是我们放进去的,如果局部变量给它们初始化,那就是把随机值覆盖了。

3.函数调用时参数时如何传递的?传参的顺序是怎样的?

        当我要调用那个函数的的时候,就已经push,push,把这两个参数从右向左开始压栈压进去,当我们进入形参函数Add的时候通过指针的偏移量找回来找到了形参

4.形参和实参的关系是什么呢?

        形参确实是我在压栈的时候开辟的空间,形参和实参只是值是相同的,空间是独立的,所以形参是实参的一份临时拷贝,改变形参不会影响实参

5.函数调用结束后是如何返回的?

        我们在调用之前就已经把call指令的下一条指令的地址压栈压进去了,当函数调用完要返回的时候,弹出ebp就能找到原始上一个函数调用的ebp,然后指针往下走的时候就能中找到esp的地址,接着跳转到call指令下一条指令的地址,返回值是通过寄存器的方式带回来的

        本文结束,感谢来访! 

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

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

相关文章

抖音商城双11好物节,从供需两侧重新定义“好货”

【潮汐商业评论/原创】 你用的第一款护肤品是什么&#xff1f; 大部分人回忆起童年的时候&#xff0c;想起来的都是那款有着牛奶香味的、塑料包装的小袋白色乳霜——郁美净儿童霜。 但是不知何时&#xff0c;它逐渐淡出了很多人、特别是年轻人的视野&#xff0c;直到今年在互…

iManager云套件支持配置kingbase

作者 yangjunlin 前言 越来越多的涉密单位对于信创环境的要求逐渐升高&#xff0c;服务应用对国产数据库的依赖性也在提高&#xff0c;针对超图iManager for k8s产品中的开源数据库替换为kingbase等国产化数据库的客户需求和场景也就随之而来&#xff0c;因此本文将带着读者一步…

ChatGPT微信小程序系统源码/开源支持二开/AI聊天微信小程序源码/人工智能ChatGPT实现的微信小程序

源码简介&#xff1a; 关键字&#xff1a;人工智能 ChatGPT 二开ChatGPT微信小程序源码&#xff0c;作为AI聊天微信小程序源码&#xff0c;它是人工智能ChatGPT实现的微信小程序。它可以适配H5和WEB端 支持AI聊天次数限制。 ChatGPT-MP(基于ChatGPT实现的微信小程序&#xf…

广东食养食疗国际研讨会成功举行

经商务部批准的第20届中国国际保健博览会11月11日在广州隆重开幕。广东省养生文化协会召开的食养食疗国际研讨会首次亮相展会&#xff0c;备受大众关注。来自20多个国家地区的代表&#xff0c;通过线下线上、现场演讲、书面交流等不同形式参加本次活动。30多个商协会负责人和近…

后门程序2

System\CurrentControlSet\Services\Disk\Enum Windows 操作系统注册表中的一个路径。这个路径通常包含有关磁盘设备的信息。在这个特定的路径下&#xff0c;可能存储了有关磁盘枚举的配置和参数 Enum&#xff08;枚举&#xff09;子键通常包含了系统对磁盘的枚举信息&#xf…

Python实现WOA智能鲸鱼优化算法优化循环神经网络回归模型(LSTM回归算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 鲸鱼优化算法 (whale optimization algorithm,WOA)是 2016 年由澳大利亚格里菲斯大学的Mirjalili 等提…

Spring的Redis客户端

如何在Spring中操作redis 在创建springboot项目的时候引入redis的依赖. 在配置文件里指定redis主机的地址和端口,此处我们配置了ssh隧道,所以连接的就是本机的8888端口. 创建一个controller类,注入操作redis的对象. 前面使用jedis,是通过jedis对象里的各种方法来操作redis的,此…

[原创]仅需小小的改变,B++ Builder 12的代码完成提示即可完美工作.

[简介] 常用网名: 猪头三 出生日期: 1981.XX.XXQQ: 643439947 个人网站: 80x86汇编小站 编程生涯: 2001年~至今[共22年] 职业生涯: 20年 开发语言: C/C、80x86ASM、PHP、Perl、Objective-C、Object Pascal、C#、Python 开发工具: Visual Studio、Delphi、XCode、Eclipse、C Bui…

实验室试剂耗材安全管理:从热点事件看其重要性

随着科学技术的不断发展&#xff0c;实验室试剂耗材在各个学科领域的应用越来越广泛。然而&#xff0c;随之而来的实验室试剂耗材安全管理问题也日益凸显。近年来&#xff0c;一系列实验室安全事件引发了社会广泛关注&#xff0c;使我们深刻认识到实验室试剂耗材安全管理的重要…

HslCommunication模拟西门子读写数据

导入HslCommunication C#端代码&#xff08;上位机&#xff09; 这里要注意的是上位机IP用的当前电脑的IP。 using HslCommunication; using HslCommunication.Profinet.Siemens; using System; using System.Collections.Generic; using System.ComponentModel; using Syste…

Ridgeline plot / 远山图 / 山脊图 怎么画?怎么优化?

工具 Origin 2022 当然&#xff0c;用Matlab、Python也是可以的。 颜色配置 色卡调整

ehcart散点图x轴不按照顺序排列的问题

如图所示&#xff0c;一开始我x轴用的type为category&#xff0c;所以导致x轴的顺序是乱的&#xff0c;如下所示&#xff1a; 后来去官网看了下文档&#xff0c;才知道只需要type改成value就可以了&#xff01;&#xff01;&#xff01;差点就去写for循环排序了呀

正交矩阵的定义

对于n阶矩阵A&#xff0c;如果&#xff0c;其中为单位矩阵&#xff0c;为A的转置矩阵&#xff0c;那么就称A为正交矩阵。 对于正交矩阵&#xff0c; 对于正交矩阵&#xff0c;其列向量都是单位向量&#xff0c;行向量都是单位向量

Databend 开源周报第 119 期

Databend 是一款现代云数仓。专为弹性和高效设计&#xff0c;为您的大规模分析需求保驾护航。自由且开源。即刻体验云服务&#xff1a;https://app.databend.cn 。 Whats On In Databend 探索 Databend 本周新进展&#xff0c;遇到更贴近你心意的 Databend 。 用户案例&#…

ENVI IDL:如何生成FY4A快照

01 数据说明 FY4A全圆盘&#xff08;DISK&#xff0c;全球&#xff09;多光谱影像&#xff0c;panoply软件打开数据层次结构如下&#xff1a; 我们生成快照主要使用到其中的NOMChannel01、NOMChannel02、NOMChannel03进行快照显示&#xff0c;注意我并没有进行辐射定标。 02 生…

《QT从基础到进阶·二十三》弹窗提示框QMessageBox和QCloseEvent事件

1、正常信息提示 QMessageBox::information(NULL, "Title", "Content", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);消息框按钮判断&#xff1a; if(QMessageBox::Ok QMessageBox::warning(this,"温馨提示","是否保存设置?…

【Android】TabLayout设置使用自定义的样式的图片显示问题

序言 TabLayout我们经常使用&#xff0c;用来和ViewPager2进行组合使用&#xff0c;做多Fragment切换页面效果。 TabLayout我们经常看到的的显示效果是上面文字&#xff0c;下面一个线段&#xff0c;在各大浏览器/新闻类APP可以看到&#xff0c;这个效果也是对TabLayout配置参…

2609. 最长平衡子字符串

2609. 最长平衡子字符串 难度: 简单 来源: 每日一题 2023.11.08 给你一个仅由 0 和 1 组成的二进制字符串 s 。 如果子字符串中 所有的 0 都在 1 之前 且其中 0 的数量等于 1 的数量&#xff0c;则认为 s 的这个子字符串是平衡子字符串。请注意&#xff0c;空子字符串也…

【数据结构】树与二叉树(十四):二叉树的基础操作:查找给定结点的父亲(算法Father )

文章目录 5.2.1 二叉树二叉树性质引理5.1&#xff1a;二叉树中层数为i的结点至多有 2 i 2^i 2i个&#xff0c;其中 i ≥ 0 i \geq 0 i≥0。引理5.2&#xff1a;高度为k的二叉树中至多有 2 k 1 − 1 2^{k1}-1 2k1−1个结点&#xff0c;其中 k ≥ 0 k \geq 0 k≥0。引理5.3&…

C# Spire.Pdf将PDF文件转换为Word文件

一.开发框架&#xff1a; .NetCore6.0 工具&#xff1a;Visual Studio 2022 二.思路&#xff1a; 1.界面上传PDF文件&#xff0c;并保存 2.PDF文件转换为Word文件并保存 3.使用SHA256Hash判断文件是否已经转换过了&#xff0c;转换过了的话&#xff0c;就返回原先转换过的文…