ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] # 栈的概念 在计算机中,栈可以理解为一个特殊的容器,用户可以将数据依次放入栈中,然后再将数据按照相反的顺序从栈中取出。也就是说,先放入的数据最后才能取出,而最后放入的数据必须先取出。这称为先进后出(First In Last Out)原则。 放入数据常称为入栈或压栈(Push),取出数据常称为出栈或弹出(Pop)。如下图所示 ![](https://box.kancloud.cn/d594e1a27a2023660dcce0e6c6ed6d47_672x273.png) 可以发现,栈底始终不动,出栈入栈只是在移动栈顶,当栈中没有数据时,栈顶和栈底重合。 从本质上来讲,栈是一段连续的内存,需要同时记录栈底和栈顶,才能对当前的栈进行定位。在现代计算机中,通常使用`ebp`寄存器指向栈底,而使用`esp`寄存器指向栈顶。随着数据的进栈出栈,esp 的值会不断变化,进栈时 esp 的值减小,出栈时 esp 的值增大。 > ebp 和 esp 都是CPU中的寄存器:ebp 是 Extend Base Pointer 的缩写,通常用来指向栈底;esp 是 Extend Stack Pointer 的缩写,通常用来指向栈顶。 如下图所示是一个栈的实例: ![](https://box.kancloud.cn/82c5bcb4fe1d9d818225195c8f52ac97_255x222.png) # 栈的大小以及栈溢出 对每个程序来说,栈能使用的内存是有限的,一般是 1M~8M,这在编译时就已经决定了,程序运行期间不能再改变。如果程序使用的栈内存超出最大值,就会发生栈溢出(Stack Overflow)错误。 > 一个程序可以包含多个线程,每个线程都有自己的栈,严格来说,栈的最大值是针对线程来说的,而不是针对程序。 栈内存的大小和编译器有关,编译器会为栈内存指定一个最大值,在 VC/VS 下,默认是 1M,在 C-Free 下,默认是 2M,在 Linux GCC 下,默认是 8M。 当然,我们也可以通过参数来修改栈内存的大小。以 VS2010 为例,在工程名处右击,会弹出一个菜单,选择“属性”,会出现一个对话框,如下图所示: ![](https://box.kancloud.cn/53d86741e1cada5a5efe34c09a2abf18_505x298.png) 该图中,我们将栈内存设置为 4M。提示:栈也经常被称为堆栈,而堆依然称为堆,所以堆栈这个概念并不包含堆,大家要注意区分 # 栈帧/活动记录 当发生函数调用时,会将函数运行需要的信息全部压入栈中,这常常被称为栈帧(Stack Frame)或活动记录(Activate Record)。活动记录一般包括以下几个方面的内容: 1. 函数的返回地址,也就是函数执行完成后从哪里开始继续执行后面的代码。例如: ~~~ int a, b, c; func(1, 2); c = a + b; ~~~ 站在C语言的角度看,func() 函数执行完成后,会继续执行`c=a+b;`语句,那么返回地址就是该语句在内存中的位置。 > 注意:C语言代码最终会被编译为机器指令,确切地说,返回地址应该是下一条指令的地址,这里之所以说是下一条C语言语句的地址,仅仅是为了更加直观地说明问题。 2. 参数和局部变量。有些编译器,或者编译器在开启优化选项的情况下,会通过寄存器来传递参数,而不是将参数压入栈中,我们暂时不考虑这种情况。 3. 编译器自动生成的临时数据。例如,当函数返回值的长度较大(比如占用40个字节)时,会先将返回值压入栈中,然后再交给函数调用者。 > 当返回值的长度较小(char、int、long 等)时,不会被压入栈中,而是先将返回值放入寄存器,再传递给函数调用者。 4. 一些需要保存的寄存器,例如 ebp、ebx、esi、edi 等。之所以要保存寄存器的值,是为了在函数退出时能够恢复到函数调用之前的场景,继续执行上层函数。 下图是一个函数调用的实例: ![](https://box.kancloud.cn/3f000b53946d8e681553d1a5c5c5fe23_308x364.png) 上图是在Windows下使用VS2010 Debug模式编译时一个函数所使用的栈内存,可以发现,理论上 ebp 寄存器应该指向栈底,但在实际应用中,它却指向了old ebp。 > 在寄存器名字前面添加“old”,表示函数调用之前该寄存器的值。 当发生函数调用时: * 实参、返回地址、ebp 寄存器首先入栈; * 然后再分配一块内存供局部变量、返回值等使用,这块内存一般比较大,足以容纳所有数据,并且会有冗余; * 最后将其他寄存器的值压入栈中。 需要注意的是,不同编译器在不同编译模式下所产生的函数栈并不完全相同,例如在VS2010下选择Release模式,编译器会进行大量优化,函数栈的样貌荡然无存,不具有教学意义,所以本教程以VS2010 Debug模式为例进行分析 # 关于数据的定位 由于 esp 的值会随着数据的入栈而不断变化,要想根据 esp 找到参数、局部变量等数据是比较困难的,所以在实现上是根据 ebp 来定位栈内数据的。ebp 的值是固定的,数据相对 ebp 的偏移也是固定的,ebp 的值加上偏移量就是数据的地址。 例如一个函数的定义如下: ~~~ void func(int a, int b){ float f = 28.5; int n = 100; //TODO: } ~~~ 调用形式为: ~~~ func(15, 92); ~~~ 那么函数的活动记录如下图所示: ![](https://box.kancloud.cn/b99a566ac3843115fcb5ac1dc0a4b890_316x453.png) 这里我们假设两个局部变量挨着,并且第一个变量和 old ebp 也挨着(实际上它们之间有4个字节的空白),如此,第一个参数的地址是 ebp+12,第二个参数的地址是 ebp+8,第一个局部变量的地址是 ebp-4,第二个局部变量的地址是 ebp-8