ПРИМЕЧАНИЕ. Вопрос в награде @WilliamJockusch отличается от исходного вопроса.
Этот ответ касается StackOverflow в общем случае сторонних библиотек и того, что вы можете / не можете с ними делать. Если вы ищете особый случай с XslTransform, см. Принятый ответ.
Переполнение стека происходит из-за того, что данные в стеке превышают определенный предел (в байтах). Подробную информацию о том, как работает это обнаружение, можно найти здесь.
Мне интересно, есть ли общий способ отследить StackOverflowExceptions. Другими словами, предположим, что где-то в моем коде есть бесконечная рекурсия, но я понятия не имею, где. Я хочу отследить это каким-нибудь способом, который проще, чем проходить через код повсюду, пока я не увижу, как это происходит. Меня не волнует, насколько это хакерское.
Как я упоминал в ссылке, обнаружение переполнения стека из статического анализа кода потребует решения проблемы остановки, которая является неразрешимой. Теперь, когда мы установили, что серебряной пули не существует, я могу показать вам несколько уловок, которые, как мне кажется, помогают отследить проблему.
Я думаю, что этот вопрос можно интерпретировать по-разному, и, поскольку мне немного скучно :-), я разобью его на разные варианты.
Обнаружение переполнения стека в тестовой среде
В основном проблема здесь в том, что у вас есть (ограниченная) тестовая среда и вы хотите обнаружить переполнение стека в (расширенной) производственной среде.
Вместо того, чтобы определять сам SO, я решаю эту проблему, используя тот факт, что глубина стека может быть установлена. Отладчик предоставит вам всю необходимую информацию. Большинство языков позволяют указать размер стека или максимальную глубину рекурсии.
В основном я пытаюсь заставить 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();
}
}
Примечание. Мы также будем использовать этот код ниже.
Как только он переполнится, вы можете установить для него большее значение, пока не получите 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();
}
}
Обратите внимание, что этот подход также работает, если вы имеете дело со сторонними компонентами, которые используют механизм обратного вызова. Единственное, что требуется, - это возможность перехватывать некоторые вызовы в трассировке стека.
Обнаружение в отдельной ветке
Вы явно предложили это, так что вот это.
Вы можете попробовать обнаружить 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>());
}
}
Теперь тот факт, что цикл метода содержит рекурсию, ни в коем случае не является гарантией того, что произойдет переполнение стека - это просто наиболее вероятное предварительное условие для исключения вашего переполнения стека. Короче говоря, это означает, что этот код будет определять фрагменты кода, в которых может произойти переполнение стека, что должно значительно сузить большую часть кода.
Другие подходы
Вы можете попробовать и другие подходы, которые я здесь не описал.
- Обработка переполнения стека путем размещения и обработки процесса CLR. Учтите, что вы все равно не можете его «поймать».
- Изменение всего кода IL, создание другой DLL, добавление проверок рекурсии. Да, это вполне возможно (я уже реализовал это раньше :-); это просто сложно и требует большого количества кода, чтобы сделать это правильно.
- Используйте API профилирования .NET для захвата всех вызовов методов и использования этого для определения переполнения стека. Например, вы можете реализовать проверки: если вы встретите один и тот же метод X раз в своем дереве вызовов, вы подадите сигнал. здесь есть проект, который даст вам фору.
person
atlaste
schedule
06.06.2015