我如何预防和/或处理StackOverflowException?
-
03-07-2019 - |
题
我想要么防止或处理 StackOverflowException
我得到从一个电话的 XslCompiledTransform.Transform
方法内的一个 Xsl Editor
我写作。该问题似乎是,用户可以编写一个 Xsl script
也就是无限的递归,它只是吹的呼吁 Transform
法。(是的,问题不仅仅是典型的方案错误,这通常是因为这样的一个例外。)
有没有方法检测和/或限制多少递归是允许的?或任何其他的想法保持这种代码只是吹在我身上?
解决方案
从Microsoft:
开始的。净框架 2.0版本,StackOverflowException 对象无法抓获一个试试-抓住 方框和相应的过程 终止的默认。因此, 建议用户编写自己的代码 侦查和防止一堆 溢出。例如,如果你的 应用取决于递归,使用 一个计数器或一个国家的条件 终止递归的循环。
我假设的例外是发生在内部的。净的方法,并不在你的代码。
你可以做一对夫妇的事情。
- 编写代码,用于检查xsl为无限递归并向用户通知之前申请转换(唉).
- 负载的请参见码成一个单独的进程(哈克,但不工作)。
你可以使用进程类载的大会,适用《转变成一个单独的进程,并提醒用户的失败,如果它死了,没有杀害你的主要应用程序。
编辑:我只是测试,这里是如何做到这一点:
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的通用方法。换句话说,假设我的代码中某处有无限递归,但我不知道在哪里。我希望通过一些方法来跟踪它,这比在整个地方逐步执行代码更容易,直到我看到它发生。我不在乎它是多么的黑暗。
正如我在链接中提到的,检测静态代码分析中的堆栈溢出需要解决不可判定的暂停问题。现在我们已经确定没有银弹,我可以向您展示一些我认为有助于追踪问题的技巧。
我认为这个问题可以用不同的方式解释,因为我有点无聊:-),我会把它分解成不同的变体。
检测测试环境中的堆栈溢出
这里的问题基本上是你有一个(有限的)测试环境,并希望在(扩展的)生产环境中检测堆栈溢出。
我没有检测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 ..但它可能对您没有任何好处。即使在进行上下文切换之前,堆栈溢出也可能发生 fast 。这意味着这种机制根本不可靠...... 我不建议实际使用它。虽然构建很有趣,所以这里是代码: - )
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();
}
}
}
}
}
在调试器中运行它,并了解会发生什么。
使用堆栈溢出的特征
您的问题的另一种解释是:“在哪里
我建议创建一个围绕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));
}
}
这个答案是给@WilliamJockusch的。
我想知道是否有一般追踪方式 StackOverflowExceptions。换句话说,假设我有无限的 在我的代码中的某处递归,但我不知道在哪里。我想要 通过某种方式跟踪它比通过代码更容易 到处都是,直到我看到它发生。我不在乎多么hackish 它是。例如,拥有一个我可以的模块会很棒 激活,甚至可能从另一个线程激活,轮询堆栈 深度和抱怨,如果它达到我认为“太高”的水平。对于 例如,我可能会设置“太高”至600帧,如果确定的话 堆栈太深,这一定是个问题。是这样的 可能。另一个例子是记录每个第1000个方法调用 在我的代码中调试输出。这可能会得到一些 过低的证据会很好,而且可能不会 把输出放得太厉害了。关键是它不能涉及 在发生溢出的地方写一张支票。因为整个 问题是我不知道那是哪里。最好是解决方案 不应该取决于我的开发环境是什么样的;即, 它不应该假设我通过特定的工具集使用C#(例如 VS)。
听起来你很想听到一些调试技术来捕捉这个StackOverflow所以我想我会分享一些让你尝试的。
1。记忆转储。
Pro的:内存转储是解决堆栈溢出原因的可靠方法。 C#MVP&我一起解决了SO问题,然后他继续写博客这里。
此方法是追踪问题的最快方法。
此方法不需要您按照日志中的步骤重现问题。
Con's :内存转储非常大,您必须在此过程中附加AdPlus / procdump。
2。面向方面编程。
Pro :这可能是您实现从任何方法检查调用堆栈大小的代码的最简单方法,而无需在应用程序的每个方法中编写代码。有一堆 AOP Frameworks 允许你在通话前后拦截。
将告诉您导致Stack Overflow的方法。
允许您在应用程序中所有方法的入口和出口处检查 StackTrace()。FrameCount
。
Con's :它会对性能产生影响 - 每个方法都会将挂钩嵌入到IL中,并且您无法真正“取消激活”它出来了。
这在某种程度上取决于您的开发环境工具集。
3。记录用户活动。
一周前,我试图追捕几个难以重现的问题。我发布了此QA 用户活动记录,遥测(和变量)在全局异常处理程序中)。我得出的结论是一个非常简单的用户操作记录器,用于查看在发生任何未处理的异常时如何在调试器中重现问题。
Pro :您可以随意打开或关闭它(即订阅活动)。
跟踪用户操作不需要拦截每个方法。
您可以计算订阅方法的数量远比使用AOP 更简单。
日志文件相对较小,专注于您的操作
如果您的应用程序依赖于3d-party代码(在Xsl脚本中),那么您必须首先决定是否要防范它们中的错误。 如果你真的想要辩护那么我认为你应该执行你的逻辑,这个逻辑在单独的AppDomain中容易出现外部错误。 捕获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
}
它对我有用,希望它对某人有所帮助。
使用.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");
}
}
}
从它的外观来看,除了启动另一个进程之外,似乎没有任何方法可以处理 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
,如果stacktrace超出您预设的特定阈值,则可以返回该函数。
您还应该尝试用循环替换一些递归函数。