C运行时堆栈溢出

3
#include <stdio.h>

int doHello(){
    doHello();
}

int main(){
    doHello();
    printf("\nLeaving Main");
    return 0;
}

当您运行此程序时,程序在屏幕上没有打印出“Leaving Main”消息就退出了。这是堆栈溢出的情况,因此程序终止,但我在命令窗口中没有看到任何错误消息。(在Windows/Cygwin上运行)
Q1. 在doHello函数中我没有声明任何局部变量,但堆栈仍然被使用。这是因为
  • 返回值
  • 有关函数调用的信息被存储吗?

澄清

Q2. 如何调试此类问题?我不是要求调试我上面提到的无限循环。
例如:
#define SIZE 512*1024
void doOVerflow(){
   char str[SIZE];
   doHello();
}

void doHello(){
   char strHello[256];  // stack gets filled up at this point
   doNothing();         // program terminates and function doNothing does not get called
}

编辑:

Q3. 运行时栈存储了哪些信息?

6个回答

10
这通常涉及到帧指针返回地址,可以参考维基百科的“调用栈”文章。如果您感兴趣:
$ gcc -S test.c  # <--- assembles, but does not compile, test.c; output in test.s
$ cat test.s
// [some contents snipped]
_doHello:
        pushl   %ebp        // <--- pushes address of stack frame onto stack
        movl    %esp, %ebp  // <--- sets up new stack frame
        call    _doHello    // <--- pushes return value onto stack, makes call
        popl    %ebp        // <--- pops address of stack frame off stack
        ret                 // <--- pops return value off stack, returns to it

如果只是为了好玩,可以尝试使用“-fomit-frame-pointers”:

$ gcc -fomit-frame-pointers -S test.c
$ cat test.s
// [some contents snipped]
_doHello:
        call    _doHello   // <--- pushes return value onto stack, makes call
        ret                // <--- pops return value off stack, returns to it

为了更加有趣,让我们看看在启用优化时会发生什么:

$ gcc -fomit-frame-pointers -O4 -S test.c # <--- heavy optimization
$ cat test.s
// [some contents snipped]
_doHello:
L2:
        jmp     L2         // <--- no more stack operations!

最后一个版本将永远运行,而不是退出,至少在我的设置中(目前使用的是cygwin)。

要诊断此类问题,您可以在您喜欢的调试器中运行(例如Microsoft Visual C++或gdb),或者使用这些调试器通常生成的堆栈转储文件(.core或.stackdump文件)进行检查。

如果您的调试器支持,您还可以在堆栈顶部附近设置硬件断点 - 任何尝试写入此变量的操作都可能导致堆栈已满。一些操作系统具有额外的机制来警告您堆栈溢出。

最后,像valgrindApplication Verifier 这样的调试环境可能会有所帮助。


关于递归和调用栈的说明。 - pankajt

6
每次调用函数时,呼叫的地址都会被存储在堆栈中。

我必须同意这一点。但是,更详细的描述会更好。我在哪里可以找到这个? - pankajt

3

即使您没有任何本地变量,堆栈帧仍将包含返回地址。

注意:现代编译器在足够高的优化级别上应该能够执行尾调用优化,从而避免堆栈溢出。


我对此并没有太多的了解。我在 gcc 3.4.4 上运行了这个。 - pankajt
2
是的,它避免了堆栈溢出,但导致了无限循环 :-) - nothrow

2

这是因为a. 返回值 b. 存储函数调用信息的原因吗?

在你的情况下,消耗栈的是返回地址。

如何在程序中调试这种情况?

首先不要写这种递归函数。递归函数必须始终具有终止条件。

Q3. 运行时堆栈中存储哪些信息?

对于大多数语言而言,每个函数调用的本地变量和每个函数调用的返回地址。

当进行函数调用时,当前的执行点被推入堆栈,然后执行函数的代码。当执行结束时,堆栈上的值被弹出,并且执行路径返回到该点。在你的代码中,返回地址被推入堆栈,函数被调用 - 函数调用的第一件事是调用它自己,这意味着返回地址被推入堆栈,函数被调用,以此类推,直到发生堆栈溢出。


下一个要被调用的函数的信息也存储在堆栈中吗? - pankajt
不,不是。将要调用的mect函数将出现在进程的代码段(或类似段)中作为一个地址。 - anon
我也持有同样的观点。对此没有详细的了解。由于我没有指定任何返回值,我假设默认的返回值正在被使用并消耗堆栈。尽管Michiel的答案指向另一个方向,但我仍然不确定。 - pankajt
Michiel和我都是正确的 - 你误解了函数调用的工作方式。 - anon

1

反汇编并分析该函数。

让我们来看看你的第一段代码(保存为so.c):

int doHello(){
    doHello();
}

int main(){
    doHello();
    printf("\nLeaving Main");
    return 0;
}

编译并反汇编doHello()函数:

$ gcc -Wall -ggdb3 -O0 so.c -o so
$ gdb --annotate=3 so
(gdb) disassemble doHello
Dump of assembler code for function doHello:
0x080483e4 <doHello+0>: push   %ebp
0x080483e5 <doHello+1>: mov    %esp,%ebp
0x080483e7 <doHello+3>: sub    $0x8,%esp
0x080483ea <doHello+6>: call   0x80483e4 <doHello>
0x080483ef <doHello+11>:        leave
0x080483f0 <doHello+12>:        ret
End of assembler dump.

现在,我们已经有了该函数的汇编代码,可以清楚地看到它在做什么。事实上,问题的答案就摆在我们面前:第一条指令将一个双字(返回指针)推入堆栈。
希望这能澄清问题。
谢谢。

1

其他人已经解释了为什么会出现堆栈溢出的问题。

关于调试,还有一个额外的提示。大多数现代调试器都会在堆栈溢出时中断程序,并让您在调试器中查看调用堆栈。此时,您会看到同一个函数一遍又一遍地出现。如果您没有使用现代调试器来诊断此类问题,那么您会使自己不必要地陷入困境。


1
+1 - 人们也应该意识到,即使你有一个太蠢以至于不能显示堆栈跟踪的调试器,只需逐步执行代码通常就能很好地了解问题所在。这是90%以上软件问题的真相。这也是调试器的作用。 - Michael Burr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接