首页 > 其他分享 >函数栈帧的创建和销毁

函数栈帧的创建和销毁

时间:2023-04-15 17:34:09浏览次数:40  
标签:销毁 函数 栈顶 add ebp main 栈帧

前言

C语言中,在函数被调用的时候,会在栈区为该函数创建一块空间,这块空间被称为函数栈帧,并且使用栈底指针gbp,栈顶指针gsp来维护这块空间。(gbp,gsp都是寄存器)

一、关于栈和栈区

1.1什么是栈

栈是数据结构中的一种只允许在一端插入和删除数据的存储结构(线性表),它遵循后进先出的原则。

其实,栈就类似一个去掉顶层的长方体盒子,当我们要把一本书放入盒子中,是不是只能由盒子的顶层放入到盒子底层。当我们要拿出一本书时,是不是只能从最后一本书开始,依次往外拿书,一直到下一本书是我们想要的。

函数栈帧的创建和销毁_栈和栈区

如图所示,是我们依次放入盒子的五本书,其中,book1是最先放入的,book5是最后放入的,当我们想要拿出book1时,无法直接拿出,只能先拿走当前盒子里最顶层的内本书籍,也就是book5,当拿出book5时,最顶层的书籍变成了book4,再依次拿出book4,book3,book2后才能拿到book1。这就是栈所说的后进先出的涵义所在。而放入书本,就是插入数据(压栈),拿出书本,就是删除数据(出栈)。

1.2栈顶和栈底

我们把插入和删除数据的内一端叫做栈顶,另一端叫做栈底。

那么我们插入和删除数据具体是在哪个位置呢?

函数栈帧的创建和销毁_函数栈_02

如上图,当栈非空时,栈顶总是指向栈顶元素的下一位置。

当栈为空的时候,栈底和栈顶指向同一位置,

如下图,

函数栈帧的创建和销毁_函数栈帧的创建和销毁_03


总结:栈底的位置是始终不变的,栈顶的位置是可以变化的,栈空时,栈顶和栈底指向位置相同,栈非空时,它指向的是栈当前栈顶元素的下一个位置(谁在最上面谁就是栈顶元素),同时栈顶一端也是插入和删除元素的一端。

1.3栈区

C语言内存四区之一,用来存储局部变量,以及在函数调用的时候,为该函数创建一块空间(函数栈帧)。

1.4栈区的使用规则

优先使用高地址处的空间。存储数据的规则和栈相同,且优先从高地址处存储数据(对应栈底)。

栈区须知:

栈区空间的创建和销毁由编译器控制,无需我们操心,我们直接使用即可。栈区。简单来说,我们就把栈区当作我们所理解的栈,这是完全没有问题的,栈底位置是高地址处,从栈底到栈顶,地址是逐渐递减的。

函数栈帧的创建和销毁_函数栈_04

二、函数栈帧

函数栈帧是:在栈区上开辟的、在调用函数时为该函数所创建的一块空间的统称

1.函数栈帧的维护

用两个存放地址的寄存器(esp,ebp)来维护对应函数栈帧所在的空间,其中ebp又被称为栈底指针,指向当前函数栈帧的栈底,esp又被称为栈顶指针,指向当前函数栈帧栈顶位置,随着数据的入栈和出栈会指向新的栈顶。他们的作用是用来维护函数栈帧,也就是因为函数调用在栈区创建的空间

2.需要了解的基本汇编指令

push:执行压栈操作,和栈的使用相同 ,压栈后栈顶指针会指向新的栈顶.例如push  a  语句表示将a的值压入栈中

mov:转移的意思,可以理解为是赋值。例如 mov  b,a语句表示将a的值转移给b

pop:出栈,同样会使栈顶指针指向新的栈顶。例如pop b语句,表示取出栈中存放的b的值。

sub:减法。例如:sub   a,b语句表示a=a-b(也就是给前面的值减去b)

add:加法。例如:add  a,b语句表示a=a+b(也就是给前面的值加上b)

call:调用目标函数,并且会将call指令的下一条语句的地址压入栈中

jmp:进入到目标函数内部

ret:回到进入函数前的下一条指令

3.实例剖析函数栈帧的创建

如下是一段简单的代码,我们将在VS2019的32位平台下,一步步剖析main和add函数栈帧的创建和销毁

#define _CRT_SECURE_NO_WARNINGS
#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 ", c);
  return 0;
}

在观察main的函数栈帧前,我们需要了解,main函数也是由别的函数所调用,我们将通过调用堆栈来观察是什么函数调用的main函数

如图所示:

按下F10,开始调试代码,鼠标右击,选择转到反汇编,我们就看到了main()函数的汇编代码

函数栈帧的创建和销毁_入栈_05

函数栈帧的创建和销毁_函数栈_06

在汇编代码一直按F10,来到ret处,按下F11,进入到调用mian函数的函数,如下图

F10调试来到ret处

函数栈帧的创建和销毁_函数栈_07

F11按下后,可以看到我们来到了invoke_main()函数,也就是调用main函数的函数

函数栈帧的创建和销毁_函数栈_08


调用main函数的关系链条并不止如此,但是我们是为了观察main函数的栈帧,因此,只需知道是谁调用的main函数即可(invoke_main()函数)

而当main函数被调用的时候,调用main函数的函数的函数栈帧已经被创建好,如下图所示


函数栈帧的创建和销毁_函数栈_09


接下来,我们将解读main函数的汇编代码

第一到三句:开辟main函数栈帧空间,并用esp,ebp维护

函数栈帧的创建和销毁_函数栈帧的创建和销毁_10


push ebp 语句解释:

当前虽然在main函数内,但并没有真正执行main函数的代码,其实这部分的操作是在为main函数的栈帧开辟做准备,push表示压栈操作,ebp是栈底指针,指向的是当前函数栈帧的底部(也就是指向了该函数在栈区创建的函数栈帧空间),这行语句表示的意思是将ebp的值(该函数函数栈帧空间的起始地址)压入到栈中,其实,压入ebp,其实就是保存调用main函数的函数的栈帧的地址,压入完成后,esp指向新的栈顶,如图

函数栈帧的创建和销毁_栈和栈区_11

move    ebp,esp 语句解释:

表示将esp(指向当前栈顶,存放的是地址)的值(地址)赋值给ebp,该语句会使栈底指针ebp指向当前栈顶指针指向的位置,如图

函数栈帧的创建和销毁_函数栈_12

函数栈帧的创建和销毁_入栈_13

sub   esp,0E4h  语句解释:

sub表示减的意思,整体的意思是esp=esp-0E4h,这会让esp指向一个新的地址,同时,在栈空间中,从下到上,地址是由高到低的,我们假设esp到了上方的某一个区域,从ebp到esp的空间也是为main函数开辟的栈帧空间,如图

函数栈帧的创建和销毁_函数栈_14

函数栈帧的创建和销毁_栈和栈区_15

第四到六句:压栈,保存调用main的函数的栈帧起始地址


函数栈帧的创建和销毁_栈和栈区_16

表示依次将ebx,esi,edi(我们简单理解为指针,可以存储地址)压入栈中,同时esp会指向新的栈顶,如图

函数栈帧的创建和销毁_main函数_17

函数栈帧的创建和销毁_函数栈帧的创建和销毁_18

第七到十句:初始化main函数栈帧区域


函数栈帧的创建和销毁_函数栈帧的创建和销毁_19

lea是load effective address的缩写,即加载有效地址,将ebp-24h的地址赋值给edi,

函数栈帧的创建和销毁_函数栈_20

edi此时的地址是:0x008ffbb4

函数栈帧的创建和销毁_main函数_21

压入ebx,esi,edi前esp的地址是0x008ffbd8

edi的地址高于压入ebx,esi,edi前esp的地址,如图

函数栈帧的创建和销毁_入栈_22

mov       ecx,9      表示将9赋值给ecx,     

mov       eax,0CCCCCCCCh        表示将0CCCCCCCCh赋值给eax,    这三条语句加上rep...这条语句,表示将从edi位置开始向下的9个地址初始化为eax的值,我们可以在监视窗口,通过内存来看看是不是这样

函数栈帧的创建和销毁_main函数_23

确实如此,刚好到ebp时,并没有初始化

函数栈帧的创建和销毁_main函数_24

第十二到十二句:我们直接忽略,下图是勾选了显示符号名后的显示结果

函数栈帧的创建和销毁_栈和栈区_25

,第十三到十九句:将main函数内创建的局部变量放到对应的地址处,即main函数栈帧初始化区域处,在调用add函数后,首先进行add函数传参操作,参数从右到左依次压入栈中(eax,ecx保存的是变量b,a)

函数栈帧的创建和销毁_函数栈_26

函数栈帧的创建和销毁_函数栈帧的创建和销毁_27


第二十到二十一句子:在call指令处按F11调用add函数,再次按F11,通过jum指令进入add函数内部,并在栈中压入下一条指令的地址(008118F7)

函数栈帧的创建和销毁_入栈_28

函数栈帧的创建和销毁_main函数_29

如上图,当前栈顶是esp对应的0a000000

当call指令执行后,其上多了一个地址是f7188100,恰好是call指令下一条指令的地址,如下图

函数栈帧的创建和销毁_函数栈_30

在进入add函数后,我们会发现汇编代码和进入main函数时相差无几,都是在为add函数栈帧的开辟做准备,先是压入了main函数ebp的值,再预开辟add函数的栈帧空间,在对空间进行初始化后,将add函数内部创建的局部变量z放入初始化空间 中,通过ebp找到了在main函数传入的参数,并利用add指令计算了两个参数之和,最后保存到z变量的地址中去

关于add函数传参问题,在进入到add函数时,我们已经将两个实参进行拷贝(压入栈中),add函数在使用参数的时候,也是找到实参的拷贝当成自己的形参

函数返回值问题:函数在执行完成后,其中的局部变量会被立即销毁,那返回值是如何被带回的呢?其实,当返回值被计算出来的时候,会将返回值放入到一个全局的寄存器中,而寄存器不会随着函数的销毁而销毁 ,就这样借助寄存器带回了返回值。

4.函数栈帧的销毁(对应实例里的代码)

函数栈帧在函数调用完成后,会进行销毁,如下图所示的指令是add函数内销毁add函数栈帧的操作

函数栈帧的创建和销毁_函数栈_31

下图三条指令,会弹出add函数栈帧空间顶部的三个数据

函数栈帧的创建和销毁_函数栈帧的创建和销毁_32

如下指令通过把add函数的ebp传给esp,销毁add函数的栈帧空间,在通过弹出main函数的ebp,让add函数的ebp重新指向main函数的esp位置,并ret返回到指向main函数里call指令的下一条指令地址处(该指令地址在call指令执行后已经被压入栈)

函数栈帧的创建和销毁_main函数_33

如下图所示

函数栈帧的创建和销毁_函数栈帧的创建和销毁_34

函数栈帧的创建和销毁_函数栈帧的创建和销毁_35

再次来到main函数后,先通过add指令使地址增长销毁了形参的空间,并将add函数寄存器(eax)存储的返回值放入到main函数的c变量存储空间去,如下图分别是有符号名和无符号名时的代码

函数栈帧的创建和销毁_入栈_36

函数栈帧的创建和销毁_入栈_37

再利用和销毁add函数栈帧空间相同的方法,来销毁main函数的栈帧空间,我们会发现,在销毁栈帧空间时,我们在创建栈帧空间时保存的每个函数的ebp派上了大用场,它可以帮助我们销毁当前栈帧空间后,使得内层函数的esp,ebp(被销毁函数)能够指向外层函数的esp,ebp,借此一步步完成对每个调用函数栈帧的销毁。下图是main函数销毁栈帧的指令,与add函数销毁栈帧的指令逻辑完全相同。

函数栈帧的创建和销毁_函数栈帧的创建和销毁_38




标签:销毁,函数,栈顶,add,ebp,main,栈帧
From: https://blog.51cto.com/u_15466618/6192385

相关文章

  • C语言的fgets函数
    fgets是C语言中的一个标准库函数,用于从指定文件中读取一行字符串。它的声明如下:char*fgets(char*str,intn,FILE*stream);其中,str是一个字符数组,用来存储读取的字符串;n表示读取的最大字符数(包括换行符和终止符);stream表示要读取的文件流。fgets函数会从stream中读取字符,直到遇......
  • 查看oracle数据库中的函数
    SQLPLUS下:查看建了哪些函数,注意,引号内大写selectobject_namefromuser_objectswhereobject_type='FUNCTION';查看函数内容,引号内为你要查询的函数名,也要大写selecttextfromuser_sourcewherename='函数名';PLSQLDeveloper下查询用户下的函数:SELECT*FROMdba_objects......
  • 为什么要在函数的定义前加static?
    1.作用函数定义前加static的含义不是指存储方式,而是指对函数的作用域仅限于本文件2.用处使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名,因为同名也没有关系。3.根本原因根本原因是C语言中同一个工程中不能有同名函数。......
  • Delphi FDMemTable内存表用法及简单操作函数封装(转)
    在某些场景下当轻量级的应用需要在内存中缓存数量比较多且字段比较多的高频使用数据时。以前我都是采用Ini或直接使用sqlite数据库。JSON也试过基本无法或很难实现需要的功能,因为当涉及某一同类型对象多字段多列时不通过遍历基本无法直接取到或修改数据。这样就导致了效率的低下。......
  • python3正则-替换和切割函数
    1、介绍这里整理sub、subn和split三个函数的使用。2、sub函数sub(pattern,repl,string,count=0,flags=0)pattern,正则表达式repl,替换文本string,待处理字符串count,表示替换的最大次数。默认为0表示全部替换flags,标志,处理模式作用是在flags代表的模式下,匹配strings指......
  • python3正则-多匹配函数
    1、介绍这里介绍findall和finditer两个函数。2、findall函数findall(pattern,string,flags=0)pattern,正则表达式string,待处理字符串flags,标志,处理模式返回类型为list,如果不存在匹配,返回空列表[]。如果存在匹配,则返回全部匹配项,这里需要注意()的影响2.1无()importr......
  • C语言函数大全-- h 开头的函数
    C语言函数大全本篇介绍C语言函数大全--h开头的函数或宏1.hypot,hypotf,hypotl1.1函数说明函数声明函数功能doublehypot(doublex,doubley);计算直角三角形的斜边长(double)floathypotf(floatx,floaty);计算直角三角形的斜边长(float)longdoublehypot(lo......
  • python3正则-单匹配函数
    1、介绍re模块是python3用于处理正则的模块。这里介绍三个函数,re.match、re.fullmatch和re.search的使用。其都是如果匹配,则返回re.Match类对象,为初次匹配项。不匹配则返回None。且()不影响匹配结果,只是匹配过程中对需要匹配的描述。2、match函数match(pattern,string,fla......
  • Python 利用正则表达式和filter函数 筛选序列(列表等)
    在 Python 中,序列类型包括字符串、列表、元组、集合和字典http://c.biancheng.net/view/4312.htmlPython内建的filter()函数用于过滤序列https://www.liaoxuefeng.com/wiki/1016959663602400/1017404530360000序列内逐个元素筛选filter并用list保存结果筛选的判断条件是re.match......
  • python列表函数的基本使用
    一.列表简介序列是Python最常见的操作,是最经常使用的一种数据操作。列表是当前序列中使用最多的。序列中的每一个值对应的位置,称之为索引。通常情景下,第一个索引是位置为0,第二个索引位置为1...python中共有6个序列的内置类型,最常用的是列表和元组操作,其次是字典操作。Python中......