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

啊我摔倒了..有没有人扶我起来学习....
你好,我是CGod,每个人都可以5分钟编程。
欢迎来到我的主页:《CGod的后花园》
前言
对于函数的学习,我们可能有以下疑惑:
- 局部变量是怎么创建的?
- 为什么局部变量的值是随机的?
- 函数是怎么传参的?传参的顺序是怎样的?
- 形参和实参是什么关系?
- 函数调用是怎么做的?
- 函数调用结束后怎么返回的?
==如果知道函数栈帧的创建和销毁就都会了,其实就是修炼了自己的内功,也能搞懂后期更多的知识==
一、知识铺垫
- 这次讲解博主使用的是VS2013,因为越高级的编译器实现函数栈帧的创建越复杂,越不容易学习和观察函数栈帧的创建和销毁;同时在不同的编译器下,函数调用过程中栈帧的创建是略有差异的,具体细节取决于编译器的实现
- 引入2个寄存器:
ebp、esp,这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函数的过程来研究
-
鼠标右键在随便一个空白处点击,转到反汇编页面


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

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

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

对应下图:

-
mov ebp,esp表示把esp的值赋给ebp,此时ebp和esp的指向一致

对应下图:

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



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

-
对应下图:

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

对应下图:

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

这个cccccccc就是烫烫烫烫烫的来源。。,
- 先从监视窗口查看
-
继续继续,
0Ah就是10,把10放进ebp-8,ebp-8就是a的所在,==注意此时内存里的值是十六进制==

对应下图:

-
这两步同理可得,其中
ebp-14h是b的所在,ebp-20h是c的所在

对应下图:

- 到此我们就已经充分了解了,原来函数栈帧是==第1-6步==这样创建的,而创建了函数栈帧之后,函数内部的局部变量是==第7-8步==这样创建的且赋值前并不是0。而每个局部变量之间不是连续存放的,若越界访问就可能访问到
cccccccc,打印出烫烫烫烫烫...
4. 深入了解函数栈帧的创建与销毁——Add函数栈帧的创建与销毁
- 此时回顾一下,代码已经执行到这一步了

没想到这才执行了几步!!!下面要开始调用Add函数了,继续看看是怎么调用的吧~~
-
这四步相信铁汁们也很容易理解了,把
ebp-14h的值传给eax,再将eax压栈;把ebp-8h的值传给ecx,再将ecx压栈,这四步其实就是在传参

对应下图:

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

对应下图:

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

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

对应下图:

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

-
pop表示出栈,弹出edi、esi、ebx

对应下图:

-
把
ebp的值传给esp,此时esp与ebp指向同一个方向

对应下图:

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

对应下图:

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

-
而
add esp,8表示esp + 8,那么esp指向再次改变

对应下图:

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

对应下图:

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

- 点赞
- 收藏
- 关注作者












































评论(0)