如何防止和/或处理StackOverflowException?

我想阻止或处理一个StackOverflowException,我从一个Xsl编辑器中调用XslCompiledTransform.Transform方法得到我写的。 问题似乎是用户可以写一个无限recursion的Xsl脚本,它只是在调用Transform方法的时候爆发了。 (也就是说,问题不仅仅是典型的程序错误,这通常是这种例外的原因。)

有没有办法检测和/或限制允许多lessrecursion? 或者任何其他的想法,以防止这些代码只是吹了我?

来自微软:

从.NET Framework 2.0版开始,StackOverflowException对象不能被try-catch块捕获,相应的进程默认是终止的。 因此,build议用户编写代码来检测和防止堆栈溢出。 例如,如果您的应用程序依赖于recursion,则使用计数器或状态条件来终止recursion循环。

我假设exception发生在一个内部的.NET方法,而不是在你的代码。

你可以做一些事情。

  • 编写代码检查xsl的无限recursion,并在应用转换之前通知用户(Ugh)。
  • 加载XslTransform代码到一个单独的进程(Hacky,但工作较less)。

您可以使用Process类加载将应用转换的程序集到单独的进程中,并在用户死亡时提醒用户,而不会中断主应用程序。

编辑:我刚刚testing,这是如何做到这一点:

MainProcess:

 // This is just an example, obviously you'll want to pass args to this. Process p1 = new Process(); p1.StartInfo.FileName = "ApplyTransform.exe"; p1.StartInfo.UseShellExecute = false; p1.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; p1.Start(); p1.WaitForExit(); if (p1.ExitCode == 1) Console.WriteLine("StackOverflow was thrown"); 

ApplyTransform过程:

 class Program { static void Main(string[] args) { AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); throw new StackOverflowException(); } // We trap this, we can't save the process, // but we can prevent the "ILLEGAL OPERATION" window static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { if (e.IsTerminating) { Environment.Exit(1); } } } 

注意 @WilliamJockusch的赏金问题和原来的问题是不同的。

这个答案是关于StackOverflow在第三方库的一般情况下,以及你可以/不能用它们做什么。 如果您使用XslTransform查看特殊情况,请参阅接受的答案。


由于堆栈中的数据超过了一定的限制(以字节为单位),因此会发生堆栈溢出。 这个检测工作的细节可以在这里find。

我想知道是否有一个通用的方法来追踪StackOverflowExceptions。 换句话说,假设我在代码中有无限的recursion,但是我不知道在哪里。 我想通过一些方法来追踪它,而不是逐步遍历整个地方的代码,直到我看到它发生。 我不在乎它是多么的黑客。

正如我在链接中提到的,从静态代码分析中检测堆栈溢出将需要解决不可判定的暂停问题。 现在我们已经确定没有银弹了 ,我可以告诉你一些我认为有助于追踪问题的技巧。

我认为这个问题可以用不同的方式来解释,因为我有点无聊:-),我会把它分解成不同的变体。

在testing环境中检测堆栈溢出

基本上这里的问题是你有一个(有限的)testing环境,并且想要在(扩展的)生产环境中检测堆栈溢出。

我不用检测SO本身,而是利用可以设置堆栈深度的事实来解决这个问题。 debugging器会给你所有你需要的信息。 大多数语言允许您指定堆栈大小或最大recursion深度。

基本上我试图通过使堆栈的深度尽可能小来强制SO。 如果它不溢出,我总是可以使其更大(=在这种情况下:更安全)的生产环境。 当你得到一个堆栈溢出的时候,你可以手动决定它是否是“有效”的。

为此,将堆栈大小(在我们的例子中:一个小的值)传递给一个Thread参数,看看会发生什么。 .NET中默认的堆栈大小是1MB,我们将使用一个较小的值:

 class StackOverflowDetector { static int Recur() { int variable = 1; return variable + Recur(); } static void Start() { int depth = 1 + Recur(); } static void Main(string[] args) { Thread t = new Thread(Start, 1); t.Start(); t.Join(); Console.WriteLine(); Console.ReadLine(); } } 

注意:我们也将使用下面的代码。

一旦溢出,你可以设置一个更大的值,直到你得到一个有意义的SO。

在你之前创buildexception

StackOverflowException是不可捕获的。 这意味着发生这种事情时你可以做的事情不多。 所以,如果你相信代码中的某些东西肯定会出错的话,那么在某些情况下你可以做出自己的exception。 你唯一需要的就是当前的堆栈深度。 不需要计数器,可以使用.NET的实际值:

 class StackOverflowDetector { static void CheckStackDepth() { if (new StackTrace().FrameCount > 10) // some arbitrary limit { throw new StackOverflowException("Bad thread."); } } static int Recur() { CheckStackDepth(); int variable = 1; return variable + Recur(); } static void Main(string[] args) { try { int depth = 1 + Recur(); } catch (ThreadAbortException e) { Console.WriteLine("We've been a {0}", e.ExceptionState); } Console.WriteLine(); Console.ReadLine(); } } 

请注意,如果您正在处理使用callback机制的第三方组件,则此方法也适用。 唯一需要的是您可以拦截堆栈跟踪中的一些调用。

在单独的线程中检测

你明确提出这个,所以这里是这个。

你可以尝试在一个单独的线程中检测一个SO,但它可能不会对你有任何好处。 甚至在获得上下文切换之前,堆栈溢出可能会发生得很快 。 这意味着这种机制是不可靠的… 我不会推荐实际使用它 。 虽然build立起来很有趣,所以这是代码:-)

 class StackOverflowDetector { static int Recur() { Thread.Sleep(1); // simulate that we're actually doing something :-) int variable = 1; return variable + Recur(); } static void Start() { try { int depth = 1 + Recur(); } catch (ThreadAbortException e) { Console.WriteLine("We've been a {0}", e.ExceptionState); } } static void Main(string[] args) { // Prepare the execution thread Thread t = new Thread(Start); t.Priority = ThreadPriority.Lowest; // Create the watch thread Thread watcher = new Thread(Watcher); watcher.Priority = ThreadPriority.Highest; watcher.Start(t); // Start the execution thread t.Start(); t.Join(); watcher.Abort(); Console.WriteLine(); Console.ReadLine(); } private static void Watcher(object o) { Thread towatch = (Thread)o; while (true) { if (towatch.ThreadState == System.Threading.ThreadState.Running) { towatch.Suspend(); var frames = new System.Diagnostics.StackTrace(towatch, false); if (frames.FrameCount > 20) { towatch.Resume(); towatch.Abort("Bad bad thread!"); } else { towatch.Resume(); } } } } } 

在debugging器中运行这个,玩得开心。

使用堆栈溢出的特性

您的问题的另一个解释是:“哪些代码可能会导致堆栈溢出exception?”。 显然这个答案是:recursion的所有代码。 对于每一段代码,你可以做一些手动分析。

也可以使用静态代码分析来确定。 你需要做的是反编译所有的方法,并找出它们是否包含无限recursion。 这里有一些代码可以帮你:

 // A simple decompiler that extracts all method tokens (that is: call, callvirt, newobj in IL) internal class Decompiler { private Decompiler() { } static Decompiler() { singleByteOpcodes = new OpCode[0x100]; multiByteOpcodes = new OpCode[0x100]; FieldInfo[] infoArray1 = typeof(OpCodes).GetFields(); for (int num1 = 0; num1 < infoArray1.Length; num1++) { FieldInfo info1 = infoArray1[num1]; if (info1.FieldType == typeof(OpCode)) { OpCode code1 = (OpCode)info1.GetValue(null); ushort num2 = (ushort)code1.Value; if (num2 < 0x100) { singleByteOpcodes[(int)num2] = code1; } else { if ((num2 & 0xff00) != 0xfe00) { throw new Exception("Invalid opcode: " + num2.ToString()); } multiByteOpcodes[num2 & 0xff] = code1; } } } } private static OpCode[] singleByteOpcodes; private static OpCode[] multiByteOpcodes; public static MethodBase[] Decompile(MethodBase mi, byte[] ildata) { HashSet<MethodBase> result = new HashSet<MethodBase>(); Module module = mi.Module; int position = 0; while (position < ildata.Length) { OpCode code = OpCodes.Nop; ushort b = ildata[position++]; if (b != 0xfe) { code = singleByteOpcodes[b]; } else { b = ildata[position++]; code = multiByteOpcodes[b]; b |= (ushort)(0xfe00); } switch (code.OperandType) { case OperandType.InlineNone: break; case OperandType.ShortInlineBrTarget: case OperandType.ShortInlineI: case OperandType.ShortInlineVar: position += 1; break; case OperandType.InlineVar: position += 2; break; case OperandType.InlineBrTarget: case OperandType.InlineField: case OperandType.InlineI: case OperandType.InlineSig: case OperandType.InlineString: case OperandType.InlineTok: case OperandType.InlineType: case OperandType.ShortInlineR: position += 4; break; case OperandType.InlineR: case OperandType.InlineI8: position += 8; break; case OperandType.InlineSwitch: int count = BitConverter.ToInt32(ildata, position); position += count * 4 + 4; break; case OperandType.InlineMethod: int methodId = BitConverter.ToInt32(ildata, position); position += 4; try { if (mi is ConstructorInfo) { result.Add((MethodBase)module.ResolveMember(methodId, mi.DeclaringType.GetGenericArguments(), Type.EmptyTypes)); } else { result.Add((MethodBase)module.ResolveMember(methodId, mi.DeclaringType.GetGenericArguments(), mi.GetGenericArguments())); } } catch { } break; default: throw new Exception("Unknown instruction operand; cannot continue. Operand type: " + code.OperandType); } } return result.ToArray(); } } class StackOverflowDetector { // This method will be found: static int Recur() { CheckStackDepth(); int variable = 1; return variable + Recur(); } static void Main(string[] args) { RecursionDetector(); Console.WriteLine(); Console.ReadLine(); } static void RecursionDetector() { // First decompile all methods in the assembly: Dictionary<MethodBase, MethodBase[]> calling = new Dictionary<MethodBase, MethodBase[]>(); var assembly = typeof(StackOverflowDetector).Assembly; foreach (var type in assembly.GetTypes()) { foreach (var member in type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance).OfType<MethodBase>()) { var body = member.GetMethodBody(); if (body!=null) { var bytes = body.GetILAsByteArray(); if (bytes != null) { // Store all the calls of this method: var calls = Decompiler.Decompile(member, bytes); calling[member] = calls; } } } } // Check every method: foreach (var method in calling.Keys) { // If method A -> ... -> method A, we have a possible infinite recursion CheckRecursion(method, calling, new HashSet<MethodBase>()); } } 

现在,一个方法循环包含recursion的事实绝不是保证堆栈溢出将会发生 – 这只是堆栈溢出exception的最可能的先决条件。 简而言之,这意味着这段代码将决定堆栈溢出可能发生的代码片段,这将大大缩小大部分代码。

还有其他方法

还有其他一些方法可以尝试,我在这里没有描述。

  1. 通过托pipeCLRstream程并处理它来处理堆栈溢出。 请注意,您仍然无法“捕捉”它。
  2. 改变所有的IL代码,build立另一个DLL,添加recursion检查。 是的,这是很有可能的(我已经在过去实现它:-); 这只是困难的,并涉及到很多代码才能正确使用。
  3. 使用.NET分析API捕获所有的方法调用,并使用它来找出堆栈溢出。 例如,您可以执行检查,如果在呼叫树中X次遇到相同的方法,则会发出一个信号。 这里有一个项目会给你一个开始。

我build议围绕XmlWriter对象创build一个包装器,这样就可以对WriteStartElement / WriteEndElement的调用数量进行计数,而且如果将标签数量限制为某个数字(fe 100),则可以引发不同的exception, InvalidOperation。

这应该可以解决大多数情况下的问题

 public class LimitedDepthXmlWriter : XmlWriter { private readonly XmlWriter _innerWriter; private readonly int _maxDepth; private int _depth; public LimitedDepthXmlWriter(XmlWriter innerWriter): this(innerWriter, 100) { } public LimitedDepthXmlWriter(XmlWriter innerWriter, int maxDepth) { _maxDepth = maxDepth; _innerWriter = innerWriter; } public override void Close() { _innerWriter.Close(); } public override void Flush() { _innerWriter.Flush(); } public override string LookupPrefix(string ns) { return _innerWriter.LookupPrefix(ns); } public override void WriteBase64(byte[] buffer, int index, int count) { _innerWriter.WriteBase64(buffer, index, count); } public override void WriteCData(string text) { _innerWriter.WriteCData(text); } public override void WriteCharEntity(char ch) { _innerWriter.WriteCharEntity(ch); } public override void WriteChars(char[] buffer, int index, int count) { _innerWriter.WriteChars(buffer, index, count); } public override void WriteComment(string text) { _innerWriter.WriteComment(text); } public override void WriteDocType(string name, string pubid, string sysid, string subset) { _innerWriter.WriteDocType(name, pubid, sysid, subset); } public override void WriteEndAttribute() { _innerWriter.WriteEndAttribute(); } public override void WriteEndDocument() { _innerWriter.WriteEndDocument(); } public override void WriteEndElement() { _depth--; _innerWriter.WriteEndElement(); } public override void WriteEntityRef(string name) { _innerWriter.WriteEntityRef(name); } public override void WriteFullEndElement() { _innerWriter.WriteFullEndElement(); } public override void WriteProcessingInstruction(string name, string text) { _innerWriter.WriteProcessingInstruction(name, text); } public override void WriteRaw(string data) { _innerWriter.WriteRaw(data); } public override void WriteRaw(char[] buffer, int index, int count) { _innerWriter.WriteRaw(buffer, index, count); } public override void WriteStartAttribute(string prefix, string localName, string ns) { _innerWriter.WriteStartAttribute(prefix, localName, ns); } public override void WriteStartDocument(bool standalone) { _innerWriter.WriteStartDocument(standalone); } public override void WriteStartDocument() { _innerWriter.WriteStartDocument(); } public override void WriteStartElement(string prefix, string localName, string ns) { if (_depth++ > _maxDepth) ThrowException(); _innerWriter.WriteStartElement(prefix, localName, ns); } public override WriteState WriteState { get { return _innerWriter.WriteState; } } public override void WriteString(string text) { _innerWriter.WriteString(text); } public override void WriteSurrogateCharEntity(char lowChar, char highChar) { _innerWriter.WriteSurrogateCharEntity(lowChar, highChar); } public override void WriteWhitespace(string ws) { _innerWriter.WriteWhitespace(ws); } private void ThrowException() { throw new InvalidOperationException(string.Format("Result xml has more than {0} nested tags. It is possible that xslt transformation contains an endless recursive call.", _maxDepth)); } } 

如果您的应用程序依赖于三方代码(在Xsl脚本中),那么您必须首先决定是否要防御其中的错误。 如果你真的想捍卫,那么我认为你应该执行你的逻辑,容易在单独的AppDomains中的外部错误。 捕捉StackOverflowException并不好。

也检查这个问题 。

我今天有了一个stackoverflow,我读了你的一些post,并决定帮助垃圾收集器。

我曾经有这样一个近乎无限的循环:

  class Foo { public Foo() { Go(); } public void Go() { for (float i = float.MinValue; i < float.MaxValue; i+= 0.000000000000001f) { byte[] b = new byte[1]; // Causes stackoverflow } } } 

相反,让资源耗尽这样的范围:

 class Foo { public Foo() { GoHelper(); } public void GoHelper() { for (float i = float.MinValue; i < float.MaxValue; i+= 0.000000000000001f) { Go(); } } public void Go() { byte[] b = new byte[1]; // Will get cleaned by GC } // right now } 

它为我工作,希望它可以帮助别人。

使用.NET 4.0您可以将System.Runtime.ExceptionServices的HandleProcessCorruptedStateExceptions属性添加到包含try / catch块的方法中。 这真的起作用了! 也许不推荐,但工程。

 using System; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.ExceptionServices; namespace ExceptionCatching { public class Test { public void StackOverflow() { StackOverflow(); } public void CustomException() { throw new Exception(); } public unsafe void AccessViolation() { byte b = *(byte*)(8762765876); } } class Program { [HandleProcessCorruptedStateExceptions] static void Main(string[] args) { Test test = new Test(); try { //test.StackOverflow(); test.AccessViolation(); //test.CustomException(); } catch { Console.WriteLine("Caught."); } Console.WriteLine("End of program"); } } } 

这个答案是@WilliamJockusch。

我想知道是否有一个通用的方法来追踪StackOverflowExceptions。 换句话说,假设我在代码中有无限的recursion,但是我不知道在哪里。 我想通过一些方法来追踪它,而不是逐步遍历整个地方的代码,直到我看到它发生。 我不在乎它是多么的黑客。 例如,如果有一个模块可以激活,甚至可以从另一个线程调用堆栈深度,并且如果达到我认为“太高”的级别,就会抱怨。 例如,我可能把“太高”设置为600帧,认为如果堆栈太深,那就是一个问题。 是这样的可能性。 另一个例子是将我的代码中的每个第1000个方法调用logging到debugging输出中。 这将得到一些证据显示过度的机会是相当不错的,它可能不会太糟糕的输出。 关键在于,无论溢价发生在哪里,都无法写入支票。 因为整个问题是我不知道那里是什么。 解决scheme最好不要依赖于我的开发环境。 即它不应该假定我通过特定的工具集(例如VS)使用C#。

这听起来像你热衷于听到一些debugging技术来抓住这个StackOverflow,所以我想我会分享一对夫妇为你尝试。

1.内存转储。

临的 :内存转储是一个确定的火灾方式来找出堆栈溢出的原因。 AC#MVP&我一起工作,解决了一个SO,然后他在这里博客了。

这种方法是追踪问题的最快方法。

此方法不会要求您通过以下在日志中看到的步骤重现问题。

Con的 :内存转储非常大,你必须附加AdPlus / procdump的过程。

2.面向方面的编程。

Pro's :这可能是实现代码的最简单方法,它可以从任何方法中检查调用堆栈的大小,而无需在应用程序的每个方法中编写代码。 有一堆AOP框架可以让你在通话之前和之后进行拦截。

会告诉你造成堆栈溢出的方法。

允许您检查应用程序中所有方法的入口和出口处的StackTrace().FrameCount

Con's :它会对性能产生影响 – 每种方法都会embedded到IL中,而且你不能真正的“去激活”它。

这有点取决于你的开发环境工具集。

3.logging用户活动。

一个星期前,我试图寻找几个难以重现的问题。 我发布了这个QA 用户活动logging,遥测(和variables在全局exception处理程序) 。 我得出的结论是一个非常简单的用户操作logging器,以查看在发生未处理的exception时如何在debugging器中重现问题。

亲的 :你可以打开或closures(即订阅事件)。

跟踪用户操作不需要拦截每个方法。

与AOP相比,您可以更简单地计算方法订阅的事件数量。

日志文件相对较小,并侧重于您需要执行哪些操作来重现问题。

它可以帮助您了解用户如何使用您的应用程序。

Con的 :不适合Windows服务,我敢肯定,有这样的networking应用程序更好的工具

不一定告诉你导致堆栈溢出的方法。

要求您手动重现问题而不是手动重现问题,而不是在内存转储处获取并马上进行debugging。


也许你可能会尝试我上面提到的所有技术和@atlaste发布的一些技术,并告诉我们哪个是你发现的最简单/最快/最肮脏/最可接受的在PROD环境中运行/等等。

无论如何,祝你好运跟踪这个SO。

通过它的外观,除了开始另一个进程,似乎没有任何处理StackOverflowException 。 在别人问之前,我尝试了使用AppDomain ,但是这不起作用:

 using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading; namespace StackOverflowExceptionAppDomainTest { class Program { static void recrusiveAlgorithm() { recrusiveAlgorithm(); } static void Main(string[] args) { if(args.Length>0&&args[0]=="--child") { recrusiveAlgorithm(); } else { var domain = AppDomain.CreateDomain("Child domain to test StackOverflowException in."); domain.ExecuteAssembly(Assembly.GetEntryAssembly().CodeBase, new[] { "--child" }); domain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) => { Console.WriteLine("Detected unhandled exception: " + e.ExceptionObject.ToString()); }; while (true) { Console.WriteLine("*"); Thread.Sleep(1000); } } } } } 

但是,如果最终使用单独进程解决scheme,则build议使用Process.ExitedProcess.StandardOutput并自己处理错误,以便为用户提供更好的体验。

@WilliamJockusch,如果我正确地理解了你的担心,从math的angular度来看,不可能总是识别无限recursion,因为这意味着要解决停机问题 。 为了解决这个问题,你需要一个超级recursionalgorithm (比如试用错误谓词 )或者一个可以超计算的机器( 本书的一个例子在下面的章节中有介绍)。

从实际的angular度来看,你必须知道:

  • 在给定的时间你有多less堆栈内存
  • recursion方法在给定时间需要多less堆栈内存才能达到特定的输出。

请记住,使用当前的机器,由于多任务处理,这些数据是非常可变的,而且我还没有听说过这个任务的软件。

让我知道,如果有什么不清楚。

您可以每隔几个调用Environment.StackTrace读取此属性,并且如果堆栈跟踪超出了您预设的特定阈值,则可以返回该函数。

你也应该尝试用循环来replace一些recursion函数。