如何防止和/或处理一个StackOverflowException?如何防止和/或处理一个Stack

2019-05-08 23:27发布

我想防止或处理一个StackOverflowException ,我从到一个电话让XslCompiledTransform.Transform的内法Xsl Editor我写。 这个问题似乎是,用户可以编写一个Xsl script是无限递归,和它只是吹了在调用Transform方法。 (也就是说,问题不只是典型的编程错误,这通常是这样的异常的原因。)

有没有一种方法来检测和/或限制多少递归被允许? 或任何其他的想法,以保持距离只是我炸毁这个代码?

Answer 1:

从微软:

与.NET Framework 2.0版开始,StackOverflowException对象不能被一个try-catch块捕获和相应的过程默认情况下终止。 因此,建议用户编写代码来检测和防止堆栈溢出。 例如,如果您的应用程序依赖于递归,使用计数器或状态的条件来终止递归循环。

我假设的异常是内部.NET方法内发生的事情,而不是在你的代码。

你可以做两件事情。

  • 写检查无限递归在xsl并通知之前应用一个变换(唉)用户代码。
  • 加载的XslTransform代码到单独的进程(哈克,但较少的工作)。

您可以使用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);
        }
    }
}


Answer 2:

注:这个问题由@WilliamJockusch赏金和原来的问题是不同的。

这个答案是对的StackOverflow的第三方库一般情况下,哪些是你可以/不可以与他们无关。 如果您正在寻找有关使用的XslTransform的特殊情况,请参阅接受的答案。


堆栈溢出发生,因为堆栈上的数据超过一定限度时(以字节为单位)。 这一检测是如何工作的细节,可以发现在这里 。

我不知道是否有追查StackOverflowExceptions的一般方法。 换句话说,假设我有无穷递归的地方在我的代码,但我不知道在哪里。 我想通过某种方式比通过代码遍,直到我看到它发生的地方踩着更容易追查。 我不在乎它是多么的hackish。

正如我在链路所提到的,检测来自静态代码分析堆栈溢出将需要解决停机问题是不可判定的 。 现在,我们已经建立了, 没有银弹 ,我可以告诉你,我认为有助于追查问题的一些技巧。

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

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

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

代替检测SO本身,我通过利用一个事实,即堆深度可以设定解决这个问题。 调试器会给你你需要的所有信息。 大多数语言允许您指定堆栈大小或最大递归深度。

基本上,我试图通过使堆栈深度尽可能小强制SO。 如果不溢出,我总是可以使它更大(=在这种情况下:更安全),用于生产环境。 你会得到一个堆栈溢出的那一刻,您可以手动决定,如果它是一个“有效”一个或没有。

要做到这一点,通过堆栈大小(在我们的例子:一个很小的值)为螺纹参数,看看会发生什么。 在.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)
    {
        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次在调用树,你给的信号。 有一个项目在这里会给你一个良好的开端。


Answer 3:

我建议建立围绕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));
    }
}


Answer 4:

这个答案是@WilliamJockusch。

我不知道是否有追查StackOverflowExceptions的一般方法。 换句话说,假设我有无穷递归的地方在我的代码,但我不知道在哪里。 我想通过某种方式比通过代码遍,直到我看到它发生的地方踩着更容易追查。 我不在乎它是多么的hackish。 例如,这将是巨大的,有一个模块,我可以激活,甚至从另一个线程,接受调查的堆栈深度和抱怨,如果它得到了我认为的水平“过高”。 例如,我可以设置“过高”,以600帧,盘算,如果堆栈太深,这已经是一个问题。 是类似的东西可能。 另一个例子是登录我的代码调试输出中的每1000个方法调用。 的机会,这将得到过低的一些证据是相当不错的,而且很可能不会炸毁输出还算可以。 最关键的是,它可以不涉及写支票的地方溢出正在发生的事情。 因为整个问题是,我不知道哪里是。 Preferrably解决方案不应该依靠什么我的开发环境的样子; 也就是说,它不应该assumet,我通过特定的工具集(例如VS)使用C#。

这听起来像你渴望听到一些调试技术,以赶上这StackOverflow的,所以我想我会分享一些让你尝试。

1.内存转储。

Pro的 :内存转储是确保消防方式制定出一个堆栈溢出的原因。 AC#MVP和我一起工作排查SO和他继续在博客上写下它在这里 。

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

这种方法不会要求您通过以下日志中看到的步骤来重现问题。

反对的 :内存转储是非常大的,你必须附加ADPlus的/ procdump的过程。

2.面向方面编程。

Pro的 :这可能是为你实现,检查任何方法调用堆栈的大小而不在应用程序中的每个方法编写代码的代码最简单的方法。 还有一堆的AOP框架 ,让您之前和之后调用拦截。

会告诉你,是导致堆栈溢出的方法。

允许你检查StackTrace().FrameCount在应用程序中的所有方法的进入和退出。

反对的 :它会影响性能-钩嵌入到IL每一个方法,你不能真正“去激活”了出来。

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

3.记录用户活动。

一个星期前,我试图追捕一些难以重现的问题。 我张贴这种QA 用户活动日志,遥测(和变量在全局异常处理程序) 。 我得出的结论是一个非常简单的用户操作记录器,以了解如何当任何未处理的异常发生重现调试器的问题。

Pro的 :你可以随意打开或关闭它(即订阅事件)。

跟踪用户行为,不需要拦截每一个方法。

你可以指望的事件的方法数订阅太远更简单地比AOP。

日志文件是比较小的,并专注于您需要执行重现该问题采取什么行动。

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

反对的 :不适合Windows服务,我相信有这样的网络应用程序的更好的工具

并不一定告诉你,导致堆栈溢出的方法。

需要你逐步完成日志手动复制问题,而不是一个内存转储在那里你可以得到它并调试它,立竿见影。

 


也许你可以尝试@atlaste贴我上面提到的所有技术和一些并告诉我们哪一个是你发现的是最简单的/最快/最肮脏/最能接受在PROD环境的/ etc运行。

反正好运气追查这个SO。



Answer 5:

如果应用程序依赖于3D方代码(XSL-脚本),那么你必须先决定你想从他们或没有错误辩护。 如果你真的想保卫话,我想你应该执行你的逻辑,其容易在不同的AppDomain外部错误。 捕获StackOverflowException是不好的。

还要检查这个问题 。



Answer 6:

我今天有一个计算器和我看了一些您的文章,并决定帮助了垃圾集气器。

我曾经有过这样一个近乎无限循环:

    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
}

它的工作对我来说,希望它可以帮助别人。



Answer 7:

随着.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");

        }

    }      
}


Answer 8:

通过外观上来看,除了开始另一个进程,似乎没有要处理一个任何方式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和处理自己的错误,给用户更好的体验。



Answer 9:

@WilliamJockusch,如果我理解正确你的关心,这是不可能的(从数学的角度来看) 总是识别无限递归因为这将意味着解决停机问题 。 为了解决这个问题你需要一个超级递归算法 (如试错谓词为例)或可一机hypercompute (一个例子是在解释下面的部分 -可作为预览-的这本书 )。

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

  • 你有多少堆栈内存留在给定的时间
  • 多少堆栈存储器的递归方法将需要在给定的时间,特定的输出。

请记住,与当前的机器,这个数据是非常易变的,由于多任务,我还没有听说过,做任务的一个软件。

让我知道,如果事情还不清楚。



Answer 10:

你可以阅读了这个属性每隔几个电话, Environment.StackTrace ,如果堆栈跟踪exceded你预设的特定阈值,就可以返回的功能。

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



文章来源: How do I prevent and/or handle a StackOverflowException?