- 程序在编译的时候, 得到的可执行文件分为代码段(机器指令)和数据段。程序运行时会分配虚拟内存, 将代码段和数据段都加载到虚拟内存中, CPU去代码段读取一条条的机器指令来执行。 我们的函数里面的指令就存在代码段中(有个函数名)。
- 当我们调用一个函数的时候, 其实会触发一个call指令, 引导CPU去该函数名对应的机器指令地址去指向函数体。
- 我们最后被调用的函数里面使用的内存资源总是需要最先被释放掉的(他里面的局部变量等其他函数用不了)。 所以使用栈来给函数分配变量,所谓的先进后出, 是以函数为单位的!!
函数栈帧大概结构如下 :
其中ebp指向栈基(相当于当前函数和上一个函数的分界线), esp是一个移动的指针, 随着函数中变量的分配不断下移, 最终函数执行完之后rsp指向上一个函数的esp,当前函数栈中的数据全部无效,相当于这块内存可以被重新使用。
- 一个栈帧中应该包含 :
- 函数参数(输入参数和返回参数)
- 函数执行完之后的ret指令返回的地址
- 栈基(ebp)
- 函数中定义的各种变量
- 栈指针(一直在移动)
- 示例 :
int Add(int x, int y)
{
int ret = 0;
ret = x + y;
return ret;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
printf("%d\n", c);
system("pause");
return 0;
}
函数从main开始执行, 一开始将main中的变量a、b、c从上到下压入栈中,
调用 Add(a,b) 的时候,将他的下一条机器指令的地址也传过去了。
在Add里面,先分配了 ebp, 然后分配x、y并赋值(算是传入参数)。再执行加法运算,分配ret(算是局部变量),过程中栈指针一直在往下移动, 最后返回的时候, 将栈指针指向旧的ebm, 再通过返回地址返回到main函数中。 这样原来分配给Add函数的那部分内存就可以被重新使用了。程序通过栈指针和偏移来定位每个参数和返回值。
Go里面的栈
C中函数的栈帧是逐步扩张的(每定义一个变量就扩张一次), 但是go里面函数栈帧的扩张是一次性分配一大块(直接将栈指针移动到所需最大栈空间的位置),然后通过栈指针加偏移值这种相对寻址方式使用函数的栈帧。一次性分配主要是为了避免栈访问越界(比如我有多个G在同时操作这个栈,我前面的G很可能会修改后面的G的栈),
一个函数的栈帧大小在编译期就可以确定!!! 对于栈消耗较大的函数,编译器会在函数头部插入一个检测代码,如果发现需要进行“栈增长”,就会另外分配一块足够大的栈空间, 将原来的数据拷过来并释放原来的空间。
这里如果是函数A调用函数B, 其实可能B被释放了但是A的一部分变量还没被加载到栈里面来, 其实不是严格的先进后出, 可以看作是一个包裹关系。