栈帧由以下几种元素组成。
- 返回地址
函数完成后要返回的程序内部地址。 - 局部数据存储
为局部变量分配的内存。 - 参数存储
为函数参数分配的内存。 - 栈指针和基指针
运行时系统用来管理栈的指针。
普通C程序员不会关心支持栈帧的栈指针和基指针。不过,理解它们的概念和用法能让你更深入地理解程序栈。
栈指针通常指向栈顶部。基指针(帧指针)通常存在并指向栈帧内部的地址,比如返回地址,用来协助访问栈帧内部的元素。这两个指针都不是C指针,它们是运行时系统管理程序栈的地址。如果运行时系统用C实现,这些指针倒真是C指针。
我们以下面这个函数为例来了解栈帧的创建。该函数传递了一个整数数组和一个表示数组长度的整数。三个printf
语句用来打印参数和局部变量的地址:
float average(int *arr, int size) {
int sum;
printf("arr: %p\n",&arr);
printf("size: %p\n",&size);
printf("sum: %p\n",&sum);
for(int i=0; i<size; i++) {
sum += arr[i];
}
return (sum * 1.0f) / size;
}
执行上述代码会得到类似下面的输出:
arr: 0x500
size: 0x504
sum: 0x480
参数地址和局部变量地址之间的空档,保存的是运行时系统管理栈所需要的其他栈帧元素。
系统在创建栈帧时,将参数以跟声明时相反的顺序推到帧上,最后推入局部变量,如图所示。在这个例子中,size
在arr
之前被推入。通常,接下来会推入函数调用的返回地址,然后是局部变量。推入它们的顺序跟其在代码中列出的顺序相反。
从原理上说,本例中的栈“向上”生长。不过栈帧的参数和局部变量以及新栈帧被添加到了低内存地址。栈的实际生长方向跟实现相关。
for
语句中用到的变量i
没有包含在栈帧中。C把块语句当做“微型”函数,会在合适的时机将其推入栈和从栈中弹出。在本例中,块语句在执行时被推到程序栈中average
栈帧上面,执行完后又弹出。
精确的地址可能会变化,不过顺序一般不变。这一点很重要,因为它可以解释参数和变量内存分配的相对顺序。在调试指针问题时这一点会很有用。如果你不知道栈帧如何分配,这些地址在你看来也毫无意义。
将栈帧推到程序栈上时,系统可能会耗尽内存,这种情况称为栈溢出,通常会导致程序非正常终止。要牢记每个线程通常都会有自己的程序栈。一个或多个线程访问内存中的同一个对象可能会导致冲突,我们将在8.3.1节中讨论这个问题。