我想要防止或处理从调用XslCompiledTransform.Transform
方法引起的StackOverflowException
,该方法在我编写的Xsl编辑器
中使用。问题似乎是用户可以编写无限递归的Xsl脚本
,并且在调用Transform
方法时它会崩溃。(也就是说,这个问题不仅仅是典型的程序错误,通常情况下这是这种异常的原因。)
是否有一种方式能够检测和/或限制允许的递归次数?或者还有其他想法,使得代码不会只是因此崩溃?
我想要防止或处理从调用XslCompiledTransform.Transform
方法引起的StackOverflowException
,该方法在我编写的Xsl编辑器
中使用。问题似乎是用户可以编写无限递归的Xsl脚本
,并且在调用Transform
方法时它会崩溃。(也就是说,这个问题不仅仅是典型的程序错误,通常情况下这是这种异常的原因。)
是否有一种方式能够检测和/或限制允许的递归次数?或者还有其他想法,使得代码不会只是因此崩溃?
来自微软:
从.NET Framework 2.0版本开始,StackOverflowException对象不能被try-catch块捕获,相应的进程默认终止。因此,建议用户编写代码以检测和防止堆栈溢出。例如,如果您的应用程序依赖于递归,请使用计数器或状态条件来终止递归循环。
我假设异常发生在内部.NET方法中,而不是在您的代码中。
您可以采取一些措施。
您可以使用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");
应用转换过程:
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);
}
}
}
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。
在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();
}
}
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>());
}
}
现在,一个方法循环包含递归并不意味着一定会发生堆栈溢出 - 这只是堆栈溢出异常最可能的前提条件。简而言之,这意味着这段代码将确定堆栈溢出可能发生的代码块,这应该大大缩小了大部分代码。
其他方法
还有一些其他方法可以尝试,我没有在这里描述。
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));
}
}
StackTrace().FrameCount
。
缺点: 它会对性能产生影响——钩子嵌入到每个方法的IL中,你不能真正地将其“停用”。
它在某种程度上取决于你的开发环境工具集。
一周前,我试图追踪几个难以重现的问题。我发布了这个 QA User Activity Logging, Telemetry (and Variables in Global Exception Handlers) 。我得出的结论是一个非常简单的用户操作记录器,可以在任何未处理的异常发生时查看如何在调试器中重现问题。
优点: 你可以随意打开或关闭它(即订阅事件)。
跟踪用户操作不需要拦截每个方法。
你可以更简单地计算方法订阅的事件数,远比AOP简单。
日志文件相对较小,侧重于你需要执行哪些操作才能重现问题。
它可以帮助你了解用户如何使用你的应用程序。
缺点: 不适用于Windows服务,我确信有更好的工具适用于Web应用程序。不一定告诉你导致堆栈溢出的方法。
需要手动步进日志以重现问题,而不是使用内存转储文件,可以直接获取并进行调试。
也许你可以尝试上述所有技术以及@atlaste发布的一些技术,并告诉我们哪些技术对于在PROD环境中运行最容易/最快/最脏/最可接受等等。
无论如何,祝你好运追踪这个SO。
如果您的应用程序依赖于第三方代码(在XSL脚本中),那么您首先需要决定是否要防止其中的错误。
如果您真的想防止这些错误,那么我认为您应该在单独的AppDomains中执行容易遭受外部错误的逻辑。捕获StackOverflowException不是一个好方法。
还可以查看这个问题。
我今天遇到了一个stackoverflow错误,读了一些你的帖子后决定帮助垃圾回收器。
我以前有一个类似这样的近乎无限循环:
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
}
它对我有效,希望能帮到其他人。
b
并且它没有任何副作用可以移除。我非常确定第二段代码将被内联程序捕获,因为它是如此微不足道的主体,从而产生完全相同的代码。第三,第一个赋值的范围是循环的 '{'..'}' 内容。实际上,代码唯一做的事情就是永远循环。 - atlastewhile(true){}
来实现无限循环,而不是一些模糊的基于 float
的 for
循环实现。 - Nick Mertin使用.NET 4.0,您可以将HandleProcessCorruptedStateExceptions
属性从System.Runtime.ExceptionServices添加到包含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");
}
}
}
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.Exited
和Process.StandardOutput
并自行处理错误,以给用户提供更好的体验。Environment.StackTrace
,如果堆栈跟踪超过预设的特定阈值,则可以返回该函数。
您还应尝试使用循环替换一些递归函数。
ProfilerCallback::_LogCallTrace
可能是个好的起点,但这似乎并不是一项简单的任务。通过David Browman的博客链接下载分析器源码:http://blogs.msdn.com/b/davbr/archive/2011/02/01/clrprofiler-v4-released.aspx - forsvarir