C | 函数栈帧的创建和销毁

简介: 对于函数的学习,我们可能有以下疑惑:- 局部变量是怎么创建的?- 为什么局部变量的值是随机的?- 函数是怎么传参的?传参的顺序是怎样的?- 形参和实参是什么关系?- 函数调用是怎么做的?- 函数调用结束后怎么返回的?==如果知道函数栈帧的创建和销毁就都会了,其实就是修炼了自己的内功,也能搞懂后期更多的知识==

在这里插入图片描述
啊我摔倒了..有没有人扶我起来学习....


@TOC


前言

对于函数的学习,我们可能有以下疑惑:
  • 局部变量是怎么创建的?
  • 为什么局部变量的值是随机的?
  • 函数是怎么传参的?传参的顺序是怎样的?
  • 形参和实参是什么关系?
  • 函数调用是怎么做的?
  • 函数调用结束后怎么返回的?

==如果知道函数栈帧的创建和销毁就都会了,其实就是修炼了自己的内功,也能搞懂后期更多的知识==


一、知识铺垫

  • 这次讲解博主使用的是VS2013,因为越高级的编译器实现函数栈帧的创建越复杂,越不容易学习和观察函数栈帧的创建和销毁;同时在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现
  • 引入2个寄存器:ebpesp,这2个寄存器中存放的是地址,这2个地址是用来维护函数栈帧的
  • 每一个函数调用,都要在栈区创建一个空间

二、函数栈帧的创建与销毁

1. 观察前的准备

先来一段简单的代码辅助我们去观察函数栈帧的创建
#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 = 10;
    int b = 20;
    int c = 0;

    c = Add(a, b);

    printf("%d\n", c);

    return 0;
}
  • main函数栈帧图例(栈区的使用习惯是先使用高地址再使用低地址)

在这里插入图片描述

2. 初步观察函数栈帧的创建与销毁

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

在这里插入图片描述

  • 可以看到在main函数中调用Add函数时,是由高地址往低地址层层开辟空间,往上堆砌,所谓的压栈,很形象

在这里插入图片描述
对应下图:
在这里插入图片描述

  • 当执行完Add函数之后程序又会重新回到main函数,此时Add函数的栈帧空间会被释放

在这里插入图片描述
对应下图:
在这里插入图片描述

  • 问题来了,那如果当main函数执行完之后,程序又该往哪里跑?换句话说,main函数又是被谁调用?

继续往下调试
在这里插入图片描述
对应下图:
在这里插入图片描述
可见在==VS2013==中,main函数被_tmainCRTStartup函数调用,而_tmainCRTStartup函数又被mainCRTStartup函数调用

3. 深入了解函数栈帧的创建与销毁——main函数栈帧的创建

这边我们利用 _tmainCRTStartup函数调用 main函数的过程来研究
  • 鼠标右键在随便一个空白处点击,转到反汇编页面

在这里插入图片描述
在这里插入图片描述

  • 为了便于观察地址的布局,我们先把==显示符号名==去掉

在这里插入图片描述

  • 接下来开始一步步解释反汇编中的各种指令的意思(可以打开内存窗口辅助证明,这边就不赘述了)
  1. 首先,我们现在是为了观察_tmainCRTStartup函数是如何调用main函数的,也就是说main函数目前还没被调用,栈区还没创建main函数的栈帧空间

    对应下图:

在这里插入图片描述

  1. push ebp表示用ebp压栈,压栈之后栈顶指针esp会自动指向栈顶

在这里插入图片描述
对应下图:
在这里插入图片描述

  1. mov ebp,esp表示把esp的值赋给ebp,此时ebpesp的指向一致

在这里插入图片描述

对应下图:
在这里插入图片描述

    • 这里首先,0E4h是十六进制数字(h表示十六进制),可以用监视窗口看看十进制值

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

- `sub  esp,0E4h`表示用`esp`减去`0E4h`,所以`esp`改变指向,此时`ebp`与`esp`所维护的蓝色区域就是为`main`函数预开辟的函数栈帧空间

在这里插入图片描述

对应下图:
在这里插入图片描述

  1. push这三个寄存器(先不用管是什么东西),esp再一次自动指向栈顶(后面亦是如此)

在这里插入图片描述

对应下图:
在这里插入图片描述

    • 先从监视窗口查看ebp-24h这个地址的值为0x00eafb44
    • 这四个步骤合起来可以理解为:,从ebp-24h这个地址开始,把一共9dword(dword表示为==double-word双字,占4个字节==)的空间都初始化为cccccccc

在里插入图片描述
这个cccccccc就是烫烫烫烫烫的来源。。,

  1. 继续继续,0Ah就是10,把10放进ebp-8ebp-8就是a的所在,==注意此时内存里的值是十六进制==

在这里插入图片描述
对应下图:
在这里插入图片描述

  1. 这两步同理可得,其中ebp-14hb的所在,ebp-20hc的所在

在这里插入图片描述
对应下图:
在这里插入图片描述

  • 到此我们就已经充分了解了,原来函数栈帧是==第1-6步==这样创建的,而创建了函数栈帧之后,函数内部的局部变量是==第7-8步==这样创建的且赋值前并不是0。而每个局部变量之间不是连续存放的,若越界访问就可能访问到cccccccc,打印出烫烫烫烫烫...

4. 深入了解函数栈帧的创建与销毁——Add函数栈帧的创建与销毁

  • 此时回顾一下,代码已经执行到这一步了

在这里插入图片描述
没想到这才执行了几步!!!下面要开始调用Add函数了,继续看看是怎么调用的吧~~

  1. 这四步相信铁汁们也很容易理解了,把ebp-14h的值传给eax,再将eax压栈;把ebp-8h的值传给ecx,再将ecx压栈,这四步其实就是在传参

在这里插入图片描述
对应下图:
在这里插入图片描述

  1. call指令表示调用,该指令会顺便把call指令的==下一条指令==的==地址==压栈,目的是为了调用完Add之后,还能找到回来的地址,所以先存起来下一条指令的地址,等调用完Add后再回来继续执行

在这里插入图片描述
对应下图:
在这里插入图片描述

  1. 这时,才真正调用Add函数!!上一步只是执行c = Add(a, b);的初步

在这里插入图片描述

  1. 看到这里,是不是觉得这些指令贼熟悉!我就不赘述啦,直接运行完方框内所有指令,得到z的值,==从中可以看出,Add函数栈帧内并没有给xy创建空间,而是直接调用刚才压栈的ecxeax的值(它俩分别存的是ab的值)==

在这里插入图片描述
对应下图:
在这里插入图片描述

  1. 接下来看看return z是如何实现的:把z的值传给eax之后,z就销毁了

在这里插入图片描述

  1. pop表示出栈,弹出ediesiebx

在这里插入图片描述
对应下图:
在这里插入图片描述

  1. ebp的值传给esp,此时espebp指向同一个方向

在这里插入图片描述
对应下图:
在这里插入图片描述

  1. 弹出ebp,此时ebp重新指向刚开始的地方,可以看出来,ebpebp又重新维护main的栈帧了,此时Add函数栈帧已经不属于我们了,就可以认为销毁了

在这里插入图片描述
对应下图:
在这里插入图片描述

  1. ret之后就可以重新回到main函数了,所以为什么==第2步==要存地址了

在这里插入图片描述

  1. add esp,8表示 esp + 8,那么esp指向再次改变

在这里插入图片描述
对应下图:
在这里插入图片描述

  1. eax的值传给ebp - 20h这个地方,其实就是c

在这里插入图片描述
对应下图:
在这里插入图片描述

三、回顾问题

面对这些问题,铁汁们已经有自己的答案了叭~
  • 局部变量是怎么创建的?
  • 为什么局部变量的值是随机的?
  • 函数是怎么传参的?传参的顺序是怎样的?
  • 形参和实参是什么关系?
  • 函数调用是怎么做的?
  • 函数调用结束后怎么返回的?
  1. 简单来说,每次调用函数之前,都要通过espebp创建一个栈帧空间,并初始化为cccccccc(不同编译器不尽相同),再在不连续的空间内创建局部变量
  2. 主函数按值传参的时候把实参的值压到栈顶(此时压栈的就是形参),并把==主函数调用被调函数之后要执行的下一步指令的地址==压到栈顶存着,再来创建被调用函数的函数栈帧,可以发现形参是实参的一份临时拷贝,而且并不在被调用函数的栈帧内,而返回值先存在寄存器内,等被调函数销毁后,传给接收返回值的变量

在这里插入图片描述

相关文章
|
5月前
|
编译器
函数栈帧的创建和销毁
函数栈帧的创建和销毁
30 0
|
6月前
|
存储 编译器 容器
函数栈帧的创建和销毁讲解
函数栈帧的创建和销毁讲解
42 0
|
6月前
|
编译器 容器
关于函数栈帧的创建和销毁
关于函数栈帧的创建和销毁
|
存储
函数栈帧的创建和销毁(下)
函数栈帧的创建和销毁(下)
53 0
|
6月前
|
容器
函数栈帧的创建和销毁介绍
函数栈帧的创建和销毁介绍
41 0
|
6月前
|
存储 编译器
初识函数栈帧的创建与销毁(笔记)
初识函数栈帧的创建与销毁(笔记)
|
11月前
|
编译器 程序员 C语言
函数栈帧的创建与销毁(超详解)
函数栈帧的创建与销毁(超详解)
104 0
|
存储 缓存 编译器
函数栈帧的创建与销毁
函数栈帧的创建与销毁
38 0
|
存储 C语言 C++
你知道函数栈帧的创建和销毁吗?
你知道函数栈帧的创建和销毁吗?
76 0
|
存储 编译器 C++
深入理解内存 —— 函数栈帧的创建与销毁
深入理解内存 —— 函数栈帧的创建与销毁
123 0