正片开始👀
今天来讲讲我对栈帧创建与销毁的拙见。
理解什么是栈帧首先知道什么是栈:
在数据结构中, 栈是限定仅在表尾进行插入或删除操作的线性表。栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。
栈有什么用?
在计算机系统中,栈也可以称之为栈内存是一个具有动态内存区域,存储函数内部(包括main函数)的局部变量和方法调用和函数参数值,是由系统自动分配的,一般速度较快;存储地址是连续且存在有限栈容量,会出现溢出现象程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使栈减小。
栈用于维护函数调用的上下文,离开了栈函数调用就没法实现。
讲到这里,小朋友你是否有很多问号?那打住,我们抛开无聊的学术前文,另起炉灶。
寄存器👏
要讲清楚栈帧就必须理解一手寄存器。尤其是 ebp,esp这2个寄存器中存放的地址,这两个地址是用来维护函数栈帧的。
寄存器有很多种这里不赘述
main函数创建👏
我们这里随便搞一个最简单的Add函数
int add(int x,int y) { int z; z=x+y; return z; } int main() { int data1; int data2; int ret; while(1) { int data1,data2 = 0; scanf("%d %d",&data1,&data2); add(data1,data2); return 0; }
搞栈帧的话我的编译器是不适合的,我是vs2019,因为编译器越高级函数的封装越复杂周密,不容易我们去剖析栈帧,我就尽量语言表达严谨一点吧。编译器反汇编过程就能反应我们栈帧创建的过程,这是我在网上找的反汇编页面可以参考一下
其中反汇编用到的指针我们要清楚意义:
在编译器中,main函数也是会被其他函数调用的,调用堆栈窗口后反汇编可以看到如下字样:
main _tmainCRTStartup mainCRTStartup
后面两句意义不明的玩意儿就是在调用main函数。为什么要讲这个呢?我们说每一次函数调用都要分配空间,main函数不例外也要分配栈帧空间。
以下内容和上面汇编指令表食用更佳:
首先 push ,即压栈,就是往栈sei东西进去。push 会让esp让低地址走,就会在原先基础上压进来一个 ebp 指针。
接下是 mov 指针,mov把后面的指针赋到前面去,esp给了ebp,也就是相当于在移位。
接下来是 sub 减法操作,减去一个内容来使esp指针走向低地址来开辟main函数栈帧。
过程模拟如下:
局部变量创建👏
接下来esp已经走到那几个内容的头上去了,这时出现了 lea 指针,即 load effective address 加载有效地址,其实在这个指针指定对象里面放入一个地址
我们后面的 [ebp-0C0h],其实就是刚刚 sub操作,本质上还是原来开辟栈帧起点 ebp 的地址,把这个地址放入edi 里面。
接下来的连续 mov 时在把从edi 开始的 30h 这么多个空间里面的 dword(double word-四字节数据)全部初始化成 eax 里面 “0CCCCCCCCh”的内容,保证为main函数预开辟的内存全变成 “CCCCCCCCh”,这么说来改的还是蛮多的。
接下来当我们创建变量时,比如 int a = 10;就会出现类似下面字样:
int a = 10; 00C2142E C7 45 EC 0A 00 00 00 mov dword ptr [ebp-8h],0Ah
这里就是在创建局部变量了, ebp指针减了 8h,这个 8h 就是给a留的位子**(这里的 h 是编译器给的标识,我们只需要明白这是一个十六进制数)**就行了。所以总结一下,其实创建方式与main函数没有太大出入。
函数部分👏
Add函数传参时也是在将 esp 进行压栈,但注意,这时的esp里面的值是 10,相当于是在传 10 这个值。传完参紧接着就会调用函数
00C2144B E8 91 FC FF FF call 00C210E1
call 指针作用就是调用函数,F11 执行call指令后会发现在跳转到作用域的同时,他会把 call指令的下一条指令的地址传到里面,从顶上压进来一个main函数的 ebp ,这时 esp 会继续往上面跑,一但函数执行完后返回值就会很自然的回到该地址。
在main函数的 ebp 上面又会传统艺能,以相同的方式开辟 Add 函数的空间,又初始化成全 c,以相同方式创建临时变量……
这时你可能会注意到传进函数的 x,y去哪里了?其实已经为他准备好了,在返回进行下一项指令时,x,y就会乖乖跑到这片空间储存
Add函数完成后回把传的参返回, 就是我们的 pop 指针,即出栈,这里参数每从栈顶pop一次 esp 指针就会上移一个单位,ebp也会随之退回一个单位,利用指针的偏移量找回他的形参,最后返回值ret,其逻辑本质上就是弹出main ebq那里的下一项指令的地址。
我们走出函数后,esp,ebq会回收,这时这块空间就会直接销毁,挫骨扬灰。
这样整个函数部分就完美的呈现出来了。
形参与实参👏
形参确实是我在压栈时开辟的空间,这个空间他是独立的,只有值是相同的,形参本质上是实参的一份临时拷贝,改变形参并不影响实参,那返回值是怎么带回来的呢?答:是通过寄存器。