函数栈帧(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间是用来存放:
函数参数和函数返回值
临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)
保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。
我们可能常常对这些问题感到困惑:
函数是如何创建的呢?
函数又是如何调用的呢?
为什么在创建好一个变量不初始化就是随机值呢?
函数是如何将值反回来的?
参数又是如何给函数传递的?顺序又是怎样的呢??
经过这篇文章相信能让你开启一个新世界的大门;
在开始前先让我们认识一下我们会遇到的一些汇编指令:
寄存器:
eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ebp:栈底寄存器
esp:栈顶寄存器
eip:指令寄存器,保存当前指令的下一条指令的地址
汇编指令:
mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器也要发生改变
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub:减法命令
add:加法命令
call:函数调用,1. 压入返回地址 2. 转入目标函数
jmp:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip,类似pop eip命令
我使用的是vs2022的编译器进行的操作;
首先我们要明确一个东西,在栈上的空间是由高到低使用的;
我们给函数开辟一块栈区空间,那么就是将这块空间划分给了这块函数,那么就得限制这个空间的边界,不能让别的函数在这个空间进行操作,也不能让这个函数跑到别的空间中;
当调用哪个函数的时候,esp和ebp就会跑去维护哪个函数的函数栈帧空间;
这个时候我们就可以用2个指针来维护这块区域;ebp和esp是存放的是地址;
esp用来存放栈顶的地址,ebp用来存放栈底的地址;esp~ebp之间的空间就是操作系统分配给这次函数调用所开辟的空间;
我们可以在编译器中查看函数的互相调用关系;
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;
}
以这段代码为例:
通过这段代码我们发现main函数也是被调用的;main函数被invoke_main()调用,这个函数又被
_scrt_common_main_seh()调用;往后依次这样的顺序;
前面我们知道了main函数也是被调用的,那么当我们执行到main函数的时候肯定也在前面创建了一个函数栈帧来调用main函数;
前面的invoke main函数肯定也有一块属于自己的函数栈帧空间;创建好了之后就开始调用main函数了,让我们具体来看看main函数是如何创建自己的函数栈帧空间的;
下面给大家上内存观察:
?push压入一个元素之后,esp的地址也变小了;
mov将esp的值给ebp,ebp指向了一个新的地址;
sub指令,esp-0E4h,0E4h(228)是一个16进制数字,地址减去数字还是一个地址,此时esp指向了一个新的地址;此时的esp-ebp的函数栈帧空间范围就已经创建好了;
执行3次push,esp的地址变小;esp指向了一块新的空间;
将edi这个地址开始向下进行9次将4个字节的空间初始化为0cccccccc;
到这个时候,main函数的函数栈的创建才正式完成;
内存观察:
局部变量也就全部创建好了,从这里我们也可以发现,为什么我们不初始化的话为什么里面会是随机值,这个随机值其实是在创建函数栈帧的时候自己放进去的;
在函数调用前我们先进行传参数;
mov 将ebp-14h这个地址向后4个字节的数据交给eax;然后将eax压栈;
mov 将ebp-8这个地址向后4个字节的数据交给ecx;然后将ecx压栈;
这一步就是传参;
接下来这一条指令非常重要,call指令将它下面一条指令的地址压入栈中保存;
jmp开始跳转到Add函数;
然后又是我们熟悉的开辟栈帧空间,创建局部变量;
我们好像貌似并没有发现Add的函数栈帧空间里面有x和y啊,那么形参在哪去了呢?我们接着往下看;
将ebp+8这个地址向后的4个字节的数据给eax;
ebp-8不就是我们之前往栈区压入的元素吗?
执行add指令,然后将ebp+0ch地址向后4个字节的元素相加到eax;
再将eax放入到ebp-8向后的4个字节的空间里;
然后开始把值往回带,我们注意最后一条mov指令,将ebp-8往后4个字节的空间的数据放入寄存器eax中;因为执行完条语句我们就会将函数栈帧销毁,所以这条代码的实际上的意义是将我们的答案存入到寄存器当中,并不是直接返回答案;
执行完后,就要回收空间还给操作系统了;
先弹出栈顶的三个元素然后将值赋给自己,然后esp+0cch,esp指向了栈底;mov将ebp的地址给esp,此时空间已经回收完毕;
弹出ebp(我们在栈上压入了一个元素,这个元素的内容是main函数的栈底地址)将ebp对值给自己,然后ebp回到了main函数的栈底的地址;
执行ret指令,恢复返回地址,别忘了我们在执行call指令的时候还将call指令单下一条指令单地址保存了起来,这个时候就放回保存的那个地址,至此又回到了main函数里面;但是此刻返回值还没有带回来;
先给esp加8,让栈顶指针回到main函数的栈顶;
mov将eax的值交给ebp-20h,那这不就是我们的c吗?至此函数的返回值也带回来了;
函数栈帧是如何销毁,形参是实参的一份临时拷贝,传参的顺序这些通过这一篇文章都能够知道