ПРИМЕЧАНИЕ Вопрос в награде @WilliamJockusch и первоначальный вопрос отличаются.
Этот ответ о StackOverflow в общем случае сторонних библиотек и о том, что вы можете / не можете делать с ними. Если вы ищете особый случай с XslTransform, см. Принятый ответ.
Переполнение стека происходит из-за того, что данные в стеке превышают определенный лимит (в байтах). Подробности того, как работает это обнаружение, можно найти здесь .
Мне интересно, существует ли общий способ отслеживания исключений StackOverflowException. Другими словами, предположим, что где-то в моем коде есть бесконечная рекурсия, но я понятия не имею, где. Я хочу отследить это некоторыми способами, которые проще, чем пошагово перебирать код, пока я не увижу, что это происходит. Мне все равно, насколько хакерский это.
Как я упоминал в ссылке, обнаружение переполнения стека в результате статического анализа кода потребует решения проблемы остановки, которая неразрешима . Теперь, когда мы установили, что в нет серебряной пули , я могу показать вам несколько трюков, которые, как мне кажется, помогут отследить проблему.
Я думаю, что этот вопрос можно интерпретировать по-разному, и, так как мне немного скучно :-), я разобью его на разные варианты.
Обнаружение переполнения стека в тестовой среде
В основном проблема здесь в том, что у вас есть (ограниченная) тестовая среда и вы хотите обнаружить переполнение стека в (расширенной) производственной среде.
Вместо того, чтобы определять само 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 в отдельном потоке ... но это, вероятно, не принесет вам пользы. Переполнение стека может произойти 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();
}
}
}
}
}
Запустите это в отладчике и получите удовольствие от того, что происходит.
Использование характеристик переполнения стека
Другая интерпретация вашего вопроса: «Где фрагменты кода, которые могут вызвать исключение переполнения стека?». Очевидно, что ответ таков: весь код с рекурсией. Затем для каждого фрагмента кода вы можете выполнить некоторый ручной анализ.
Это также можно определить с помощью статического анализа кода. Для этого вам нужно декомпилировать все методы и выяснить, содержат ли они бесконечную рекурсию. Вот код, который сделает это за вас:
// 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 раз в дереве вызовов, вы даете сигнал. Здесь есть проект здесь , который даст вам преимущество.