注@WilliamJockusch による報奨金の質問と元の質問は異なります。
この回答は、サードパーティのライブラリの一般的なケースでの StackOverflow と、それらでできる/できないことについてです。XslTransform の特殊なケースを探している場合は、受け入れられた回答を参照してください。
スタック オーバーフローは、スタック上のデータが特定の制限 (バイト単位) を超えるために発生します。この検出のしくみの詳細については、こちらを参照してください。
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 が得られるまで、より大きな値に設定できます。
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があります。