Joh*_*nyM 68 .net c# stack-overflow xslcompiledtransform
我想要阻止或处理我在写作中StackOverflowException
对XslCompiledTransform.Transform
方法的调用Xsl Editor
.问题似乎是用户可以写一个Xsl script
无限递归的东西,它只是在调用Transform
方法时爆炸.(也就是说,问题不仅仅是典型的编程错误,这通常是造成这种异常的原因.)
有没有办法检测和/或限制允许的递归次数?或者任何其他想法,以防止这些代码炸毁我?
Fly*_*wat 61
来自微软:
从.NET Framework 2.0版开始,try-catch块无法捕获StackOverflowException对象,默认情况下会终止相应的进程.因此,建议用户编写代码以检测并防止堆栈溢出.例如,如果您的应用程序依赖于递归,请使用计数器或状态条件来终止递归循环.
我假设异常发生在内部.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");
Run Code Online (Sandbox Code Playgroud)
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);
}
}
}
Run Code Online (Sandbox Code Playgroud)
atl*_*ste 23
注:@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();
}
}
Run Code Online (Sandbox Code Playgroud)
注意:我们也将在下面使用此代码.
一旦溢出,您可以将其设置为更大的值,直到您获得有意义的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();
}
}
Run Code Online (Sandbox Code Playgroud)
请注意,如果您正在处理使用回调机制的第三方组件,则此方法也适用.唯一需要的是你可以拦截堆栈跟踪中的一些调用.
在单独的线程中检测
你明确地建议了这个,所以这里就是这个.
您可以尝试在单独的线程中检测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();
}
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
在调试器中运行它,玩得开心.
使用堆栈溢出的特性
对您的问题的另一种解释是:"可能导致堆栈溢出异常的代码段在哪里?".显然,答案是:所有带递归的代码.对于每段代码,您可以进行一些手动分析.
也可以使用静态代码分析来确定这一点.你需要做的是反编译所有方法并确定它们是否包含无限递归.这里有一些代码可以帮到你:
// 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>());
}
}
Run Code Online (Sandbox Code Playgroud)
现在,方法循环包含递归的事实并不能保证堆栈溢出会发生 - 它只是堆栈溢出异常的最可能的前提条件.简而言之,这意味着此代码将确定可能发生堆栈溢出的代码段,这应该会大大缩小大多数代码.
还有其他方法
您可以尝试其他一些方法,我在这里没有描述.
我建议创建一个围绕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));
}
}
Run Code Online (Sandbox Code Playgroud)
这个答案是给@WilliamJockusch 的。
我想知道是否有一种通用方法可以跟踪 StackOverflowExceptions。换句话说,假设我的代码中某处有无限递归,但我不知道在哪里。我想通过某种方式来追踪它,这比在整个地方逐步执行代码直到我看到它发生更容易。我不在乎它有多黑客。例如,如果有一个我可以激活的模块,甚至可以从另一个线程激活,它会轮询堆栈深度,并在它达到我认为“太高”的级别时抱怨,那就太好了。例如,我可能将“太高”设置为 600 帧,认为如果堆栈太深,那一定是一个问题。类似的事情有可能吗?另一个例子是将代码中每第 1000 个方法调用记录到调试输出。获得一些超低证据的机会将是相当大的,而且它可能不会对产出造成太大影响。关键是它不能涉及在发生溢出的任何地方写支票。因为整个问题是我不知道那在哪里。最好的解决方案不应该依赖于我的开发环境;即,不应假定我通过特定工具集(例如 VS)使用 C#。
听起来您很想听到一些调试技术来捕获此 StackOverflow,所以我想我会分享一些供您尝试。
优点:内存转储是解决堆栈溢出原因的可靠方法。AC# MVP 和我一起对 SO 进行故障排除,他继续在此处撰写有关该问题的博客。
此方法是追踪问题的最快方法。
此方法不需要您按照日志中看到的步骤来重现问题。
缺点:内存转储非常大,您必须附加 AdPlus/procdump 进程。
优点:这可能是您实现从任何方法检查调用堆栈大小的代码的最简单方法,而无需在应用程序的每个方法中编写代码。有很多AOP 框架允许您在调用之前和之后进行拦截。
会告诉你导致堆栈溢出的方法。
允许您检查StackTrace().FrameCount
应用程序中所有方法的入口和出口。
缺点:它会对性能产生影响 - 每个方法的钩子都嵌入到 IL 中,并且您无法真正“取消激活”它。
这在某种程度上取决于您的开发环境工具集。
一周前,我试图找出几个难以重现的问题。我发布了此 QA用户活动日志记录、遥测(以及全局异常处理程序中的变量)。我得出的结论是一个非常简单的用户操作记录器,用于了解当发生任何未处理的异常时如何在调试器中重现问题。
专业版:您可以随意打开或关闭它(即订阅事件)。
跟踪用户操作不需要拦截每个方法。
您可以比 AOP 更简单地计算订阅的事件数量。
日志文件相对较小,重点关注重现问题所需执行的操作。
它可以帮助您了解用户如何使用您的应用程序。
缺点:不适合 Windows 服务,而且我确信对于 Web 应用程序有更好的工具。
不一定告诉你导致Stack Overflow的方法。
需要您手动逐步查看日志以重现问题,而不是内存转储,您可以在内存转储中立即获取并调试它。
也许您可以尝试我上面提到的所有技术以及 @atlaste 发布的一些技术,并告诉我们您发现哪一个是在 PROD 环境等中运行最简单/最快/最脏/最可接受的技术。
不管怎样,祝你好运,找到这个SO。
归档时间: |
|
查看次数: |
116337 次 |
最近记录: |