【问题标题】:Why does .NET behave so poorly when StackOverflowException is thrown?为什么抛出 StackOverflowException 时 .NET 的行为如此糟糕?
【发布时间】:2014-03-17 21:14:00
【问题描述】:

我知道 .NET 中的 StackOverflowExceptions 无法被捕获、取消其进程并且没有堆栈跟踪。这是官方记录的on MSDN。但是,我想知道行为背后的技术(或其他)原因是什么。所有 MSDN 都说:

在 .NET Framework 的早期版本中,您的应用程序可能会捕获 StackOverflowException 对象(例如,从 无限递归)。但是,目前不鼓励这种做法 因为需要大量额外的代码才能可靠地捕获 堆栈溢出异常并继续程序执行。

这个“重要的附加代码”是什么?这种行为还有其他记录在案的原因吗?即使我们无法捕获 SOE,为什么我们至少不能获得堆栈跟踪?我和几个同事只花了几个小时调试一个生产 StackOverflowException,而这需要几分钟的时间来跟踪堆栈跟踪,所以我想知道我的痛苦是否有充分的理由。

【问题讨论】:

  • "栈上没有更多可用空间了。快,把必要的额外数据放到栈上,让我们能够抛出异常,让它记录相关信息,并找到并调用适当的处理程序"
  • @jalf 这实际上是这个问题最容易克服的方面(RT 可以简单地设置一个接近实际堆栈大小的“软限制”,这样可以保证如果一个发生软溢出)。
  • @HansPassant 您应该认真考虑用该信息回答问题。很棒的东西。
  • 只有我一个人担心在stackoverflow上询问堆栈溢出会导致宇宙崩溃吗?
  • 相关 Java 问题:Why does this method print 4?Understanding java stack - 显示从 StackOverflow 恢复时可能发生的情况 - 额外的方法调用会导致额外的堆栈溢出错误,而不是处于良好状态。当然,Java 不是 .Net,但我认为它很有趣。

标签: c# .net stack-overflow


【解决方案1】:

线程的堆栈是由 Windows 创建的。它使用所谓的保护页面来检测堆栈溢出。如this MSDN Library article 中所述,通常可用于用户模式代码的功能。基本思想是堆栈的最后两页(2 x 4096 = 8192 字节)被保留,并且任何处理器访问它们都会触发一个页面错误,该页面错误会变成一个 SEH 异常,STATUS_GUARD_PAGE_VIOLATION。 p>

如果这些页面属于线程堆栈,则内核会拦截。它更改了这两个页面中第一个页面的保护属性,从而为线程提供了一些紧急堆栈空间来处理事故,然后重新引发 STATUS_STACK_OVERFLOW 异常。

这个异常又被 CLR 拦截。那时还剩下大约 3 KB 的堆栈空间。一方面,这不足以运行Just-in-time 编译器(JITter)来编译可以处理程序中异常的代码,JITter 需要更多的空间。因此,CLR 只能粗鲁地中止线程。并且通过 .NET 2.0 策略也终止了进程。

请注意,这在 Java 中问题不大,它有一个字节码解释器,因此可以保证可执行的用户代码可以运行。或者在用 C、C++ 或 Delphi 等语言编写的非托管程序中,代码是在构建时生成的。然而,这仍然是一个非常难以处理的事故,堆栈中的紧急空间被破坏了,因此没有继续在线程上运行代码是安全的情况。一个程序可以在一个完全随机的位置中止的线程和相当损坏的状态继续正确运行的可能性是非常不可能的。

如果有任何努力考虑在另一个线程上引发事件或删除 winapi 中的限制(保护页面的数量不可配置),那么这要么是一个非常保守的秘密,要么就是没有认为有用。我怀疑是后者,事实并非如此。

【讨论】:

  • +1 表示“程序可以继续正确运行的可能性,线程在完全随机的位置中止并且相当损坏的状态不太可能”单独。如果人们只会得到这个 - 无论导致这种情况的情况如何。
  • 这正是我正在寻找的信息。知道为什么我们至少不能获得异常的堆栈跟踪吗?
  • 我不买。 OP 以引号开头,表明它曾经是可能的,所以真正的问题是发生了什么变化。
  • Hmya,CLR v1.x 的策略是让线程在没有 AppDomain.UnhandledException 回调的情况下死掉,被广泛鄙视。当微软的 Windows 团队试图在 Longhorn 中使用托管代码时,他们非常讨厌托管代码。
  • 另外,探索 Mono 如何处理它可能是一个有趣的练习。
【解决方案2】:

堆栈几乎是存储有关程序状态的所有内容的地方。调用方法时每个返回站点的地址、局部变量、方法参数等。如果方法溢出堆栈,它的执行必须立即停止(因为没有更多的堆栈空间让它继续运行)。然后,为了优雅地恢复,某人需要清理该方法在堆栈死亡之前对堆栈所做的一切。这意味着在调用方法之前知道堆栈的样子。这会产生一些开销。

如果您无法清理堆栈,那么您也无法获得堆栈跟踪,因为生成跟踪所需的信息来自“展开”堆栈以发现调用了哪些方法。

【讨论】:

  • @pasty 堆栈未包装。托管堆栈的存储在创建线程时分配和提交。没有在运行时扩展它的选项。
  • 为什么不直接销毁当前溢出堆栈的线程呢?为什么要杀死整个进程?
  • @BobAlbright 因为这样做意味着进程将处于未定义状态。
  • Java 处理这个......我看不出 .NET 不能这样做的原因。我也不明白为什么它不能退回到最后一个非溢出堆栈帧并从那里正常(例外)继续。
  • @dvnrrs:这是我问题的本质。为什么 .NET 决定不优雅地处理 SO?
【解决方案3】:

为了优雅地处理堆栈溢出或内存不足的情况,有必要在堆栈实际溢出或堆内存完全耗尽之前触发异常,此时可用的堆栈和堆资源将足够执行任何需要在捕​​获异常之前运行的清理代码。在堆栈溢出异常的情况下,干净地处理它们基本上需要检查每个方法的入口处的堆栈指针(这不应该真的那么昂贵)。通常,它们是通过在堆栈末尾设置访问冲突陷阱来处理的,但这样做的问题是陷阱不会触发,直到已经为时已晚而无法干净地处理事情。可以将陷阱设置为在堆栈的最后一个内存块上触发,而不是在过去的一个内存块上触发,并让系统在触发并触发StackOverflowException 时将陷阱更改为堆栈后面的块,但问题就在那里一旦堆栈展开到那么远,确保重新启用“几乎超出堆栈”陷阱将不是一个好方法。

话虽如此,另一种方法是允许线程设置一个委托来处理如果线程破坏它的堆栈会发生什么,然后说在StackOverflowException的情况下,线程的堆栈将被清除并且它将运行提供的委托。可以在运行委托之前重新设置陷阱(此时堆栈将为空),并且代码可以维护一个线程状态对象,委托可以使用该对象来了解是否跳过了任何重要的 finally 块。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-05-10
    • 2010-11-19
    • 2010-09-06
    • 1970-01-01
    • 2015-11-29
    • 1970-01-01
    • 2012-11-16
    相关资源
    最近更新 更多