堆栈溢出错误或异常?

发布于 2024-10-27 22:19:55 字数 101 浏览 1 评论 0原文

为什么下面的结果没有错误呢?

void func()
{
   func();
}

int main()
{
   func();
}

Why does the following ends up with no error?

void func()
{
   func();
}

int main()
{
   func();
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

初相遇 2024-11-03 22:19:55

理论上,它会溢出堆栈(因为,即使没有使用局部变量,每次调用都会将之前的返回地址添加到堆栈上); 实际上,启用优化后,由于尾调用优化,它不会溢出,这实际上避免了在跳转中转换调用的任何资源消耗,因此不消耗堆栈。

通过检查OP代码生成的优化程序集可以轻松看出这一点:

func():
.L2:
        jmp     .L2
main:
.L4:
        jmp     .L4

func 被优化为无限循环,无论是“独立版本”还是 main 中的内联调用。

请注意,这与 C++ 标准的“好像”规则是一致的:编译的程序必须运行就像它是您在代码中所请求的(就效果而言),并且由于堆栈size 只是一种实现限制,使用 call 生成的代码和使用 jmp 生成的代码是等效的。

但是:这是一个更特殊的情况,因为标准甚至说无限循环(定义为 “不终止并且没有副作用”)实际上是未定义的行为,因此理论上编译器可以完全省略该调用。

In theory, it would overflow the stack (because, even if no local variables are used, each call would add the previous return address on the stack); in practice, with optimizations enabled, it doesn't overflow because of tail call optimization, which actually avoids any resource consumption transforming the call in a jump, thus not consuming the stack.

This can be easily seen by examining the optimized assembly generated by the OP code:

func():
.L2:
        jmp     .L2
main:
.L4:
        jmp     .L4

func is optimized to an infinite loop, both the "freestanding version" and the inlined call in main.

Notice that this is coherent with the C++ standard for the "as if" rule: the compiled program must run as if it were what you requested in the code (in terms of effect), and since the stack size is just an implementation limit, the generated code that uses a call and the one that uses a jmp are equivalent.

But: this is an even more particular case, as the standard even says that infinite looping (defined as "not terminating and not having some side-effect") is actually undefined behavior, so in theory the compiler would be allowed to omit that call entirely.

蒲公英的约定 2024-11-03 22:19:55

您的编译器很可能对其进行了优化,并将其转换为 while(true){} 构造。

Likely, your compiler optimized it away and turned it into a while(true){} construct.

不念旧人 2024-11-03 22:19:55

在我的 Linux 系统上,它确实分段错误结束 - Valgrind 表明可能存在堆栈溢出,这当然是正确的,因为每个函数调用都需要一个新的堆栈帧。

但是,在编译器中启用优化会将整个程序减少为无限循环,这自然不会结束:

        .file   "so.c"
        .text
        .p2align 4,,15
.globl func
        .type   func, @function
func:
.LFB0:
        .cfi_startproc
        .p2align 4,,10
        .p2align 3
.L2:
        jmp     .L2
        .cfi_endproc
.LFE0:
        .size   func, .-func
        .p2align 4,,15
.globl main
        .type   main, @function
main:
.LFB1:
        .cfi_startproc
        .p2align 4,,10
        .p2align 3
.L5:
        jmp     .L5
        .cfi_endproc
.LFE1:
        .size   main, .-main
        .ident  "GCC: (GNU) 4.4.3"
        .section        .note.GNU-stack,"",@progbits

这是有趣的部分:

.L5:
        jmp     .L5

It does end with a Segmentation fault on my Linux system - Valgrind indicates a possible stack overflow, which is of course true, since for each function call a new stack frame is required.

However, enabling optimisations in the compiler reduces this whole program to an infinite loop, which, naturally, does not end at all:

        .file   "so.c"
        .text
        .p2align 4,,15
.globl func
        .type   func, @function
func:
.LFB0:
        .cfi_startproc
        .p2align 4,,10
        .p2align 3
.L2:
        jmp     .L2
        .cfi_endproc
.LFE0:
        .size   func, .-func
        .p2align 4,,15
.globl main
        .type   main, @function
main:
.LFB1:
        .cfi_startproc
        .p2align 4,,10
        .p2align 3
.L5:
        jmp     .L5
        .cfi_endproc
.LFE1:
        .size   main, .-main
        .ident  "GCC: (GNU) 4.4.3"
        .section        .note.GNU-stack,"",@progbits

Here's the interesting part:

.L5:
        jmp     .L5
风启觞 2024-11-03 22:19:55

如果您在 Windows 上的命令窗口中编译并运行它,您可能会崩溃,但操作系统不会发出任何注释。 (我们构建了一个有趣的编译器并经常遇到这个问题)。微软的说法是,当程序做了非常糟糕的事情时,它们无法恢复......所以他们只是终止进程并重新启动命令提示符。就像您的情况一样,在递归到堆栈限制后,当陷阱处理程序尝试执行某些操作(例如在堆栈上推送陷阱状态)时,没有任何空间,Windows 会终止您的进程。

我个人认为这是不可原谅的行为。如果我的进程做了坏事,操作系统应该总是抱怨。它可能会说,“进程因偏见而终止”,以及某种指示(“你在最后的错误处理程序中耗尽了堆栈”),但它应该说些什么。

Multics 在 1966 年就做到了这一点。真是可惜我们已经有 40 多年没有应用这些经验教训了。

If you are compiling and running this on Windows in a command window, you may get a crash but without any remarks from the OS. (We build a funny compiler and run into this problem a lot). Microsoft's claim is that when program does very bad things, they can't recover... so they simply kill the process and restart the command prompt. LIkely in your case, after you've recursed to the stack limit, when the trap handler attempts to do something (like push trap status on the stack) there isn't any space and Windows kills your process.

I personally think this is inexcusable behavior. If my process does something bad, the OS should always complain. It might say, "process terminated with prejudice", along with some kind of indication ("you ran out of stack in the last-ditch error handler") but it should say something.

Multics got this right in 1966. Its a shame we haven't applied these lessons in over 40 years.

走走停停 2024-11-03 22:19:55

在我的机器上,它以段错误结束(就像无限递归应该的那样)。

也许您的 shell 没有报告段错误。您使用什么操作系统?

On my machine it ends with a segfault (like infinite recursion should).

Maybe your shell isn't reporting the segfault. What OS are you using?

鹿! 2024-11-03 22:19:55

回到过去,当您想要过度优化 ASM 程序时,有一种做法:有时一个函数会以调用另一个函数(然后返回)结束。它看起来像这样:

somefunc:

    ; do some things

    CALL someotherfunc
    RET

someotherfunc:

    ; do some other things

    RET

这样,当CALL someotherfunc发生时,下一条指令的地址(RET)被保存到堆栈中,然后someotherfunc > 返回只是为了执行返回。使用 JMPsomeotherfunc 可以获得完全相同的结果。这样堆栈将不包含最后一条指令的地址,但它将包含原始调用者的地址。因此,当 someotherfunc 使其变为 RET 时,程序将在原始调用者处继续。

因此,优化后的代码将如下所示:

somefunc:

    ; do some things

    JMP someotherfunc

someotherfunc:

    ; do some other things

    RET

如果 somefunc 将自身作为最后一条指令调用(实际上这是唯一的指令),它确实会如下所示:

somefunc:

    JMP somefunc

Back in the olden days, when you wanted to over-optimize an ASM program there was a practice: may times a function ended with calling an other function (then returning). It would look something like:

somefunc:

    ; do some things

    CALL someotherfunc
    RET

someotherfunc:

    ; do some other things

    RET

This way when CALL someotherfunc happened the address of the next instruction (the RET) is saved into the stack and then someotherfunc returned just to execute the a return. Exactly the same results can be achieved with a JMP to someotherfunc. This way the stack will not contain the address of the last instruction, but it will contain the original caller's address. So when someotherfunc makes it's RET the program will continue at the original caller.

So the optimized code would look like:

somefunc:

    ; do some things

    JMP someotherfunc

someotherfunc:

    ; do some other things

    RET

And if somefunc is calling itself as the last instruction (in fact that is the only instruction), it would indeed look like:

somefunc:

    JMP somefunc
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文