スタック:パラメーター値の分析







多くの場合、フォールスタックを見て、呼び出しが行われたパラメーター値を確認したいと思います。 VisualStudioのデバッガーの下で、これらの値を確認できます。 しかし、プログラムがデバッガなしで起動され、独自に例外を処理する場合はどうでしょうか? 回答については、猫へようこそ。



パラメータ値の問題は、私たちにとって怠idleではありません。 開発者がクラッシュレポーターを試すときに最初に尋ねる質問のほとんどは、「パラメーターの値を確認できますか?」



さて、問題をさらに詳しく調べてみましょう。



例外が処理されるかどうかに関係なく、最初はExceptionオブジェクト自体(およびそのInnerExceptionチェーン)があります。



フォールスタックはException.StackTraceプロパティから取得されます。または、 System.Diagnostics.StackTraceタイプのオブジェクトを作成することで、もう少し詳細な形式で取得できます。 また、StackTraceに含まれるフレームによって、どのメソッドが呼び出され、どのシグネチャを持っているかを判別できる場合、パラメーター値とオブジェクト参照(これ)は判別できません。



どうする? ランタイムは必要な情報を提供しないため、自分で情報を収集しようとします。



最も単純なコードを使用します。



public void DoWork(string work) { DoInnerWork(work, 5); } public void DoInnerWork(string work, int times) { object o = null; o.ToString(); }
      
      





try / catchメソッドの内容をラップしましょう。 キャッチされた各例外をメソッドパラメーターの値と共に登録し、さらに送信します。



 public void DoWork(string work) { try { DoInnerWork(work, 5); } catch (Exception ex) { LogifyAlert.Instance.TrackArguments(ex, work); throw; } } public void DoInnerWork(string innerWork, this, int times) { try { object o = null; o.ToString(); } catch (Exception ex) { LogifyAlert.Instance.TrackArguments(ex, this, innerWork, times); throw; } }
      
      





Trackメソッドには署名があります。



 public void TrackArguments(Exception ex, object instance, params object[] args)
      
      





また、引数の値を内部リストまたはディクショナリに追加して、 Exception.StackTraceの対応する行にバインドできるようにします 。 また、適切なタイミングでリストをクリアすることも重要です。そうしないと、2番目にスローされた例外に対してリストの内容が無関係になります。 これらの瞬間は何ですか? メソッドへの入り口と成功(例外をスローせずに)メソッドからの出口、およびグローバル例外ハンドラーへの入り口。 このようなもの:



警告、govnokod
 public void DoWork(string work) { LogifyAlert.Instance.ResetTrackArguments(); try { DoInnerWork(work, 5); LogifyAlert.Instance.ResetTrackArguments(); } catch (Exception ex) { LogifyAlert.Instance.TrackArguments(ex, work); throw; } } public void DoInnerWork(string innerWork, this, int times) { LogifyAlert.Instance.ResetTrackArguments(); try { object o = null; o.ToString(); LogifyAlert.Instance.ResetTrackArguments(); } catch (Exception ex) { LogifyAlert.Instance.TrackArguments(ex, this, innerWork, times); throw; } } void MethodWithHandledException(string work) { LogifyAlert.Instance.ResetTrackArguments(); try { DoInnerWork(work, 5); LogifyAlert.Instance.ResetTrackArguments(); } catch (Exception ex) { HandleException(ex); LogifyAlert.Instance.ResetTrackArguments(); } } void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { var map = LogifyAlert.Instance.MethodArgumentsMap; ExceptionTracker.Reset(); // handle exception below }
      
      







魅惑的に見え、最終的にコードは読めない何かに変わりました。 最初の反応は、悪夢のように取り壊して忘れることです。 言うまでもありませんが、原則は変わらず、パラメーター値を自分で収集する必要があります (慎重に、18 +、多くのマット)。 コードの美しさの問題は間違いなく解決しますが、それはシステムを立ち上げて実行した後です。



パラメーター値をスタック行にバインドする方法は? もちろん、スタック上のフレームのシリアル番号で! System.Diagnostics.StackTraceを作成する時点では、現在のフレームのインデックスは常に0であり、フレームの数は異なっていてもかまいません。 例外が初めてスローされると、フレーム数(スタックの深さ)が最大になります;同じ例外のその後のすべての再スローでは、スタックの深さはもっと小さくなります。 したがって、スタック上の行番号(特定の例外の場合)は、最大スタック深度と現在のスタック深度の差です。 コードの形式:



 public void TrackArguments(Exception ex, MethodCallInfo call) { StackTrace trace = new StackTrace(0, false); int frameCount = trace.FrameCount; MethodCallStackArgumentMap map; if (!MethodArgumentsMap.TryGetValue(ex, out map)) { map = new MethodCallStackArgumentMap(); map.FirstChanceFrameCount = frameCount; MethodArgumentsMap[ex] = map; } int lineIndex = map.FirstChanceFrameCount - frameCount; map[lineIndex] = call; }
      
      





MethodCallInfoは次のようになります。



 public class MethodCallInfo { public object Instance { get; set; } public MethodBase Method { get; set; } public IList<object> Arguments { get; set; } }
      
      





バインドが完了しました。 クラッシュレポートに書き込み、 Exception.StackTraceとともにサーバーに送信すると、表示がわかります。 次のようなものが得られます。







アプローチの基本的な実行可能性が証明されました。今、核戦争のようにコードが怖くならないこと、理想的にはコードをまったく書かないことを確認する必要があります。



AOPのような家庭での便利なことを思い出します。



たとえば、 Castle.DynamicProxyを試し、 インターセプターを作成します。



 public class MethodParamsInterceptor : IInterceptor { public void Intercept(IInvocation invocation) { try { LogifyAlert.Instance.ResetTrackArguments(); invocation.Proceed(); LogifyAlert.Instance.ResetTrackArguments(); } catch (Exception ex) { LogifyAlert.Instance.TrackArguments( ex, CreateMethodCallInfo(invocation) ); throw; } } MethodCallInfo CreateMethodCallInfo(IInvocation invocation) { MethodCallInfo result = new MethodCallInfo(); result.Method = invocation.Method; result.Arguments = invocation.Arguments; result.Instance = invocation.Proxy; return result; } }
      
      





クラッシュレポーターを接続します。



 var client = LogifyAlert.Instance; client.ApiKey = "<my-api-key>"; client.StartExceptionsHandling();
      
      





インターセプターを使用してテストクラスを作成します。



 var proxy = generator.CreateClassProxy<ThrowTestExceptionHelper>( new MethodParamsInterceptor() ); proxy.DoWork("work");
      
      





結果を実行して確認します。







すべてうまくいきましたが、いくつかありますが:





最後のポイントが最も重要です-スタックのパラメーター値のためだけにプロジェクト全体を大幅に書き換える必要があります。 シープスキンのゲームはほとんど価値がありません。



または、「同じものがありますが、マザーオブパールのボタンがあります」? それでも、 PostSharpがあります。 アスペクトを実現します:



 [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly | AttributeTargets.Module)] [Serializable] public class CollectParamsAttribute : OnMethodBoundaryAspect { public override bool CompileTimeValidate(MethodBase method) { if (method.GetCustomAttribute(typeof(IgnoreCallTrackingAttribute)) != null || method.Name == "Dispose") { return false; } return base.CompileTimeValidate(method); } public override void OnEntry(MethodExecutionArgs args) { base.OnEntry(args); LogifyAlert.Instance.ResetTrackArguments(); } public override void OnSuccess(MethodExecutionArgs args) { LogifyAlert.Instance.ResetTrackArguments(); base.OnSuccess(args); } [MethodImpl(MethodImplOptions.NoInlining)] public override void OnException(MethodExecutionArgs args) { if (args.Exception == null) return; if (args.Method != null && args.Arguments != null && args.Instance != this) LogifyAlert.Instance.TrackArguments(args.Exception, CreateMethodCallInfo(args)); base.OnException(args); } MethodCallInfo CreateMethodCallInfo(MethodExecutionArgs args) { MethodCallInfo result = new MethodCallInfo(); result.Method = args.Method; result.Arguments = args.Arguments; result.Instance = args.Instance; return result; } }
      
      





コードにはいくつかのニュアンスがあります。 まず、PostSharpがIgnoreCallTrackingAttribute属性でマークされたメソッドをインスツルメントすることを禁止します。 何のために? このコードを思い出してください:



 void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { var map = LogifyAlert.Instance.MethodArgumentsMap; ExceptionTracker.Reset(); // handle exception below }
      
      





PostSharpが書き換えた場合、それを呼び出すとどうなりますか? アスペクトのOnEntryメソッドが呼び出されます。これは、まず、このような困難を伴って収集した呼び出しパラメーターをクリーンアップします。 壮大な失敗。 したがって、MethodCallArgumentsTrackerにアクセスする必要があるすべてのメソッドは、IgnoreCallTrackingAttribute属性でマークする必要があります。



2番目:破棄の書き換えを禁止します。 どうやら、 ここでなぜルジコフなのか? そして、例外がアプリケーションの深さから私たちに飛んでくると、catch、finally、および他のコードブロックが完全に実行される途中で、ローカルオブジェクトへのリンクが失われ、GCはそれらをきれいにし始めます。 一般的に、この期間中にDisposeが実行される可能性は非常に高く、LogifyAlert.Instance.MethodArgumentsMapの内容を「1タブレットで十分」に捨てます。



奇妙なテストの3番目のニュアンス:



 if (args.Method != null && args.Arguments != null && args.Instance != this) LogifyAlert.Instance.TrackArguments( args.Exception, CreateMethodCallInfo(args) );
      
      





実際、PostSharpはメソッドに埋め込むコードを積極的に最適化します。 また、MethodExecutionArgsフィールドを明示的に有効にしないと、これらのフィールドの値に完全に正規のnullが含まれることになります。これにより、すべてのロジックが無意味になります。



そのため、手首を軽く振ると、アセンブリ全体にアスペクトが適用されます。



 [assembly: CollectParams]
      
      





クラッシュレポートを実行して監視します。







スタックは古いものと同じように見え、それ以上のものはありません。 既存のコードへの変更は最小限です。 結果は完璧に近いです! 潜在的な欠点は、ビルドプロセス中にPostSharpを使用することです。 おそらくこれは誰かを追い払うでしょう。



PostSharpなどのほかに、他にどのようなオプションがありますか?



まず、プロファイラーを記述し、メソッドICorProfilerInfo :: GetILFunctionBodyおよびICorProfilerInfo :: SetILFunctionBodyを使用して、プログラムの実行中にメソッドの本体を直接変更します。 ここで、これを行う方法に関する一連の記事を読むことができます 。 トピックに関するリンクの適切な選択はこちら



長所





短所





チャックノリスにふさわしいハードコアのみのハッカーメソッドがまだあります。







ここでは、いくつかの非パブリックJIT実装関数のアドレスを正確に決定できる場合、ネイティブコードにコンパイルする直前にメソッドのILコードを置き換えるためにそれらを慎重に使用することを試みることができるという事実からなるアプローチについて説明します。 欠点は、関数アドレスを正しく定義することは簡単ではなく、更新により定期的に変更できることです。 したがって、著者による記事の例は、単に機能しませんでした。 必要なアドレスを特定できませんでした。 別のマイナス-アセンブリがNGenによって処理された場合、アプローチは機能しません。



メソッドをインターセプトする元のメソッドの別の豪華な説明が ForwardAA 同志によって公開されました。ここはハブです 適切なファイルの改良により、彼のアプローチは呼び出し引数の値を収集するタスクに適応できる可能性があります。 プラスから-NGenでアセンブリを処理した後でも、このアプローチが機能する可能性があります。



おわりに



例外がスローされたときに呼び出し引数値を収集する最も信頼できる方法は、Postsharpを使用することです。 Logifyクライアントは、収集された値を、例外が発生したときに書き込まれたスタックにバインドできます。 これにより生じるクラッシュレポートは、場合によっては、スタックのみを含むよりもはるかに有益であることが判明する場合があります。



All Articles