Как предотвратить и / или обработать StackOverflowException?
Я хотел бы либо предотвратить, либо обработать исключение StackOverflowException, которое я получаю от вызова XslCompiledTransform.Метод преобразования в Редакторе Xsl, который я пишу. Проблема в том, что пользователь может написать бесконечно рекурсивный скрипт Xsl, и он просто взрывается при вызове метода Transform. (То есть проблема заключается не только в типичной программной ошибке, которая обычно является причиной такого исключения.)
есть ли способ, чтобы обнаружить и / или ограничить, сколько рекурсий разрешено? Или любые другие идеи, чтобы этот код просто не взорвался на мне?
10 ответов
От Microsoft:
начиная с .NET Framework версия 2.0, исключение StackOverflowException объект не может быть пойман try-catch блок и соответствующий процесс прекращена по умолчанию. Следовательно, пользователям рекомендуется написать свой код обнаружение и предотвращение стека переполнение. Например, если ваш применение зависит от рекурсии, использования счетчик или состояние для завершите рекурсивный цикл.
Я предполагая, что исключение происходит внутри внутреннего метода .NET, а не в вашем коде.
Вы можете сделать пару вещей.
- напишите код, который проверяет xsl на бесконечную рекурсию и уведомляет пользователя перед применением преобразования (Ugh).
- загрузите код XslTransform в отдельный процесс (Hacky, но меньше работы).
вы можете использовать класс Process для загрузки сборки, которая применит преобразование в отдельный процесс, и предупредить пользователя о сбое, если он умирает, не убивая основное приложение.
EDIT: я только что протестировал, вот как это сделать:
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, я решаю это, используя тот факт, что глубина стека может быть установлена. Отладчик предоставит вам всю необходимую информацию. Большинство языков позволяют указать размер стека или максимальную глубину рекурсии.
в основном, я пытаюсь заставить так сделать глубину стога как можно меньше. Если он не переполняется, я всегда могу сделать его больше (=в этом случае: безопаснее) для производственной среды. В тот момент, когда вы получаете переполнение стека, вы можете вручную решить, является ли оно "действительным" или нет.
для этого передайте размер стека (в нашем случае: небольшое значение) параметру Thread и посмотрите, что произойдет. Размер стека по умолчанию в .NET составляет 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();
}
}
Примечание: мы будем использовать этот код ниже.
как только он переполняется, вы можете установить его на большее значение, пока не получите так, что имеет смысл.
создание исключений перед вами так
на 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>());
}
}
теперь тот факт, что цикл метода содержит рекурсию, ни в коем случае не гарантирует, что переполнение стека произойдет - это просто наиболее вероятное предварительное условие для исключения переполнения стека. Короче говоря, это означает, что этот код будет определять фрагменты кода, где переполнение стека can происходят, что должно значительно сузить большинство кода.
пока другие подходы
есть и другие подходы вы можете попробовать то, что я не описал здесь.
- обработка переполнения стека путем размещения процесса CLR и его обработки. Обратите внимание, что вы все еще не можете "поймать" его.
- изменение всего кода IL, создание другой DLL, добавление проверок рекурсии. Да, это вполне возможно (я реализовал это в прошлом : -), это просто сложно и включает в себя много кода, чтобы получить это право.
- используйте API профилирования .NET для захвата всех вызовов методов и используйте это для вычисления из переполнения стека. Например, можно реализовать проверки, что если вы столкнулись с тем же методом X раз в дереве вызовов, вы даете сигнал. Есть проект здесь это даст вам фору.
Я бы предложил создать обертку вокруг объекта XmlWriter, чтобы он подсчитывал количество вызовов WriteStartElement / WriteEndElement, и если вы ограничиваете количество тегов некоторым числом (f.e. 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));
}
}
Если ваше приложение зависит от 3d-кода (в 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
}
это сработало для меня, надеюсь, это поможет кому-то.
этот ответ для @WilliamJockusch.
Мне интересно, есть ли общий способ отслеживать StackOverflowExceptions. Другими словами, предположим, что я бесконечен. рекурсия где-то в моем коде, но я понятия не имею, где. Я хочу отследить его с помощью некоторых средств, что проще, чем шаг через код повсюду, пока не увижу, как это происходит. Мне все равно, насколько банально. это. Например, было бы здорово иметь модуль, который я мог бы активировать, возможно даже из другой нити, которая опрашивала стек глубина и жаловалась, если она доходила до уровня, который я считал "слишком высоким"." Для например, я могу установить "слишком высоко" на 600 кадров, полагая, что если стек был слишком глубоким, это должно быть проблемой. Что-то подобное вероятный. Другой пример-регистрировать каждый 1000-й вызов метода в моем коде для вывода отладки. Шансы это получить некоторые доказательство оверлоу было бы довольно хорошим, и это, вероятно, не будет взорвать выход тоже плохо. Ключ в том, что он не может включать написание чека везде, где происходит переполнение. Потому что вся проблема в том, что я не знаю, где это. Желательно решение не должно зависеть от того, как выглядит моя среда разработки; i.e, он не должен assumet, что я использую C# с помощью конкретных инструментов (например, ПРОТИВ.)
похоже, вы хотите услышать некоторые методы отладки, чтобы поймать этот StackOverflow, поэтому я подумал, что поделюсь парой для вас пытаться.
1. дамп памяти.
про: дампы памяти-это надежный способ устранить причину переполнения стека. C# MVP и я работали вместе, устраняя неполадки, и он продолжил блог об этом здесь.
этот метод является самым быстрым способом отслеживания проблемы.
этот метод не требует, чтобы вы воспроизводили проблемы, выполнив шаги, описанные в журналах.
Кон: дампы памяти очень большой, и вы должны прикрепить AdPlus / procdump процесс.
2. Аспектно-Ориентированное Программирование.
про: это, вероятно, самый простой способ для вас реализовать код, который проверяет размер стека вызовов из любого метода без написания кода в каждом методе вашего приложения. Есть куча рамки AOP, которые позволяют перехватывать до и после вызова.
скажет вам методы, которые вызывают переполнение стека.
позволяет проверить StackTrace().FrameCount
при входе и выходе всех методов в вашем приложении.
Кон: это будет иметь влияние на производительность-крючки встроены в IL для каждого метода, и вы не можете действительно "деактивировать" его.
это несколько зависит от вашего набора инструментов среды разработки.
3. Ведение Журнала Активности Пользователя.
неделю назад я пытался выследить несколько трудно воспроизвести проблемы. Я опубликовал это QA ведение журнала активности пользователей, телеметрия (и переменные в глобальных обработчиках исключений) . Вывод, к которому я пришел, был очень простым регистратором действий пользователя, чтобы увидеть, как воспроизводить проблемы в отладчике при возникновении любого необработанного исключения.
про: вы можете включить или выключить по желанию (т. е. подписка на события).
Отслеживание действий пользователя не требует перехвата каждого метода.
вы можете рассчитывать количество методов событий тоже подписано гораздо проще, чем с AOP.
файлы журнала относительно малы и сосредоточены на том, какие действия необходимо выполнить для воспроизведения проблемы.
Это может помочь вам понять, как пользователи используют приложение.
Кон: не подходит для службы Windows и я уверен, что есть лучшие инструменты для веб-приложений.
не обязательно расскажу вам методы, которые вызывают переполнение стека.
требует, чтобы вы прошли через журналы вручную воспроизводя проблемы, а не дамп памяти, где вы можете получить его и отладить его сразу.
возможно, вы могли бы попробовать все методы, которые я упоминал выше, и некоторые из них @atlaste опубликовали и сказали нам, какой из них вы нашли, был самым простым/быстрым/грязным/наиболее приемлемым для запуска в среде PROD/и т. д.
в любом случае удачи в отслеживании этого так.
судя по всему, кроме запуска другого процесса, не существует никакого способа обработки 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
и обрабатывать ошибки самостоятельно, чтобы дать вашим пользователям лучший опыт.
@WilliamJockusch, если я правильно понял вашу озабоченность, это невозможно (с математической точки зрения)всегда определить бесконечную рекурсию, как это означало бы решить проблема останова. Чтобы решить его, вам понадобится супер-рекурсивный алгоритм (типа предикаты проб и ошибок например) или машина, которая может hypercompute (пример объясняется в следующий раздел - доступен как просмотр в книги).
с практической точки зрения, вы должны знать:
- сколько памяти стека вы оставили в данный момент времени
- сколько стековой памяти потребуется рекурсивному методу в данный момент времени для конкретного вывода.
имейте в виду, что с текущими машинами эти данные чрезвычайно изменчивы из-за многозадачности, и я не слышал о программном обеспечении, которое делает задача.
Дайте мне знать, если что-то непонятно.
вы можете прочитать это свойство каждые несколько звонков, Environment.StackTrace
, и если stacktrace превысил определенный порог, который вы задали, вы можете вернуть функцию.
вы также должны попытаться заменить некоторые рекурсивные функции циклами.