函数栈帧的创建和销毁——“C”
创始人
2024-05-24 21:13:25
0

各位CSDN的uu们你们好呀,今天小雅兰来为大家介绍一个知识点——函数栈帧的创建和销毁。其实这个知识点,我们很早之前就要讲,但是因为我的一系列原因,才一直拖到了现在,那么,话不多说,让我们一起进入函数栈帧的世界吧

我们学习了前面这么多内容,不由得会想起几个问题:

  • 局部变量是如何创建的?
  • 为什么局部变量不初始化内容是随机的?
  • 函数调用时参数是如何传递的?传参的顺序是怎样的?
  • 函数调用是怎么做的?
  • 函数的形参和实参分别是怎样实例化的?
  • 形参和实参的关系是什么?
  • 函数的返回值是如何带回的? 

带着这一肚子的疑惑,就有了今天的函数栈帧的创建和销毁了。


寄存器

什么是函数栈帧

什么是栈

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


首先,我还得给大家拓展一个知识点——寄存器

寄存器的功能是存储二进制代码,它是由具有存储功能的触发器组合起来构成的。一个触发器可以存储1位二进制代码,故存放n位二进制代码的寄存器,需用n个触发器来构成

按照功能的不同,可将寄存器分为基本寄存器移位寄存器两大类。

基本寄存器只能并行送入数据,也只能并行输出。

移位寄存器中的数据可以在移位脉冲作用下依次逐位右移或左移,数据既可以并行输入、并行输出,也可以串行输入、串行输出,还可以并行输入、串行输出,或串行输入、并行输出,十分灵活,用途也很广。

这边介绍一下寄存器的基本含义、基本概念、结构、工作原理、类型、存放代码满足条件、寄存器组织、寄存器寻址

 

 

 相关寄存器和汇编指令

 相关寄存器

 

  •  eax:通用寄存器,保留临时数据,常用于返回值
  •  ebx:通用寄存器,保留临时数据
  •  ebp:栈底寄存器
  •  esp:栈顶寄存器
  •  eip:指令寄存器,保存当前指令的下一条指令的地址

 

相关汇编命令

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

 什么是函数栈帧

我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。

那函数是如何调用的?

函数的返回值又是如何带会的?

函数参数是如何传递的?

这些问题都和函数栈帧有关系。

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

   

    函数参数和函数返回值

   

    临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)

   

    保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。

看到这里,我们就必须还想到一个问题——什么是栈? 


什么是栈

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

在经典的计算机科学中,栈被定义为一种特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO)。就像叠成一叠的术,先叠上去的书在最下面,因此要最后才能取出。

在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。

在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。 在我们常见的i386或者x86-64下,栈顶由成为 esp 的寄存器进行定位的。

在了解了这些准备工作之后,我们就可以进入我们的正题啦——解析函数栈帧的创建和销毁 


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

首先我们达成一些预备知识才能有效的帮助我们理解,函数栈帧的创建和销毁。

 1. 每一次函数调用,都要为本次函数调用开辟空间,就是函数栈帧的空间。

 2. 这块空间的维护是使用了2个寄存器: esp 和 ebp ebp 记录的是栈底的地址, esp 记录的是栈顶的地址。

 3. 函数栈帧的创建和销毁过程,在不同的编译器上实现的方法大同小异,本次演示以VS2010为例。  

函数的调用堆栈

#include
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函数也确实被调用了

在VS2010中,main函数也是被其他函数调用的   __tmainCRTStartup  这个函数又是被调用的  mainCRTStartup

即,mainCRTStartup调用了__tmainCRTStartup,__tmainCRTStartup又调用了main函数

 

 现在转到我们的反汇编,把这个显示符号名的勾勾去掉,这样方便观察

 

 

 压栈(push)操作

 mov操作,表示把esp的值给ebp

 sub操作,表示esp的值减去0E4h

0E4h是一个十六进制数字,转为十进制为228

经过sub操作,esp的值就变了

 

 然后,esp就指向上面开辟的某一块空间了

 

 这一块空间,就是为我们的main函数预开辟的一块空间了,也就是main函数的栈帧

然后再是三个push操作,push了ebx、esi、edi

 

 

 再是lea操作,lea表示Load Effective Address,是为加载有效地址

 把[ebp+FFFFFF1Ch]的值加载到edi中,但是这个值不好观察,那我们还得把我们之前取消的显示符号名给勾上

 

 

这三个操作的意思是,把刚刚main函数的栈帧全部初始化为CCCCCCCC

dword的意思是double word(双字),一个字是两个字节,双字就是四个字节

 

 走了这么半天,竟然还没有执行一行有效的代码!!!

#include
int Add(int x, int y)
{int z = 0;z = x + y;return z;
}
int main()
{int a = 10;int b = 20;int ret = 0;ret = Add(a, b);printf("%d\n", ret);return 0;
}

 

 这就是我们的变量为什么要初始化的原因,如果不初始化的话,内存里面放的是一个随机值

 

接下来,就是调用函数

 

 

这几个动作就是在传参

 

 我们会发现,这个Add函数的指令和我们的main函数开始的指令几乎是一样的,这实际上就是在准备栈帧

 

 

其实初始化并不止这么多次,把33h这个十六进制数字换成十进制,是多少次就初始化多少次CCCCCCCC

 

 

通过画图,我们可以清楚地知道,并没有给形参创建空间,这也验证了我们之前的结论:实参传递给形参的时候,形参是实参的一份临时拷贝,改变形参是不会影响实参的

把[ebp-8]的值放到eax这个寄存器中

 

 

 

 


好啦,小雅兰今天的函数栈帧的创建和销毁的内容就到这里了,总体来说,我觉得这个内容比较地抽象,难度也是很大的,对于我们这种初学者来说,但是,不奢求一遍就把它看懂,但求每多看一遍,收获的知识点就多一点点,这样我就心满意足啦!!!

 

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...