栈
什么是栈?
在计算机中,栈可以理解为一个特殊的容器,用户可以依次把数据放进去,栈可以存放形参,局部变量,局部数组等函数作用域内的数据,目的是为了完成函数的调用。
注意:栈也经常被称为堆栈,但是堆仍被叫做堆,所以堆栈这个名字只是表示栈,而不包含堆。
栈怎么存放数据?
栈中存放数据依据“先进后出”原则,即先放入的数据最后才能取出,后放入的数据先取出,且先放入的数据处于高地址。
内存中怎么确定栈的位置?
栈是一段连续的内存,计算机通过栈顶和栈底来确定栈区的位置(同时确定底和顶的位置),通常栈顶被esp寄存器指向,栈底被ebp寄存器指向,当数据出栈入栈时,只移动栈顶,所以数据入栈(也叫压栈(push))时,esp指向减小,当数据出栈(也叫弹出(pop))时,esp指向增大。
栈有大小吗?大小是多少?与什么有关?
对每个程序来说栈的大小一般是1m~8m,这是在编译期间就决定的,在后面程序运行期间不能更改,栈的内存与编译器有关,编译器会为栈设定一个默认的最大值(在 VC/VS 下,默认是 1M,在 C-Free 下,默认是 2M,在 Linux GCC 下,默认是 8M。),当然我们也可以自己在编译器中更改栈内存的大小。
注意:栈是对一个线程来说的,每个线程都有自己的栈,所以一个程序可以有许多个栈,并且由此严格来说,栈内存的最大值是对于线程,而不是对于程序。
栈溢出
当程序使用某个栈内存大于默认值时,就会出现栈溢出错误
栈区的内存管理
栈区的内存系统自动分配和释放,发生函数调用时就分配内存给函数内部数据,使用完后销毁,所以函数内部的形参,局部变量,局部数组,局部对象等等只在函数内部有效。
一个函数在栈上是怎么样的?
当发生函数调用时,要把所有运行函数时要用的信息压入栈中,这叫栈帧(Stack Frame)或活动记录。
活动记录一般包括下面几点内容
1.返回地址。当函数调用时,要回到原来,继续执行下面的代码,所以要有一个返回地址来回到函数调用时的位置,以便执行接下来的代码。
2.参数和局部变量。ps:有些编译器或者编译器开启了优化模式选项时,会通过寄存器来传递参数,而不会压入栈中。
3.编译器产生的临时数据。例如:当返回值是占较少字节的数据类型时(char,int,long等),直接放到寄存器然后交给调用者,但当较多字节时,返回值会被暂时压入栈中,然后交给调用者。
4.一些寄存器的值,这些寄存器的值能使我们回到调用时的情形,继续执行上层函数。
函数调用惯例(calling convention)
为了程序正常进行,调用方和被调用方必须遵循同样的规则,这叫调用惯例
一般调用惯例包括以下几点
1.参数的传递方式。要说明是通过栈还是通过寄存器。
2.参数的传递顺序。比如从左到右还是从右到左。
3.参数的弹出方式。在函数调用结束后必须要把栈的数据全部弹出以便栈在调用前后一致,这个工作可以由被调用方完成也可以由调用方完成。
4.函数名修饰方式。函数名被编译时会被修改,调用惯例可以决定如何修改函数名。
下面是函数进栈出栈的整个过程
-
void func(int a, int b){ int p =12, q = 345; } int main(){ func(90, 26); return 0; }
最后两步:old ebp 出栈,并赋值给现在的 ebp,此时 ebp 就指向了 func() 调用之前的位置,即 main() 活动记录的 old ebp 位置,如步骤⑨所示。
这一步很关键,保证了还原到函数调用之前的情况,这也是每次调用函数时都必须将 old ebp 压入栈中的原因。
最后根据返回地址找到下一条指令的位置,并将返回地址和实参都出栈,此时 esp 就指向了 main() 活动记录的栈顶, 这意味着 func() 完全出栈了,栈被还原到了 func() 被调用之前的情况。
注意:虽然总说函数结束时局部变量就被销毁了,但其实栈上的数据只有在后续函数继续入栈时才能被覆盖掉,这就意味着,只要时机合适,在函数外部依然能够取得局部变量的值。