使用"函数调用"在堆栈上?

时间:2014-07-09 05:29:31

标签: c++ c function memory stack

main() calling f1(), f1() calling f2(), f2 calling f3(), f3() calling f4() and so on...

一个函数调用另一个函数并且链继续。

|        |
| f4()   |
| f3()   |
| f2()   |
| f1()   |
| main() |
__________

当调用某个函数时,会准备一个名为激活记录的结构,其中包含与其调用相关的信息。关于每个函数的激活记录被推送到名为程序堆栈运行时堆栈的堆栈。关于激活记录运行时堆栈,我不知道。我的问题是:

  1. 一个函数调用另一个函数并且链继续。但是持续多久?这个嵌套调用可以持续多久?他们依赖什么?是机器的 OS 位架构吗?

  2. 激活记录包含哪些内容?它的结构是什么样的?本地数据是否也传递给了这个?

  3. 为此类函数(具有多个嵌套调用或递归函数)的堆栈溢出设置了哪些参数?我的意思是如何提前知道以避免溢出

3 个答案:

答案 0 :(得分:2)

"激活记录"是一个抽象的概念,对于学术界对算法进行正式分析非常有用,而且对实际实现并不太关注。

真实系统具有函数参数,局部变量,保存的寄存器值,返回地址等。某些系统使用CPU寄存器来处理所有这些信息。

由于堆栈指针通常是保存的寄存器之一,因此在许多环境中,人们可以“走”"堆栈通过使用帧指针寄存器来定位堆栈指针和帧指针的保存值,因为它存在于调用者中,并根据需要重复。如果编译器设置如"帧指针省略"被激活(或者系统没有使用帧指针寄存器),这可以使用堆栈指针与调试元数据相结合来完成。

如果您正在编写这样的堆栈步行算法或计算总堆栈使用量,则需要关注堆栈布局。它还有助于理解堆栈变量中的缓冲区溢出如何导致覆盖返回地址"返回到libc"利用。

但是对于大多数用途,例如解释递归函数如何具有局部变量的多个实例,或者局部变量的别名(指针和引用)的生命周期,一堆激活记录的概念模型就足够了。

答案 1 :(得分:1)

如果您的函数在堆栈上创建局部变量,您将看到该变量的地址随着堆栈变深而变化。您可以通过跟踪该数字的变化来大致了解您已经消耗了多少堆栈。这样做是为了你的教育 - 而不是实际的代码。像这样:

#include <stdio.h>

int foo(void);
char *sp;

int main(void){
  char a;
  sp = &a;
  foo();
  return 0;
}

int foo(void) {
  char c;
  long depth;
  depth = sp - &c;
  if(depth < 1000) {
    printf("depth is %ld\n", depth);
    foo();
  }
  return 0;
}

在我的机器上,对于foo()的每次后续调用,堆栈似乎增加了48个字节。

答案 2 :(得分:0)

&#34;激活记录&#34;是&#34;堆栈框架的代名词&#34; - 我不得不这样看,因为我对这个词并不熟悉。

  1. 在大多数系统中,堆栈以最大内存地址开始,然后向下扩展。为堆栈分配一定量的内存(并为每个线程创建堆栈)。可以将帧添加到堆栈中,直到达到限制,然后它变为堆栈溢出。这通常只发生在递归函数调用自身太多次的情况下 - 非递归调用导致堆栈溢出非常罕见(一种可能是函数在堆栈上分配大量内存)。
  2. 堆栈框架的内容在维基百科(http://en.wikipedia.org/wiki/Call_stack)上详细说明 - 通常它包含函数调用的每个堆栈分配的返回地址和空间(又名&#34;本地&#34;或&#34;自动&#34;)变量。
  3. 通常,程序无法在运行时检查自己的调用堆栈。避免堆栈溢出的唯一方法是自己跟踪深度计数:

  4. function beginFoo() {
    
        foo(0);
    }
    
    function foo(int depth) {
        if( depth > 10 ) return; // avert possible stack-overflow
        foo(depth + 1);
    }