函数栈帧的创建和销毁"/>
函数栈帧的创建和销毁
⭐博客主页:️CS semi主页
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:C语言初阶
⭐代码仓库:C Advanced
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作最大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!
函数栈帧的创建和销毁
- 前言
- 一、基础知识
- 1.了解ebp和esp
- 2.main函数也是被调用的
- 二、main函数的创建
- (一)调用函数反汇编(main函数的开辟)
- 1.push指令
- 2.mov指令
- 3.sub指令
- 4.main函数的栈帧
- 5.压入三个临时栈
- 6.lea指令
- 7.小知识
- (二)局部变量的创建
- 1.存入a的值
- 小知识:
- 2.存入b的值
- 3.存入c的值
- 三、Add函数的调用
- (一)传参
- 1.传入b的参数
- 2.传入a的参数
- (二)执行代码
- 1.call指令
- 2.开辟函数栈帧
- 3.执行运算
- 1.存入z的值
- 2.运算z=x+y
- 3.小知识
- 4.返回和销毁
- 四、函数栈帧创建和销毁详细图
- (一)函数栈帧的创建:
- (二)函数栈帧的销毁:
- 五、回答前言的问题
- 1.局部变量是怎么创建的?
- 2.为什么局部变量不是个固定值,而是个随机值呢?
- 3.函数是怎么传参的?
- 4.传参的顺序是怎么样的呢?
- 5.形参和实参有什么联系呢?
- 6.函数调用是怎么做的?
- 7.函数调用的结果是怎么返回的?
- 总结
前言
在学习C语言的前期,可能会面临诸多疑惑,例如:局部变量是怎么创建的?为什么局部变量不是个固定值,而是个随机值呢?函数是怎么传参的?传参的顺序是怎么样的呢?形参和实参有什么联系呢?函数调用是怎么做的?函数调用的结果是怎么返回的?大家前期遇到诸多的疑惑很正常,接下来,我将细细讲解一下函数栈帧的创建与销毁,这样大家就能更深层次了解函数与C语言。
声明:用的是vs2013编译器,编译器越高级,运算越简洁,这里用vs2013能让大家看清楚底层的运算,以下解释均用如下代码:
#include<stdio.h>
int Add(int x, int y) {int z = x + y;return z;
}
int main() {int a = 10;int b = 20;int c = Add(a, b);printf("%d\n", c);return 0;
}
一、基础知识
在了解函数栈帧之前,大家先要了解一下寄存器的概念。
1.了解ebp和esp
我们了解过寄存器的概念,常见的寄存器有eax,ebp,ecx,edx,但我们这里要用到的两个关键的寄存器为ebp何esp,大家可能会对它们比较陌生,接下来先上一张图片解析一下。
如图,esp存放的是低地址,ebp存放的是高地址,当main函数调用的时候,在栈区开辟一块空间,ebp和esp这两个寄存器是用来维护函数栈帧的,但大家可能不太理解,大家慢慢往下看自然就会理解的。
ps:大家可能会好奇一件事情,这个栈顶指针为什么为低地址,而栈底指针则为高地址呢?等到大家看到指针那边学习内存的时候就会很轻松的了解了,这是关于大端存放与小端存放的,是取决于编译器的,VS编译器中是小端存放。
2.main函数也是被调用的
先上一张图片供大家理解。
用VS中的调用堆栈可以看到是mainCRTStartup调用__tmainCRTStartup再调用的main函数的。
接下来,细节来了,既然是函数的调用都会在栈区分配一块空间,那mainCRTStartup和__tmainCRTStartup也是会在栈区分配空间的。
另外,main函数是有返回值的。
这就是为什么我们写程序的时候要在main函数的最后一行写上return 0,因为main函数是有返回值的!
二、main函数的创建
(一)调用函数反汇编(main函数的开辟)
当转到反汇编的时候,发现有许多指令命令,例如:push,mov,sub等等,那接下来我将带你了解这些指令。
1.push指令
这是__tmainCRTStartup的栈区空间,利用push进行压栈,压进去一个ebp,esp指针往上一个ebp空间。如图:
push称为压栈,将ebp压到__tmainCRTStartup上面。
esp的内存显示:经过push压栈以后,esp进行了改变。
2.mov指令
mov指令其实和简单,就把某一个寄存器中的地址移动到另一个寄存器中。将esp往上移动到覆盖ebp寄存器,如图:
大家还可以看一下监视窗口,看ebp是不是把esp覆盖了,很显然是的。
3.sub指令
sub指令就是减指令,那减指令是怎么实现的呢?先上一张图片:
那为什么这个esp寄存器会往上走呢?原因很简单,上面对应的是低地址,栈底对应的是高地址,倘若进行减的操作,esp是往低地址处开辟空间,这个空间的大小取决于减去的大小,以此题为例,减去的大小为0E4h,也就是十进制的128。如下图:
4.main函数的栈帧
大家可能会感到奇怪的一件事情就是为什么平白无故多了一块那么大的空间,总要有个理由吧!这么大一块空间其实是为main函数开辟的一块预用的空间,叫做main函数的栈帧。如图:
5.压入三个临时栈
根据此题目中压入三个栈,这三个寄存器五关紧要,只是压栈压进去的。
根据上面所学习到的内容,push是压栈,将这几个寄存器压进去,将esp寄存器往上走,至于这几个寄存器,等到用完这片空间以后就会被弹出去。
6.lea指令
先简单介绍一下lea吧!lea的英文全拼为:load effective address。
这个ebp-0E4h有点熟悉,这不就是前面sub指令的内容吗,所以这句话的意思是将ebp指针指向的位置减去0E4h到esp指针那个位置,但要想这么做,就需要下面三个语句的加持(mov,mov,rep stos),意思是什么呢?很简单,是将从edi寄存器那个位置开始往下39h个空间都改成eax的内容为CCCCCCCC。如下图演示:
这样我们就将main函数栈帧里面初始化了CCCCCCCC。
7.小知识
push叫做压栈,是从栈顶压进去元素。
pop叫做出栈,是从栈顶拿出去元素。
(二)局部变量的创建
1.存入a的值
假定一个紫色方框为4个字节,ebp-8就为往上两个空间,存入0Ah。如下两图:
小知识:
倘若这里a中没有初始化值,那存放的是CCCCCCCC,大家可能经常会看到没有存放初始值,经常会显示的结果为烫烫烫烫,这就是因为没有初始值存放的是CCCCCCCC,所以说,不初始会有bug的哦!
2.存入b的值
存入b的值与存入a的值一样,如下图:
3.存入c的值
和上面两个一样,如下图:
三、Add函数的调用
(一)传参
那我们继续往下看Add函数。
1.传入b的参数
2.传入a的参数
(二)执行代码
1.call指令
大家一定要先记住这条指令的地址!!!
按一下F11,看一下结果:
call指令是把它下一条指令的地址压到它上一个地址上。
解释:当我们执行完call指令以后,此时需要进入到Add函数的内部进行执行,那执行完以后需要继续往下执行命令,当call记下下一条指令的地址,并将这个地址压入栈顶后,我们回来Add函数的时候正好能找到这个地址,我们看下图的jmp指令。
如下图:
2.开辟函数栈帧
按一下F11,这才真正进入Add函数。
大家发现这一串和前面开辟main函数一样呀!那也需要给大家梳理一下。
这个就是创建Add函数的栈帧。
3.执行运算
1.存入z的值
2.运算z=x+y
先看指令,怎么是ebp+8?这加8不就是传参中a传过去的ecx的a的值吗!
找到ebp+8的地址,存入eax的值为10,再加ebp+12地址内的值20,存入到eax中的值为30了,再放到ebp-8的地址中,也就是z中!
3.小知识
传参的时候是从右往左传参的,压栈是从右往左依次压入的,用的时候是从左往右用的。所以形参是实参的一份临时拷贝!
4.返回和销毁
这一步是先将ebp-8的值暂时存放到寄存器eax中,防止到后面z变量销毁了以后值不存在了。
利用下面几条指令让ebp和esp指回到main函数里面。
esp指向的地址已经是call指令的下一条指令的地址,再经过+8的操作,esp指向edi。
四、函数栈帧创建和销毁详细图
(一)函数栈帧的创建:
(二)函数栈帧的销毁:
五、回答前言的问题
1.局部变量是怎么创建的?
回答:利用反汇编里面的各个指令,先进行压栈移动esp和ebp两个寄存器的位置开辟一块main函数的函数栈帧,再进行ebp-x(字节)的操作在函数栈帧内存放局部变量的值,开辟局部变量。
2.为什么局部变量不是个固定值,而是个随机值呢?
回答:因为如果你没有给这个局部变量定义值,而仅仅就这么放着,内存中存放的是cc cc cc cc,这是能从反汇编和内存中一起看出来的。
3.函数是怎么传参的?
还未调用函数的时候先两个push进行值的压栈,在函数内部利用指针的偏移量找到参数并传过去。即传参时要用到参数的时候只需要ebp寄存器进行加减字节就能找到参数的位置,并进行传参。
4.传参的顺序是怎么样的呢?
回答:从右往左传参并进行压栈的,因为先把右边的参数压进去,再把左边的参数压进栈内,这样当ebp往高地址(向下)寻找参数的时候,先找到函数左边的值再找到函数右边的值,返回的更加严谨缜密。
5.形参和实参有什么联系呢?
回答:函数的形参就相当于实参的一份临时拷贝,存放在各自独立的空间,但值是相同的,改变形参不会影响实参。
6.函数调用是怎么做的?
回答:利用上面的讲解很好的就能够理解函数是如何被调用的又是怎样返回的了。
7.函数调用的结果是怎么返回的?
回答:我们先是把call指令的下一条指令的地址已经记住了,压进栈里了,也事先已经把原始函数的ebp压栈压进去了,利用pop、move等指令跳转到call指令的下一条指令的地址,返回值是通过寄存器的方式返回的。
总结
这里已经比较详细的讲述了函数栈帧的创建和销毁,但还有很多细节没有讲明白,例如很多其他寄存器的原理、新出的vs2022里面寄存器的使用等等,但大家先要理解好这篇所讲的内容,继而后面很难的内容也能够一点点理解!
客官,都读到这里了,赏脸来个三连支持一下吧!
更多推荐
函数栈帧的创建和销毁
发布评论