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

本章的内容:

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

本文放到 --> 该专栏内: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.mfbz.cn/a/138576.html

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

相关文章

计数排序及优化

&#x1f389;个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名乐于分享在学习道路上收获的大二在校生 &#x1f43b;‍❄个人主页&#x1f389;&#xff1a;GOTXX&#x1f43c;个人WeChat&#xff1a;ILXOXVJE&#x1f43c;本文由GOTXX原创&#xff0c;首发CSDN&a…

汽车一键启动智能系统功能作用

在现代科技的推动下&#xff0c;我们的生活每天都在发生着变化。其中&#xff0c;汽车智能一键启动系统就是科技改变生活的最好例子之一。 首先&#xff0c;我们来简单了解一下汽车智能一键启动系统。它是一种利用先进的电子技术和无线通信技术&#xff0c;实现无需钥匙即可启…

基于单片机智能输液器监控系统的设计

**单片机设计介绍&#xff0c; 基于单片机智能输液器监控系统的设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的智能输液器监控系统可以实现对输液过程的实时监测和控制&#xff0c;以下是一个基本的设计介绍&am…

【数据结构——队列的实现(单链表)】

数据结构——队列的实现&#xff08;单链表&#xff09; 一.队列1.1队列的概念及结构 二.队列的实现2.1 头文件的实现——&#xff08;Queue.h&#xff09;2.2 源文件的实现—— &#xff08;Queue.c&#xff09;2.3 源文件的实现—— &#xff08;test.c&#xff09; 三.队列的…

拼多多API接口,打造智能化电商平台

近年来&#xff0c;电商行业的崛起给人们的购物带来了极大的方便。随着电商行业的发展&#xff0c;拼多多作为新兴电商平台已经成为市场焦点。 同时&#xff0c;随着技术的发展&#xff0c;API&#xff08;Application Programming Interface&#xff0c;应用程序编程接口&…

pta 6翻了 Python3

“666”是一种网络用语&#xff0c;大概是表示某人很厉害、我们很佩服的意思。最近又衍生出另一个数字“9”&#xff0c;意思是“6翻了”&#xff0c;实在太厉害的意思。如果你以为这就是厉害的最高境界&#xff0c;那就错啦 —— 目前的最高境界是数字“27”&#xff0c;因为这…

云课五分钟的一些想法

起源 自中学起&#xff0c;就积极学习和掌握互联网相关知识&#xff0c;到如今已经快30年了。 个人也全程经历了从信息时代的互联网&#xff08;硬&#xff09;到智能时代的大模型&#xff08;软&#xff09;。 整体信息到智能的基础设施&#xff0c;由硬到软&#xff0c;机…

CRM系统:除了销售管理,还能做些什么?

企业的健康发展&#xff0c;离不开业绩的提升。在企业数字化转型的背景下&#xff0c;采用数字化应用进行管理已成为共识。许多企业认识到了应该使用CRM客户管理系统来进行销售管理&#xff0c;但CRM能做的还有很多。下面说说除了销售管理&#xff0c;CRM系统还能做些什么&…

继承和多态_Java零基础手把手保姆级教程(超详细)

文章目录 Java零基础手把手保姆级教程_继承和多态&#xff08;超详细&#xff09;1. 继承1.1 继承的实现&#xff08;掌握&#xff09;1.2 继承的好处和弊端&#xff08;理解&#xff09; 2. 继承中的成员访问特点2.1 继承中变量的访问特点&#xff08;掌握&#xff09;2.2 sup…

石英增强光声光谱气体传感技术中的高精密压力控制解决方案

摘要&#xff1a;光声池内气体压力的可调节控制以及稳定性是保证光声法高精度测量的关键&#xff0c;但在目前的光声和光谱研究中&#xff0c;对气体样品池内压力控制技术的报道极为简单&#xff0c;甚至很多都是错误的&#xff0c;根本无法实现高精度调节和控制&#xff0c;为…

Autosar模块介绍:Memory_3(MemIf-内存接口抽象)

上一篇 | 返回主目录 | 下一篇 Autosar模块介绍&#xff1a;Memory_3(MemIf-内存接口抽象 1 基本术语解释2 MemIf组成结构图 1 基本术语解释 编号缩写原文解释1(Logical) Block——可单独寻址的连续内存区域&#xff08;即&#xff0c;用于读、写、擦除、比较等操作&#xff…

眼科动态图像处理系统使用说明(2023-8-11 ccc)

眼科动态图像处理系统使用说明 2023-8-11 ccc 动态眼科图像捕捉存贮分析与传输系统&#xff0c;是由计算机软件工程师和医学专家组结合&#xff0c;为满足医院临床工作的需要&#xff0c;在2000年开发的专门用于各类眼科图像自动化分析、处理和传输的软件系统。该系统可以和各…

【NLP】大型语言模型,ALBERT — 用于自监督学习的 Lite BERT

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

基于正余弦算法优化概率神经网络PNN的分类预测 - 附代码

基于正余弦算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于正余弦算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于正余弦优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神经网络…

【Linux】第十五站:环境变量

文章目录 一、进程相关的一些概念1.一些常见的概念2.对于并发3.**进程切换** 二、环境变量1.PATH环境变量2.HOME环境变量3.SHELL环境变量4.env5.系统调用接口与环境变量6.什么是环境变量&#xff1f;7.命令行参数8.main函数的第三个命令行参数9.如何验证环境变量是可以被继承的…

2、工厂模式的实现

工厂模式概念 工厂模式是一种常用的设计模式&#xff0c;它主要用于实例化对象。这种模式的主要思想是在不暴露具体的实现细节的情况下&#xff0c;让客户端能够创建具有特定接口的对象。它可以让我们在运行时决定实例化哪个类。 在C语言中&#xff0c;实例化对象意味着创建一…

使用LLM-Tuning实现百川和清华ChatGLM的Lora微调

LLM-Tuning项目源码&#xff1a; GitHub - beyondguo/LLM-Tuning: Tuning LLMs with no tears&#x1f4a6;, sharing LLM-tools with love❤️.Tuning LLMs with no tears&#x1f4a6;, sharing LLM-tools with love❤️. - GitHub - beyondguo/LLM-Tuning: Tuning LLMs wit…

eNSP毕业设计系列-《大型企业网》-BGP网络无nat

客户主要需求&#xff1a;需要有三栋楼&#xff0c;每栋楼有三个业务。 又新增了要求&#xff0c;要双核心、双防火墙。 所以我根据客户的需求&#xff0c;完成了如下组网设计。 主要技术&#xff0c;MSTPVRRP链路聚合OSPF传统纵网&#xff0c;&#xff08;万金油组合&#…

企业计算机中了eking勒索病毒如何解毒,eking勒索病毒文件恢复

网络技术的不断发展&#xff0c;为企业的生产生活提供了极大便利&#xff0c;但随之而来的网络安全威胁也不断增加&#xff0c;近期&#xff0c;很多企业的计算机服务器遭到了eking勒索病毒攻击&#xff0c;导致企业的计算机服务器所有数据被加密&#xff0c;无法正常使用&…

Swagger3 GET请求,使用对象接收 Query 参数,注解怎么写?

简中互联网上就没一个靠谱的答案&#xff0c;最终翻到了 Github Issue 上才解决&#xff0c;真 TMD…… CSDN 就一坨 shit mountain 解决方案 原文&#xff1a;https://github.com/swagger-api/swagger-core/issues/4177 太长不看&#xff1a; 请求方法参数上加 ParameterObj…