天天看点

函数调用栈与活动记录

函数调用栈与活动记录

在调试的时候经常遇到栈溢出,由此总结了下函数调用栈的知识。

为了理解C++是如何执行函数调用的,先考虑一个称为栈(stack)的数据结构。栈是一种后入先出的数据结构——压入(插入)栈的最后一项,是从栈中弹出(移走)的第一项。

函数调用栈是“在幕后起作用的”,它支持函数调用/返回机制。它还支持每个被掉函数的自动变量的创建、维护和销毁。

当调用每个函数时,它可能调用其他函数,而后者可能进而调用另外的函数,但所有的调用都是在返回前进行的。最终,每个函数都必须将控制返回给调用它的那个函数。

因此必须以某种方式跟踪每个函数的返回地址,以便将控制返回到它的调用者。函数调用栈是处理这个信息的绝佳数据结构。每次调用函数时,就会将一个项压入栈中。这个项称为栈帧(stack frame)或活动记录,它包含被调用函数返回到调用函数时所需的返回地址。如果被调函数返回,则会弹出这个函数调用的帧栈,且控制会转移到被弹出的帧栈所包含的返回地址。

调用栈的亮点在于每个被调函数都能够在调用栈的顶部找到返回到它的调用者时所需要的信息。而且,如果一个函数调用了另一个函数,则这个新函数的栈帧也会被简单地压入调用栈。因此,新的被调函数返回到它的调用者所需要的返回地址,就位于栈的顶部。

帧栈还有另外一个重要责任。大多数函数都有自动变量,包括参数及他说声明的所有局部变量。自动变量需要在函数执行时存在。如果函数调用了其他函数,则他们仍然需要保持活动状态。但是当被调函数返回到他的调用者后,它的自动变量需要“消失”。被调函数的帧栈是保存它的自动变量的理想场所。只要被调函数处于活动状态,它的帧栈就会存在。当函数返回时(此时不在需要它的局部自动变量),它的帧栈就从栈弹出,而这些局部变量不再为程序所知。

当然,计算机中的内存容量是有限的,因此只要一定数量的内存能够用于在函数调用栈上保存活动记录。如果发生的函数调用超出了函数调用栈上能容纳的活动记录,就会发生栈溢出(stack overflow)的错误。

实际使用函数调用栈

调用栈和活动记录支持函数调用/返回机制,也支持自动变量的创建和销毁。

下面以一个demo说明调用栈如何支持main所调用的square函数的操作(见图1第08-14行)。

首先:操作系统调用main,会将这一活动压入栈中(如下图1所示)。这个活动记录会告诉main函数如何返回到操作系统(即转移到返回地址R1) ,并包含main的自动变量(即初始化为10的a)所需的空间。

#include "stdafx.h"
#include <iostream>
using namespace std;

int square(int);


int _tmain(int argc, _TCHAR* argv[])
{
 int a = 10;
 cout << a << " squared: " << square(a) << endl;
 system("pause");
 return 0;
}

int square(int x)
{
 return x*x;
}
           
函数调用栈与活动记录

在返回操作系统之前,现在main函数在代码第11行调用square函数。这会导致square的一个帧(16~19行)(见图2)被压入函数调用栈(见图2)这个栈帧包含返回地址(即R2),使square函数可以返回到main函数,它还包括square的自动变量(即x)所需的内存。

在square计算出实参的平方之后,它需要返回到main,并且不再需要它的自动变量x的内存。因此,栈被弹出,向square提供main中的返回位置(即R2),并丢失square的自动变量,(图3)展示了square的活动记录被弹出之后的函数调用栈。

现在,main函数显示了调用square的结果(第11行),然后x执行return语句(13行)。这会使main的活动记录从栈弹出。它向main提供了返回到操作系统的地址(图1中的R1),并使的main的自动变量(即a)的内存不能再访问。

函数调用栈与活动记录
函数调用栈与活动记录

参考资料:摘自c++程序员教程  P138-P141  电子工业出版社    张良华 译

继续阅读