WinDbg+SOSによるクラッシュダンプ解析の知識マップ(知識グラフ)
この記事はクラッシュダンプをWinDbgで解析する手順を扱い、WinDbgはSOS拡張を読み込むことで初めて.NETのマネージドヒープや例外情報を読めるようになります。未処理例外の調査は!clrstackでスタックを、!peで例外オブジェクトを確認する流れになり、メモリが増え続ける調査は!dumpheap -statでヒープの統計を取ってから!gcrootで参照元をたどります。ネイティブクラッシュには!analyze -vによる自動解析を用い、PDBが無くても多くのコマンドは機能しますがソースファイル名・行番号だけは失われます。純粋なマネージドコードの調査なら、WinDbgより軽量なdotnet-dump analyzeも選択肢になりますが、ネイティブスタックフレームは扱えません。
クラッシュダンプ
プロセスのメモリ内容・スタック情報・例外情報などをファイルへ書き出した、クラッシュ調査に使うスナップショットである。
.dmpファイル
ダンプファイル
WinDbg
クラッシュダンプやハングしたプロセスの状態をカーネル/ユーザーモードで解析するためのMicrosoft製デバッガー。SOS拡張によるマネージドヒープ解析や、Application Verifierの検出結果の解析にも使われる。
SOS拡張
マネージドヒープの情報収集、ヒープ破損の検出、ランタイム内部データ型の表示など、CLRの内部をデバッガー上から読むためのデバッガー拡張である。
PDB(プログラムデータベース)
ビルド後の.dll/.exeと、ソースコード上の行・変数名を結びつけるデバッグ情報ファイル。アプリケーションの実行には通常不要だが、デバッグ・例外調査・ダンプ解析に使う。
シンボルファイル
未処理例外
アプリケーションのどこでもキャッチされないままランタイムまで届き、プロセスを終了させる例外である。
!clrstack(CLRStackコマンド)
SOS拡張が提供する、マネージドコードだけのスタックトレースを表示するコマンドである。
!pe(PrintExceptionコマンド)
SOS拡張が提供する、現在のスレッドで最後にスローされた例外の型・メッセージ・内部例外・スタックトレースを表示するコマンドである。
マネージドメモリリーク
マネージドオブジェクトへの参照が解放されずに残り続け、マネージドヒープの使用量が増え続ける状態である。
メモリリーク(.NET)
dumpheap -statコマンド
ヒープ上の型ごとの件数(Count)とサイズ(Size)の統計を表示するSOSデバッガーコマンド。
gcrootコマンド
指定したオブジェクトがどこから参照されているか、GCルートまでの参照経路を表示するSOSデバッガーコマンド。
マネージドヒープ
.NETのガベージコレクターが管理する、マネージドオブジェクトが確保されるメモリ領域である。
ネイティブクラッシュ
C++のDLL、COM、ベンダーSDKなどが絡む、マネージド例外ではなくネイティブコード側で発生するクラッシュである。
アクセス違反
ネイティブ例外
!analyze -v
クラッシュ・例外の自動解析を行うWinDbgの拡張コマンドで、-vオプションを付けると落ちた場所・推定原因・所属モジュールなどを詳細表示する。
dotnet-dump
プロセスのダンプ(ヒープダンプやクラッシュダンプ)を収集・解析する.NETの診断ツール。SOSコマンド(dumpheap・gcroot・clrstackなど)でヒープの参照元やスタックトレースを調べられるが、ネイティブのスタックフレームは扱えない。
.NET Framework
Windowsのコンポーネントとして扱われ、インストールされているOSのライフサイクルに沿ってサポートされるランタイム。System.Messagingの対象範囲。
.NET(Core以降)
.NET Frameworkの後継となる、クロスプラットフォーム対応の.NETランタイム(.NET 5以降)。
モダンな.NET
ハンドルリーク
参照カウントやハンドルカウントの数え違いなどにより、不要になったハンドル(ファイル・イベント・スレッド・GDIオブジェクト・アクセストークンなどカーネルオブジェクトへの参照)が解放されずに積み上がっていく状態。
.NETアプリのダンプでマネージドヒープの中身やC#のスタックフレームを見るには、WinDbg上でSOS拡張を読み込む必要がある
WinDbgは-zオプションまたはFileメニューからクラッシュダンプファイルを指定して開き、その中身を解析する
ソースファイル名・行番号やネイティブフレームの関数名まで見るには、_NT_SYMBOL_PATHにPDBの場所を通す必要があるが、多くのコマンドはPDBが無くても機能する
マネージドコードの未処理例外で落ちたダンプは、例外を持つスレッドに切り替えて!clrstackでマネージドスタックを表示することで、どの経路で落ちる場所まで来たかを確認できる
マネージドコードの未処理例外では、!pe(PrintException)で例外オブジェクトの型・メッセージ・内部例外を確認できる
!dumpheap -statでマネージドヒープの型ごとの件数・合計サイズを確認し、量で殴っている型を特定する
!dumpheapで特定した個別インスタンスについて!gcrootを実行し、GCで回収されずに残り続ける原因の参照元を洗い出す
!dumpheapはマネージドヒープを走査して型ごとの統計または個別インスタンスの一覧を表示する
!gcrootはマネージドヒープとハンドルテーブルの全体を検索して参照経路を洗い出す
アクセス違反などのネイティブクラッシュは、まず!analyze -vの自動解析でFAULTING_IPやFOLLOWUP_IPなどの主要フィールドを確認するところから入る
dotnet-dump analyzeはクラッシュダンプファイルを指定して対話セッションを開く
dotnet-dump analyzeはSOSが事前インストールされた対話セッションを開き、clrstackやdumpheapなどSOSのコマンド体系をそのまま使える
dotnet-dumpはネイティブなデバッガーではないためネイティブのスタックフレームを扱えず!analyze -vも使えないので、ネイティブクラッシュの調査には向かない
COMやP/Invoke、ネイティブDLLが絡む可能性がある調査には、ネイティブスタックフレームと!analyze -vが使えるWinDbgが本記事の推奨である
純粋なマネージドコードのメモリリーク調査で、CIや複数プラットフォームでも動かしたい場合は、導入が軽いdotnet-dump analyzeが本記事の推奨である
対象アプリが.NET Frameworkの場合、ランタイム本体clr.dllの近傍から.loadby sos clrでSOSを読み込む
対象アプリが.NET Core/.NET 5+の場合、ランタイム本体coreclr.dllの近傍から.loadby sos coreclrでSOSを読み込む
SafeHandleを持つマネージドラッパーが原因のハンドルリークでは、!dumpheap -statでマネージドヒープの増加がないと分かった後、そのラッパーが残っていれば!gcrootでGCルートを追い、OSハンドルの解放漏れ箇所を特定できる
未処理例外はプロセスを終了させ、WER LocalDumpsやProcDumpの設定次第でその瞬間のクラッシュダンプが書き出される