为什么.NET抛出StackOverflowException时表现如此糟糕?

时间:2014-03-17 21:14:01

标签: c# .net stack-overflow

我知道无法捕获.NET中的StackOverflowExceptions,删除它们的进程,并且没有堆栈跟踪。这是正式记录的on MSDN。但是,我想知道这种行为背后的技术(或其他)原因是什么。所有MSDN都说:

  

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

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

3 个答案:

答案 0 :(得分:85)

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

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

此异常又被CLR拦截。此时,剩下大约3千字节的堆栈空间。例如,这不足以运行Just-in-time编译器(JITter)来编译可以处理程序中的异常的代码,JITter需要更多的空间。因此,CLR无法做任何其他事情,只能粗暴地中止该线程。并且通过.NET 2.0策略也终止了该过程。

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

如果在考虑在另一个主题上提出事件或取消winapi中的限制(保护页面的数量不可配置)时有任何努力,那么这是一个非常保密的秘密或只是没有被认为是有用的。我怀疑后者,不知道这个事实。

答案 1 :(得分:16)

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

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

答案 2 :(得分:7)

要正常处理堆栈溢出或内存不足的情况,必须在堆栈实际溢出或堆内存完全耗尽之前触发异常,此时可用堆栈和堆资源足够执行任何需要在捕​​获异常之前运行的清理代码。在堆栈溢出异常的情况下,干净地处理它们基本上需要在每个方法的入口处检查堆栈指针(这不应该是非常昂贵的)。通常情况下,他们通过在堆栈末端设置访问违规陷阱来处理它们,但这样做的问题是陷阱不会被解雇,直到它已经来不及处理事情干净利落。可以将陷阱设置为在堆栈的最后一个内存块上触发,而不是在过去的一个内存块上启动,并且一旦它触发并触发StackOverflowException,系统就会将陷阱更改为堆栈中的块,但问题是没有什么好方法可以确保"几乎没有堆栈"一旦堆栈解开那么远,陷阱就会重新启用。

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