(C语言内存十三)一个函数在栈上到底是怎样的?

引言

函数的调用和栈是分不开的,没有栈就没有函数调用,本节就来讲解函数在栈上是如何被调用的。

栈帧/活动记录

当发生函数调用时,会将函数运行需要的信息全部压入栈中,这常常被称为栈帧(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 等。之所以要保存寄存器的值,是为了在函数退出时能够恢复到函数调用之前的场景,继续执行上层函数。

实例

下图是一个函数调用的实例:

上图是在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);

那么函数的活动记录如下图所示:

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

原文地址:https://www.cnblogs.com/still-smile/p/14900525.html