为什么在抛出StackOverflowException时,.NET的行为如此糟糕?

我知道在.NET中的StackOverflowExceptions不能被捕获,取消他们的进程,并没有堆栈跟踪。 这在MSDN上正式logging。 不过,我想知道行为背后的技术(或其他)原因。 所有MSDN说是:

在以前版本的.NET Framework中,您的应用程序可能会捕获一个StackOverflowException对象(例如,从无限recursion中恢复)。 然而,这种做法目前是不鼓励的,因为需要大量额外的代码才能可靠地捕获堆栈溢出exception并继续执行程序。

这个“重要的附加代码”是什么? 这种行为是否有其他logging的原因? 即使我们不能抓住国有企业,为什么我们至less得不到一个堆栈跟踪呢? 几个同事,我只是沉没了几个小时,debugging生产StackOverflowException,这将需要几分钟的堆栈跟踪,所以我想知道是否有一个很好的理由,我的痛苦。

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

在属于线程堆栈的页面的情况下,这被内核拦截。 它改变了这两个页面中第一个的保护属性,从而给线程一些紧急的堆栈空间来处理事故,然后重新产生一个STATUS_STACK_OVERFLOWexception。

这个exception又被CLR拦截。 那时候剩下大约3千字节的堆栈空间。 这是一个不足以运行即时编译器(JITter)来编译可以处理程序中的exception的代码,JITter需要更多的空间。 因此,CLR不能做任何事情,只能粗暴地放弃这个线程。 而.NET 2.0策略也终止了这个过程。

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

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

该堆栈实际上是关于程序状态的所有内容都存储的地方。 当方法被调用时,每个返回地址的地址,局部variables,方法参数等。如果一个方法溢出堆栈,它的执行必须立即停止(因为没有更多的堆栈空间可以继续运行) 。 然后,为了恢复正常, 有人需要清理任何方法在堆栈死亡之前对堆栈进行的操作。 这意味着在调用方法之前知道栈的外观。 这招致了一些开销。

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

为了平稳地处理堆栈溢出或内存不足的情况,有必要在堆栈实际溢出或堆内存完全耗尽之前触发一个exception,此时可用的堆栈和堆资源足以执行在捕获exception之前需要运行的清理代码。 在堆栈溢出exception的情况下,干净地处理它们基本上需要检查每个方法入口处的堆栈指针(这应该不是那么昂贵)。 通常情况下,通过设置访问违规陷阱来处理它们,但是这样做的问题在于,陷阱不会被触发,直到处理事情已经太迟了。 人们可以设置陷阱在堆栈的最后一个内存块上触发,而不是一个过去,让系统将陷阱更改为通过堆栈的块,一旦触发并触发StackOverflowException ,但问题是会有没有很好的方法来确保一旦堆栈已经解开,那么“几乎走出堆叠”的陷阱被重新启用。

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