在学习C语言函数的过程中,我们学会了如何创建函数,调用函数等。然而函数的创建以及销毁的过程都已被编译器自动执行,我们需要去了解函数栈帧的创建与销毁,帮助我们更好的理解函数在栈区内的基本操作。
1、什么是栈区
讲到栈区,我们不得不提出内存分区的概念,图中表示的是内存中各分区的基本元素。 由此,我们可以知道栈区中存放的形参 ,局部变量 等,都是函数内部常用的组成元素。
栈区的使用习惯是先使用高地址后使用低地址,由高到低。
2、什么是寄存器
寄存器:是集成到CPU上的独立 的存储空间。 作为计算机最小 的存储空间,其速度是各个存储设备中最快 的,与CPU实时交互 的存储设备。 硬盘,内存以及寄存器是完全独立的存储空间。
基本寄存器:eax,ebx,ecx,edx,ebp ,esp 。 ebp,esp两个寄存器中存放的栈区内存地址。
ebp(栈底指针)一般存储的是目标函数在栈区内的最高地址。 esp(栈顶指针)一般存储的是目标函数在栈区内的最低地址。
这两个寄存去存储的地址是用来维护函数栈帧的,而ebp和esp维护的是正在被调用的函数栈区空间。
可以在寄存器内部对数据执行算术及逻辑运算。 可以将值存入寄存器,让其保留并带出。 存于寄存器内的地址可用来指向内存的某个位置,即寻址。
3、函数栈帧关键问题
首先分配好栈帧空间,初始化该空间,为局部变量分配栈帧中的空间。
如果不初始化局部变量,该内存地址对应的值就会由操作系统分配随机值。
没有进入Add函数时,提前将参数值压入栈中,方便Add函数根据内存地址获取。传参的顺序是调用函数时从右向左传递,因为栈区地址中栈底到栈顶是由高到低,函数在获取参数时是由低到高,所以先压后面的参数,后压前面的参数最合理。
形参是压栈是开辟的空间,形参与实参值相同,而空间是独立的(临时拷贝)。
调用函数之前将call的下一条指令地址压入,然后存入ebp在上一个函数内的地址(记住返回地址)返回值是由寄存器带回的。
4、函数栈帧的创建与销毁
*本次实验均使用十六进制演示
4.1部分指令
- ret指令- 返回栈顶内存地址
- push- 压栈操作 pop指令- 出栈操作
- add指令- 相加操作
- call指令- 调用函数
- mov指令- 赋址操作
- lea ( load effective address) 指令- 加载有效地址
- esp寄存器- 会随着指令的实行,改变自身的内存地址
- ecx寄存器- 存储的是rep stos操作的移动次数
4.2过程
4.2.1调用main函数
在vs中,首先调用main函数,main函数作为程序的入口,但是它也是被其他函数调用的。
调用过程:main->__tmainCRTStartup->mainCRTStrartup
- 开始调用函数时,栈区首先压入ebp, 此时esp也会随着ebp移动,esp的地址会比之前少4 个字节。
- 将esp的值给ebp,之后开辟新空间,由编译器决定其大小由此改变esp的地址。
- push ebx, esi, edi,每push一次esp的地址会改变(指向顶端)。
- lea 将ebp地址减去之前开辟的新空间地址大小,放入edi中。
- mov ecx, 39 h
- mov eax, 0 CCCCCCCCh dword ( doubleword) “双字”- 4 个字节。
- rep stos dword ptr。
es: [ edi] 由edi开始向下的39 h的空间改为eax的内容(0 CCCCCCCCh )
- 将main函数预留空间内的值全部初始化为eax的内容 main函数栈帧的开辟到此结束
4.2.2Add函数栈帧
4.2.2.1*main函数内创建局部变量
- mov dword ptr [ ebp- 8 ] , 0 Ah 将0 Ah放入ebp- 8 的位置 ebp- 8 的位置也就是为a存放的空间。
- mov dword ptr [ ebp- 14 h] , 14 h 将14 h放入ebp- 14 h的位置。
ebp- 14 h的位置也就是为b存放的空间, 这里的14 h换算出来是20 。
- dword ptr [ ebp- 20 h] , 0 存放变量c的值。
4.2.2.2*Add函数栈区创建
- 传参操作将b, a重新压栈进入空间内,esp值随之改变。
- 调用函数后,压入call指令的下一条指令的地址。(该命令调试部分在调用Add之前)
- 调用成功后,与main函数栈帧的创建相同,创建一个Add函数栈帧。此时的push操作只是将ebp寄存器借用上来,mov操作才是将ebp真正的挪上来。
- 将上面压入的a, b值以内存地址的方式找到并调用到Add函数中。
- 将eax的值放入ebp- 8 (即变量z的内存地址)中,函数参数从右向左传。
- 形参根本不是Add函数内部产生的,而是main函数传参后的空间中。
- 寄存器不会因为程序的推出而销毁,简单理解是函数内部的值存入后,即使函数调用完成仍能保留 将esp的地址赋给ebp,销毁Add函数栈帧。
- 将之前借的main函数ebp地址弹给现在的ebp,相当于找到了之前ebp的内存地址,此时回到了之前main函数的栈帧中。(即返回操作)
- ret指令返回之前存入的call指令的下一条指令的内存地址。
- 由于形参x, y已经没用了,所以这里直接跳过(释放该空间),将esp指向高8 个字节的地址 main函数的销毁与Add函数的销毁步骤相同。
注意:静态变量不属于栈区,所以其创建与销毁与函数栈帧无关
5、Add函数
# 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 ;
}
关于函数栈帧创建与销毁问题的学习到这里就结束了,并且相关更深层次的逻辑更清晰也会更复杂。