我想要阻止或处理我在写作中StackOverflowException
对XslCompiledTransform.Transform
方法的调用Xsl Editor
.问题似乎是用户可以写一个Xsl script
无限递归的东西,它只是在调用Transform
方法时爆炸.(也就是说,问题不仅仅是典型的编程错误,这通常是造成这种异常的原因.)
有没有办法检测和/或限制允许的递归次数?或者任何其他想法,以防止这些代码炸毁我?
来自微软:
从.NET Framework 2.0版开始,try-catch块无法捕获StackOverflowException对象,默认情况下会终止相应的进程.因此,建议用户编写代码以检测并防止堆栈溢出.例如,如果您的应用程序依赖于递归,请使用计数器或状态条件来终止递归循环.
我假设异常发生在内部.NET方法中,而不是在您的代码中.
你可以做几件事.
编写检查xsl无限递归的代码,并在应用转换(Ugh)之前通知用户.
将XslTransform代码加载到一个单独的进程中(Hacky,但工作量较少).
您可以使用Process类加载将转换应用到单独进程的程序集,并在用户死亡时向用户发出故障警报,而不会终止主应用程序.
编辑:我刚测试过,这是怎么做的:
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的特殊情况,请参阅接受的答案.
发生堆栈溢出是因为堆栈上的数据超过了某个限制(以字节为单位).有关此检测的工作原理的详细信息,请参见此处.
我想知道是否有一种通用的方法来跟踪StackOverflowExceptions.换句话说,假设我的代码中某处有无限递归,但我不知道在哪里.我希望通过一些方法来跟踪它,这比在整个地方逐步执行代码更容易,直到我看到它发生.我不在乎它是多么的hackish.
正如我在链接中提到的,从静态代码分析中检测堆栈溢出将需要解决不可判定的暂停问题.既然我们已经确定没有灵丹妙药,我可以向你展示一些我认为有助于追踪问题的技巧.
我认为这个问题可以用不同的方式解释,因为我有点无聊:-),我会把它分解成不同的变化.
检测测试环境中的堆栈溢出
基本上,这里的问题是您有一个(有限的)测试环境,并希望在(扩展)生产环境中检测堆栈溢出.
我没有检测SO本身,而是通过利用可以设置堆栈深度的事实来解决这个问题.调试器将为您提供所需的所有信息.大多数语言允许您指定堆栈大小或最大递归深度.
基本上我试图通过使堆栈深度尽可能小来强制SO.如果它没有溢出,我总是可以让生产环境更大(在这种情况下:更安全).当您获得堆栈溢出时,您可以手动确定它是否是"有效"的.
为此,将堆栈大小(在我们的例子中:一个小值)传递给Thread参数,看看会发生什么..NET中的默认堆栈大小是1 MB,我们将使用更小的值:
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
是不可捕获的.这意味着当它发生时你无能为力.因此,如果您认为代码中的某些内容肯定会出错,那么在某些情况下您可以自己做出异常.你唯一需要的是当前的堆栈深度; 不需要计数器,您可以使用.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(); } }
请注意,如果您正在处理使用回调机制的第三方组件,则此方法也适用.唯一需要的是你可以拦截堆栈跟踪中的一些调用.
在单独的线程中检测
你明确地建议了这个,所以这里就是这个.
您可以尝试在单独的线程中检测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) { HashSetresult = new HashSet (); 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 calling = new Dictionary (); 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 ()) { 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 ()); } }
现在,方法循环包含递归的事实并不能保证堆栈溢出会发生 - 它只是堆栈溢出异常的最可能的前提条件.简而言之,这意味着此代码将确定可能发生堆栈溢出的代码段,这应该会大大缩小大多数代码.
还有其他方法
您可以尝试其他一些方法,我在这里没有描述.
通过托管CLR进程并处理它来处理堆栈溢出.请注意,你仍然无法"抓住"它.
更改所有IL代码,构建另一个DLL,添加对递归的检查.是的,这很有可能(我过去实施了它:-); 这很困难,需要很多代码才能做到正确.
使用.NET分析API捕获所有方法调用,并使用它来计算堆栈溢出.例如,您可以实现检查,如果您在调用树中遇到相同的方法X次,则会给出一个信号.有一个项目在这里会给你一个良好的开端.
我建议创建一个围绕XmlWriter对象的包装器,因此它会计算对WriteStartElement/WriteEndElement的调用量,如果你将标签数量限制为某个数字(fe 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)); } }