一.什么是函数栈帧?
函数栈帧:(stack frame)就是函数调用过程中在程序的调用栈(call stack)所开辟的空间,这些空间 是用来存放:
1.函数参数和函数返回值。
2.临时变量(包括函数的非静态的局部变量以及编译器自动生产的其他临时变量)。
3.保存上下文信息(包括在函数调用前后需要保持不变的寄存器)。
预备知识:
1.首先在解析之前,我们要知道栈的一些相关知识。
栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今看到的所有的计算机语言。在经典的计算机科学中,栈被定义为一种特殊的容器
用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但是栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO)。就像叠成一叠的术,先叠上去的书在最下面,因此要最后才能取出。 在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据 从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减小。(先进后出,后进先出)。在经典的操作系统中,栈总是向下增长(由高地址向低地址)的。在我们常见的i386或者x86-64下,栈顶由成为 esp 的寄存器进行定位的。
2.其次,我们要知道寄存器和汇编指令的一些知识。
a.相关的寄存器: :不同的编译器或者不同的版本所用的代号不同(但是汇编代码的大致思路都一样。)
eax:通用寄存器,保留临时数据(常用于放函数return的返回值)
ebx:通用寄存器,保留临时数据
ecx,edx:通用寄存器,保留临时数据
ebp:栈底寄存器(存的是地址,相当于一个指针指向我们需要访问的空间位置)
esp:栈顶寄存器(存的是地址,相当于一个指针指向我们需要访问的空间位置)
rip:指令寄存器,保存当前指令的下一条指令的地址
b.相关的汇编指令:
mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器也要发生改变
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub:减法命令
add:加法命令
call:函数调用,1. 压入返回地址 2. 转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip,类似pop eip命令
分析:利用一个例子如下:
#include<stdio.h>
int Add(int x, int y)
{
int z=0;
z=x+y;
return z;
}
int main()//在main函数中调用实现两个数相加的一个函数
{
int a=10;
int b=20;
int c=0;
c=Add(a,b);
printf("%d\n",c);
return 0;
}
首先,main函数在vs2022中也是被其他函数调用的叫做invoke_main函数。此时会生成一个invke_main的栈帧空间,栈顶指针esp指向低地址的栈帧空间的最上面的位置,而栈底指针ebp指向高地址的栈帧空间的最下面的位置。
然后,同理,在进入main函数后,在创建我们的局部变量a,b,c之前,会执行我们的汇编指令,用push,sub等相关指令,改变esp和ebp指针的指向,创建新的main函数的栈帧空间。之后还是在main函数的栈帧空间利用mov等相关汇编指令,改变esp和ebp指针的指向,找到合适空间创建局部变量a,b,c。
之后,调用Add函数,在进入Add函数之前,会将我们的参数a,b的值放入我们的ecx,edx寄存器中(这里我们也可以看出传参时,形参只是我们的实参的拷贝,我们再函数内部改变形参的值,并不会反过来改变我们的实参),c放入eax寄存器中。随后进入Add函数,通过mov汇编指令,找到ecx,edx,存的a,b的值。随后还是和之前一样,开辟一个Add函数的栈帧空间,在Add的函数栈帧空间中创建新的局部变量z,通过指针把b的值放到eax中,再把a的值放到ecx中,通过add算出的ecx加eax的值放到eax中,把eax的值通过指针找到z的地址,放入z的变量中。执行return时,为了防止调用完函数,函数栈帧被销毁,里面的数据也消失。所以我们要把返回值再装进eax寄存器中。之后通过pop汇编指令,出栈销毁。
紧接着,我们从Add函数中出来,Add创建的函数栈帧被销毁,再一次回到main函数的栈帧空间,把eax中的返回值,通过指针找到我们c变量的地址,并存入c中,这样就得到了我们的两个数的和。直到我们main函数执行完之后,也通过pop,出栈销毁。
大致的过程图表示:下图是再vs2022中运行得到的,可能代号和上述有差别。
进入mian函数,创建一个main函数的栈帧空间:
在main函数中创建临时变量a,b,c,并调用Add函数,把a,b传参到edx,ecx寄存器中:
进入Add函数,实现加法运算,并把返回值存入z中,在函数栈帧销毁前,把z的值存入寄存器eax随后把Add的函数栈帧空间销毁。
执行完Add函数后,再回到main函数的栈帧空间中,把eax寄存器中的值存入c中并打印。最后把main函数执行完后,并把main函数的函数栈帧空间销毁。