注意@WilliamJockusch 的赏金问题与原始问题不同。
这个答案是关于 StackOverflow 在第三方库的一般情况下以及你可以/不能用它们做什么。如果您正在寻找 XslTransform 的特殊情况,请参阅接受的答案。
堆栈溢出的发生是因为堆栈上的数据超过了某个限制(以字节为单位)。可以在here 找到有关此检测工作原理的详细信息。
我想知道是否有一种通用的方法来追踪 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。但它可能对您没有任何好处。堆栈溢出可能发生快速,甚至在您获得上下文切换之前。这意味着这种机制根本不可靠...... 我不建议实际使用它。不过构建起来很有趣,所以这里是代码:-)
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,添加递归检查。是的,这很有可能(我过去已经实现了:-);这很困难,并且需要大量代码才能正确完成。
- 使用 .NET 分析 API 捕获所有方法调用并使用它来确定堆栈溢出。例如,您可以实施检查,如果您在调用树中遇到 X 次相同的方法,您会发出信号。有一个项目clrprofiler 可以让您抢占先机。