如何防止和/或处理 StackOverflow 异常?

我想要或者阻止或者处理一个 StackOverflowException,我正在从一个 Xsl Editor内的 XslCompiledTransform.Transform方法的调用中得到它。问题似乎在于,用户可以编写一个无限递归的 Xsl script,而且它只是在调用 Transform方法时崩溃。(也就是说,问题不仅仅是典型的编程错误,这通常是导致这种异常的原因。)

是否有办法检测和/或限制允许的递归数量?还有什么办法能阻止这个代码爆炸吗?

164352 次浏览

来自微软:

从.NETFramework 开始 版本2.0,一个 StackOverflow 异常 对象不能被 try-catch 捕获 块,并且相应的进程是 默认终止。因此, 建议用户编写代码 检测和防止堆栈 溢出。例如,如果您的 应用程序依赖于递归,使用 计数器或状态条件 终止递归循环。

我假设异常发生在一个内部的.NET 方法中,而不是在您的代码中。

你可以做几件事。

  • 编写代码,检查 xsl 是否有无限递归,并在应用转换之前通知用户(呃)。
  • 将 XslTransform 代码加载到一个单独的进程中(有些繁琐,但工作量较少)。

您可以使用 Process 类来加载程序集,该程序集将把转换应用到一个单独的进程中,并在失败时向用户发出失败警告,而不用关闭您的主应用程序。

编辑: 我刚刚测试过,这里是如何做到这一点:

主进程:

// 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");

应用转换过程:

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);
}
}
}

我建议在 XmlWriter 对象周围创建一个包装器,这样它就可以计算对 WriteStartElement/WriteEndElement 的调用数量,如果将标记数量限制为某个数字(比如100) ,就可以抛出一个不同的异常,例如 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));
}
}

和。NET 4.0您可以从 System 添加 HandleProcessCorruptedStateExceptions属性。运行时间。包含 try/catch 块的方法的 ExceptionServices。这真的有用!也许不被推荐,但是很管用。

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");


}


}
}

如果您的应用程序依赖于3d-party 代码(在 Xsl-script 中) ,那么您必须首先决定是否要防御它们中的 bug。 如果你真的想要辩护,那么我认为你应该执行你的逻辑,这容易在单独的应用领域的外部错误。 捕获 StackOverflow 异常并不好。

也检查这个 有个问题

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

您还应该尝试用循环替换一些递归函数。

今天我的书堆满了,我读了你的一些帖子,决定帮助垃圾收集者。

我曾经有一个无限循环,就像这样:

    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
}

对我很有效,希望能帮到别人。

注意@WilliamJockusch 在悬赏中提出的问题与原问题不同。

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


发生堆栈溢出是因为堆栈上的数据超过了某个限制(以字节为单位)。这种检测工作原理的详细信息可以在 给你中找到。

我想知道是否有一种通用的方法来跟踪 StackOverflow 异常。换句话说,假设在我的代码中有无限递归,但是我不知道在哪里。我想通过一些方法来跟踪它,这比遍历所有地方的代码更容易,直到我看到它发生。我不管它有多黑客。

正如我在链接中提到的,检测来自静态程序分析的堆栈溢出需要解决停机问题,即 无法确定。现在,我们已经建立了 没有什么灵丹妙药,我可以向你展示一些技巧,我认为有助于跟踪问题。

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

在测试环境中检测堆栈溢出

基本上,这里的问题是您有一个(有限的)测试环境,并希望在一个(扩展的)生产环境中检测堆栈溢出。

我没有检测 SO 本身,而是利用可以设置堆栈深度的事实来解决这个问题。调试器将为您提供所需的所有信息。大多数语言允许指定堆栈大小或最大递归深度。

基本上,我试图通过尽可能小的堆栈深度来强制一个 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。

在您之前创建异常

StackOverflowException是不可捕捉的。这意味着当它发生时,你能做的不多。因此,如果您认为代码中一定会出现错误,那么在某些情况下可以自己制造异常。您唯一需要的是当前堆栈深度; 不需要计数器,您可以使用。网址:

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();
}
}

请注意,如果您正在处理使用回调机制的第三方组件,那么这种方法也可以工作。唯一需要的是您可以在堆栈跟踪中拦截 一些调用。

在单独的线程中进行检测

你明确地提出了这个建议,所以这个建议是这样的。

您可以尝试在单独的线程中检测 SO。.但可能对你没什么好处。甚至在得到上下文切换之前,堆栈溢出就可能发生 很快。这意味着这种机制根本不可靠... ... 我不建议你真的用它。但是构建起来很有趣,所以这里是代码: -)

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();
}
}
}
}
}

在调试器中运行这个命令,并对所发生的事情感到高兴。

使用堆栈溢出的特性

对您的问题的另一种解释是: “可能导致堆栈溢出异常的代码片段在哪里?”.显然,这个问题的答案是: 所有代码都是递归的。对于每段代码,您可以进行一些手动分析。

也可以用静态程序分析来确定。要做到这一点,您需要反编译所有方法,并确定它们是否包含无限递归。这里有一些代码可以帮你做到这一点:

// 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>());
}
}

现在,方法循环包含递归这一事实并不能保证会发生堆栈溢出——它只是堆栈溢出异常最可能的前提条件。简而言之,这意味着这段代码将确定出现堆栈溢出 可以的代码片段,这将大大缩小大多数代码的范围。

还有其他方法

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

  1. 通过托管 CLR 进程并处理它来处理堆栈溢出。请注意,您仍然无法“捕获”它。
  2. 更改所有 IL 代码,构建另一个 DLL,添加递归检查。是的,这是很有可能的(我在过去已经实现过了: ——) ; 这很困难,而且需要很多代码才能做好。
  3. 使用。NET 分析 API 来捕获所有方法调用并使用它来计算堆栈溢出。例如,您可以实现检查,以确保如果在调用树中遇到相同的方法 X 次,就会给出一个信号。有一个 Clrprofiler项目可以让你领先一步。

答案是@William Jockusch。

我想知道是否有一个通用的方法来追踪 换句话说,假设我有无穷大 递归在我的代码中的某个地方,但是我不知道在哪里。我想 通过某种方法来跟踪它,这比单步执行代码更容易 直到我看到它发生。我不在乎如何黑客 是的。例如,如果我能有一个模块就太好了 激活,甚至可能来自轮询堆栈的另一个线程 如果达到我认为“太高”的水平,我会抱怨 例如,我可能将“ too high”设置为600帧,计算如果 堆栈太深了,这肯定是个问题。是这样的东西 另一个例子是记录每1000次方法调用 在我的代码调试输出。机会这将得到一些 超标的证据将是相当不错的,但可能不会 关键是它不能涉及 在溢出发生的地方写一张支票。因为整个 问题是我不知道它在哪里,最好是解决方案 不应该取决于我的开发环境是什么样的; 即, 它不应该假设我是通过一个特定的工具集来使用 C # 的(例如:。 VS).

听起来您很想听到一些调试技术来捕捉这个 StackOverflow,所以我想我可以分享一些技术供您尝试。

1. 内存转储。

Pro’s : 内存转储是一种确定的解决堆栈溢出原因的有效方法。一个 C # MVP 和我一起解决了一个 SO 的故障,他继续写关于它的博客 给你

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

此方法不需要通过日志中的步骤重现问题。

Con’s : Memory Dumps 非常大,您必须附加 AdPlus/procdump 进程。

2. 面向方面编程。

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

将告诉您导致堆栈溢出的方法。

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

Con’s : 它会对性能产生影响——钩子嵌入到每个方法的 IL 中,你不能真正“关闭”它。

这在一定程度上取决于您的开发环境工具集。

3. 记录用户活动。

一周前,我试图找出几个难以复制的问题。我发布了这个质量保证 用户活动日志记录、遥测(和全局异常处理程序中的变量)。我得出的结论是一个非常简单的用户操作日志记录器,用于查看在发生任何未处理的异常时如何在调试器中重现问题。

Pro’s : 你可以随意打开或关闭它(即订阅活动)。

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

可以计算订阅 远比 AOP 简单得多的事件方法的数量。

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

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

Con’s : 不适合 Windows Service 我相信有更好的工具,像这样的网络应用程序

必须的没有告诉您导致堆栈溢出的方法。

需要手动遍历日志来重现问题,而不是通过内存转储直接获取和调试问题。


也许你可以尝试我上面提到的所有技术,以及@atlaste 发布的一些技术,告诉我们你发现哪些技术在 PROD 环境中运行起来最容易/最快/最脏/最容易接受等等。

不管怎样,希望你能顺利找到这个人。

看起来,除了启动另一个进程之外,似乎没有任何处理 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);
}
}
}
}
}

但是,如果您最终使用的是独立进程解决方案,我建议您使用 Process.ExitedProcess.StandardOutput并自己处理错误,以便为用户提供更好的体验。

@ WilliamJockusch,如果我正确理解了你的关心,它是不可能的(从数学的角度来看) 一直都是确定一个无限递归,因为它将意味着解决 停止问题。为了解决这个问题,你需要一台 超级递归算法(例如 试错谓词)或者一台能够 超级计算的机器(例如 以下部分-可以预览-这本书)。

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

  • 在给定的时间内您还剩下多少堆栈内存
  • 递归方法在特定输出的给定时间需要多少堆栈内存。

请记住,在当前的机器上,由于多任务处理,这些数据是非常易变的,我还没有听说过有软件可以完成这项任务。

如果有什么不清楚的地方就告诉我。