函数栈帧的销毁和建立

目录

前言

一、什么是函数栈帧?

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

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

3.1 什么是栈

3.1.2 部分寄存器以及相关指令介绍 

3.1.3  压栈(push)与出栈(pop)

3.2 案例分析

3.2.1 函数栈帧的创建

1.为main函数创建栈帧:

2.执行main函数内部代码 

3.为add函数创建栈帧 

4.执行add函数内部代码 

 5.add函数栈帧出栈

 6.返回main函数栈帧

四、解答前面的问题

 

总结


前言

在上一篇介绍的递归文章中,提到了栈溢出,那么什么是函数栈帧,它有什么用,又该如何创建和销毁呢?下面让我们一起来揭开它的面纱。


一、什么是函数栈帧?

        我们在写C语言代码的时候,经常会把一个独立的功能抽象为函数,所以C程序是以函数为基本单位的。那函数是如何调用的?函数的返回值又是如何待会的?函数参数是如何传递的?这些问题都和函数栈帧有关系。

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

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

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

         通过理解函数栈帧,能够较为清晰的回答以下问题:

  1. 局部变量是怎么创建的?
  2. 为什么局部变量不初始化,它的值是随机值?
  3. 函数是怎么传参的?传参的顺序是怎样的?
  4. 形参和实参是什么关系?
  5. 函数调用的具体过程是怎么样的?
  6. 函数调用结束后返回值是如何返回的?

        接下来让我们来学习函数栈帧如何创建和销毁的。

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

3.1 什么是栈

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

在经典的计算机科学中:

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

在计算机系统中:

栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。 在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。 在我们常见的i386或者x86-64下,栈顶由名为 esp 的寄存器进行定位。

3.1.2 部分寄存器以及相关指令介绍 

寄存器: 

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

相关指令:

  • push----PUSH 指令首先减少 ESP 的值,再将源操作数复制到堆栈。操作数是 16 位的,则 ESP 减 2,操作数是 32 位的,则 ESP 减 4(将对象进行压栈);
  • mov----MOV 指令将源操作数复制到目的操作数;
  • sub----两个操作数的相减,即从A中减去B,其结果放在A中;
  • lea----LEA指令将存储器操作数mem的4位16进制偏移地址送到指定的寄存器;
  • pop----将操作对象弹出栈帧,出栈
  • call----调用函数,调用前会将call下一条语句的地址压栈在栈顶
  • ret----将栈顶的地址弹出并返回到该地址的地方

3.1.3  压栈(push)与出栈(pop)

        每一个函数在调用的时候都在栈区上开辟一块内存空间:

 

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

3.2 案例分析

下面通过一个加法程序进行分析:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int add(int x, int y) {
	int z = 0;
	z = x + y;
	return z;
}

int main() {
	int a = 5;
	int b = 4;
	int c = 0;
	c = add(a, b);
	printf("%d", c);
}

 在课上经常听到老师将,main是一个程序的入口,那么它真的是第一个函数满,会不会在main前面也有其他函数去调用main呢?下面我们了看看:

先进入调试,打开堆栈窗口


 观察堆栈窗口可以发现main函数也是被其他函数调用的

那么是谁调用了main函数呢?

  观察上面两图发现,每当有一个新的函数被调用的时候,在内存中就会在上一个函数的顶上创建新的函数栈帧,这个就叫压栈;当函数内代码执行结束时,函数栈帧就会重新归还内存,这叫出栈。

        如果main函数之前的函数栈帧没有显示,则

3.2.1 函数栈帧的创建

  转入反汇编

1.为main函数创建栈帧:

 

2.执行main函数内部代码 

3.为add函数创建栈帧 

 

call代码是调用函数的意思,但在调用前call会把它下一句语句的地址进行压栈,这样我们到时候出栈就能返回到上一个函数栈帧的原地址 ; 

继续下一步,进入add函数栈帧的创建,此处与main函数的函数栈帧创建相同:

        通过上面两个函数栈帧的创建,可以观察他们的创建方式是一样的:先将ebp(栈底指针)压栈(push),然后将esp(栈顶指针)的地址给esp(可以认为ebp与esp指向同一个地址,并列),接着esp(栈顶指针)向低地址移动一定位置,在将几个寄存器压栈来存变量的数值,最后再将ebp~esp这段函数栈帧赋初值(初值为随机数),这样一个函数栈帧就创建好了。

4.执行add函数内部代码 

 5.add函数栈帧出栈

ret指令会将我们之前存放的call下一条语句地址从栈顶弹出,然后返回到call下一条语句的地址;

 6.返回main函数栈帧

        后面的main函数栈帧销毁可以对比add函数栈帧销毁,这里放图,不作分析。

 

四、解答前面的问题

 

1.局部变量是怎么创建的?

        函数栈帧创建后编译器分配由高到低地址创建变量;

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

        函数栈帧创建后会默认将所有内容初始化为0cccccccch;

3.函数是怎么传参的?传参的顺序是怎么样的?

        传参是将实参值拷贝后进行压栈在栈顶,顺序是由右到左;

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

        形参是实参的临时拷贝,只是值相同却是不同的地址;

5.函数调用是怎么做的?

        利用call指令;

6.函数调用结束后怎么返回的?

        利用ret指令;

总结


以上就是今天要讲的内容,本文仅仅简单介绍了什么是函数栈帧,它有什么用,又该如何创建和销毁的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值