我知道无法捕获.NET中的StackOverflowExceptions,删除它们的进程,并且没有堆栈跟踪。这是正式记录的on MSDN。但是,我想知道这种行为背后的技术(或其他)原因是什么。所有MSDN都说:
在.NET Framework的早期版本中,您的应用程序可能会捕获 StackOverflowException对象(例如,要从中恢复) 无限递归)。但是,目前不鼓励这种做法 因为需要大量额外的代码来可靠地捕获a 堆栈溢出异常并继续执行程序。
这是什么“重要的附加代码”?这种行为还有其他记录的原因吗?即使我们无法捕获SOE,为什么我们至少不能获得堆栈跟踪?几个同事和我只是沉没了几个小时来调试一个生产StackOverflowException,它可能需要几分钟的堆栈跟踪,所以我想知道是否有充分的理由让我受苦。
答案 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
块。