转载,详见原文:https://www.cnblogs.com/zhangjinfu/articles/11276542.html
函数的调用和栈是分不开的,没有栈就没有函数调用,本节就来讲解函数在栈上是如何被调用的。
栈帧/活动记录
当发生函数调用时,会将函数运行需要的信息全部压入栈中,这常常被称为栈帧(Stack Frame)或活动记录(Activate Record)。活动记录一般包括以下几个方面的内容:
- 函数的返回地址,也就是函数执行完成后从哪里开始继续执行后面的代码。例如:
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">1 int a, b, c; 2 func(1, 2); 3 c = a + b;</pre>
站在C语言的角度看,func() 函数执行完成后,会继续执行c=a+b;
语句,那么返回地址就是该语句在内存中的位置。
注意:C语言代码最终会被编译为机器指令,确切地说,返回地址应该是下一条指令的地址,这里之所以说是下一条C语言语句的地址,仅仅是为了更加直观地说明问题。
-
参数和局部变量。有些编译器,或者编译器在开启优化选项的情况下,会通过寄存器来传递参数,而不是将参数压入栈中,我们暂时不考虑这种情况。
-
编译器自动生成的临时数据。例如,当函数返回值的长度较大(比如占用40个字节)时,会先将返回值压入栈中,然后再交给函数调用者。
当返回值的长度较小(char、int、long 等)时,不会被压入栈中,而是先将返回值放入寄存器,再传递给函数调用者。
- 一些需要保存的寄存器,例如 ebp、ebx、esi、edi 等。之所以要保存寄存器的值,是为了在函数退出时能够恢复到函数调用之前的场景,继续执行上层函数。
下图是一个函数调用的实例:

上图是在Windows下使用VS2010 Debug模式编译时一个函数所使用的栈内存,可以发现,理论上 ebp 寄存器应该指向栈底,但在实际应用中,它却指向了old ebp。
在寄存器名字前面添加“old”,表示函数调用之前该寄存器的值。
当发生函数调用时:
- 实参、返回地址、ebp 寄存器首先入栈;
- 然后再分配一块内存供局部变量、返回值等使用,这块内存一般比较大,足以容纳所有数据,并且会有冗余;
- 最后将其他寄存器的值压入栈中。
需要注意的是,不同编译器在不同编译模式下所产生的函数栈并不完全相同,例如在VS2010下选择Release模式,编译器会进行大量优化,函数栈的样貌荡然无存,不具有教学意义,所以本教程以VS2010 Debug模式为例进行分析。
关于数据的定位
由于 esp 的值会随着数据的入栈而不断变化,要想根据 esp 找到参数、局部变量等数据是比较困难的,所以在实现上是根据 ebp 来定位栈内数据的。ebp 的值是固定的,数据相对 ebp 的偏移也是固定的,ebp 的值加上偏移量就是数据的地址。
例如一个函数的定义如下:
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">1 void func(int a, int b){ 2 float f = 28.5; 3 int n = 100; 4 //TODO:
5 }</pre>
调用形式为:
<pre class="info-box" style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; color: rgb(51, 51, 51); font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">func(15, 92);</pre>
那么函数的活动记录如下图所示:

这里我们假设两个局部变量挨着,并且第一个变量和 old ebp 也挨着(实际上它们之间有4个字节的空白),如此,第一个参数的地址是 ebp+12,第二个参数的地址是 ebp+8,第一个局部变量的地址是 ebp-4,第二个局部变量的地址是 ebp-8。
网友评论