WinDbg + SOSでクラッシュダンプを読む ── 収集した後の実務解析入門

· 更新日: · · WinDbg, SOS, クラッシュダンプ, .NET, CSharp, デバッグ, PDB, 不具合調査, 技術相談

更新履歴(3件・最終更新 2026年08月02日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
主要なコマンドについて、出力のどの行を読むかの注釈を付けました(出力は公式ドキュメント掲載分の引用です)。あわせてシンボルの有無で何が見えなくなるかの比較表、練習用のダンプを自分で作る手順、前提と略語の説明を追加しています。
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589937)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「WinDbg + SOSでクラッシュダンプを読む ── 収集した後の実務解析入門」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589937 https://staging.comcomponent.com/blog/windbg-sos-crash-dump-analysis/

DOI(最新版)
10.5281/zenodo.21589937
DOI(この版)
10.5281/zenodo.21732925

以前の記事「Windowsクラッシュダンプ収集入門」では、WER LocalDumps・ProcDump・MiniDumpWriteDump を使ってダンプを「取る」ところまでを整理しました。ただ、ダンプは取れただけでは何も教えてくれません。実際に手を動かして「どのスレッドが」「なぜ」落ちたのか、あるいは「何が」メモリを握り続けているのかを読み解いて初めて、調査の材料になります。

この記事では収集編の続きとして、取れたダンプを WinDbg と SOS 拡張で実際に読む手順に絞って書きます。インストールとシンボル設定、.NET アプリで必須になる SOS 拡張の読み込み、!clrstack!dumpheap -stat といった代表的なコマンドで「何を見て、どう判断するか」、ネイティブクラッシュの !analyze -v、そして WinDbg を使わない dotnet-dump analyze との使い分けまでを扱います。

この記事の前提

項目 内容
対象読者 手元にクラッシュダンプ(.dmp)があり、これから中身を読む Windows / .NET アプリの開発・保守担当
前提知識 C# のスタックトレースが読めること。WinDbg の使用経験は前提にしません
前提記事 ダンプの取り方は収集編で扱っています。この記事は「すでにダンプがある」ところから始めます。未読でも、第 2.4 節の手順で練習用のダンプを自分で作れば読み進められます
使うツール WinDbg(現行版)、SOS 拡張、必要に応じて dotnet-dump

以降、断りなく使う略語は次の 4 つです。

略語 フルスペル この記事での意味
WER Windows Error Reporting Windows 標準のエラー報告機構。LocalDumps の設定でクラッシュ時のダンプを自動保存できます(設定手順は収集編)
PDB Program Database ビルド時に生成されるシンボルファイル。ソースファイル名・行番号の対応表を持ちます(第 8 章)
CLR Common Language Runtime .NET の実行エンジン本体。clr.dll / coreclr.dll として読み込まれます1
SOS Son of Strike その CLR の内部を読むためのデバッガー拡張。第 3 章で読み込みます2

1. まず結論

  • ダンプ解析の主役は WinDbg(現行版。旧称 WinDbg Preview)です。winget install Microsoft.WinDbg または Microsoft Store から入手でき、Windows 10 Anniversary Update (1607) 以降 / Windows 11 の x64・ARM64 で動きます。3
  • シンボル(PDB)が読み込めていなくても !clrstack!dumpheap -stat!gcroot などはCLRのメタデータ・ヒープデータからそのまま機能します。失われるのはマネージドのソースファイル名・行番号と、ネイティブフレームのシンボル名です。 とはいえソース行まで追いたいなら話は別なので、_NT_SYMBOL_PATH に Microsoft のパブリックシンボルサーバーと自社 PDB の場所を両方通すのが定石です。4 シンボルの設定手順は第 2 章「PDB がある/ない で何が見えなくなるか」と運用は第 8 章にまとめてあります。
  • .NET(Framework / Core / 5+)アプリのダンプでは、SOS 拡張を読み込んで初めてマネージド情報が見えます。ネイティブの k(スタック表示)だけでは C# のコードは追えません。2
  • 代表的な調査の型は 3 つです。例外で落ちたなら !clrstack!peメモリが増え続けるなら !dumpheap -stat!gcrootネイティブクラッシュなら !analyze -v から入ります。
  • WinDbg を使わない選択肢として dotnet-dump analyze があります。SOS コマンドの多くをそのまま使えますが、ネイティブのスタックフレームは扱えません。手元にネイティブ DLL や COM が絡まないマネージドオンリーの調査なら、こちらのほうが導入が軽いです。5
  • ここで書く手順は「読み方」であって「取り方」ではありません。ダンプの取得方法(WER / ProcDump / MiniDumpWriteDump)は収集編を、クラッシュ時にログと突き合わせる設計は「クラッシュ時にログとダンプを残す設計」を参照してください。

この記事の知識マップ

この記事はクラッシュダンプをWinDbgで解析する手順を扱い、WinDbgはSOS拡張を読み込むことで初めて.NETのマネージドヒープや例外情報を読めるようになります。未処理例外の調査は!clrstackでスタックを、!peで例外オブジェクトを確認する流れになり、メモリが増え続ける調査は!dumpheap -statでヒープの統計を取ってから!gcrootで参照元をたどります。ネイティブクラッシュには!analyze -vによる自動解析を用い、PDBが無くても多くのコマンドは機能しますがソースファイル名・行番号だけは失われます。純粋なマネージドコードの調査なら、WinDbgより軽量なdotnet-dump analyzeも選択肢になりますが、ネイティブスタックフレームは扱えません。

WinDbg+SOSによるクラッシュダンプ解析の知識マップクラッシュダンプをWinDbgとSOS拡張で読み解く際に、対象が未処理例外かマネージドメモリリークかネイティブクラッシュかに応じてどのコマンドを使うか、シンボル(PDB)の有無で何が変わるか、dotnet-dumpとの使い分けを示した図。利用する利用する前提とするで確認できるで確認できるで確認できるで確認できる利用する利用するで確認できる前提とする利用する用いるのは非推奨推奨される対応推奨される対応前提とする前提とするで確認できる原因になり得るクラッシュダンプWinDbgSOS拡張PDB(プログラムデータベース)未処理例外!clrstack(CLRStackコマンド)!pe(PrintExceptionコマンド)マネージドメモリリークdumpheap -statコマンドgcrootコマンドマネージドヒープネイティブクラッシュ!analyze -vdotnet-dump.NET Framework.NET(Core以降)ハンドルリーク

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全19件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

2. WinDbgを入れてシンボルを通す

2.1 インストール

現行の WinDbg は次のいずれかで入ります。3

winget install Microsoft.WinDbg

Microsoft Store 経由でも同じエンジンが入り、コマンド・拡張・ワークフローは共通です。インストール後は自動更新される(Store・直接インストールの場合はバックグラウンドで、winget の場合は winget upgrade Microsoft.WinDbg で)ので、バージョン違いによる挙動差にはあまり悩まされません。3

2.2 ダンプを開く

windbg -z C:\CrashDumps\MyApp\MyApp_20260702_101500.dmp

-z はダンプファイルを指定して起動するオプションです。GUI から開く場合は、File メニューのダンプを開く項目(WinDbg Classic での表記は Open crash dump、ショートカットは Ctrl+D)を使います。6

なお、この記事には画面キャプチャを載せていません。代わりに、コマンドを打ち込む場所とメニュー名を文字で書きます。ダンプを開くと Command ツールウィンドウが開き、7 その最下部にある 0:000> のようなプロンプトが入力欄です。以降に出てくる ! 付きのコマンドは、すべてこの Command ウィンドウに打ち込みます(Microsoft の解析例でも 0:000> !analyze -v の形で書かれています)。8 プロンプトの 0:000 は「プロセス 0 のスレッド 0 を選択中」という意味で、第 4 章の ~5s でスレッドを切り替えると 0:005> に変わります。

2.3 シンボルパスを設定する

Windows デバッガーがシンボルファイル(PDB)を探す場所は _NT_SYMBOL_PATH 環境変数、またはセッション内の .sympath コマンドで指定します。4 実務では、Microsoft のパブリックシンボルサーバーと自社 PDB の場所を両方通すのが基本形です。

.symfix C:\Symbols\Microsoft
.sympath+ C:\Symbols\MyApp
.reload
  • .symfix は Microsoft のパブリックシンボルサーバー(https://msdl.microsoft.com/download/symbols)へのパスを、指定したローカルキャッシュ付きで設定するショートカットです。OS 標準 DLL のシンボルはここから自動ダウンロードされます。9
  • .sympath+ は既存のパスに自社 PDB の置き場所を追記します。自社コードの PDB は自分で用意する必要があり、Microsoft のシンボルサーバーには載りません。
  • .reload で読み込み直し、モジュール一覧のシンボル状態を確認します。

環境変数で恒久的に設定する場合は次の形です。CI やビルドサーバーでの自動解析にはこちらが向いています。

set _NT_SYMBOL_PATH=srv*C:\Symbols\Microsoft*https://msdl.microsoft.com/download/symbols;C:\Symbols\MyApp

シンボルが正しく読めているかは、lm(loaded modules)コマンドでモジュール一覧を出し、対象のモジュールが pdb symbols になっているかで確認できます。deferred のままなら、まだシンボルが解決されていない状態です。

2.4 練習用のダンプを自分で作る

いきなり本番障害のダンプで練習するのは辛いので、わざと落ちるミニアプリを用意して、この記事の手順をひととおり試せるようにしておくと理解が早くなります。手元に読めるダンプがない場合は、まずここから始めてください。

コンソールアプリを 1 つ作ります(.NET 8 / C# 12。dotnet new console -n CrashLab で作ったプロジェクトの Program.cs を、まるごと次で置き換えます)。

// Program.cs
using System;
using System.Collections.Generic;

// (A) 第5章の !dumpheap -stat / !gcroot 用: 解放されないまま増えていく配列
var cache = new List<byte[]>();
for (int i = 0; i < 2000; i++)
{
    cache.Add(new byte[100_000]);
}
Console.WriteLine($"cached: {cache.Count} blocks");

// (B) 第4章の !clrstack / !pe 用: 未処理例外でプロセスを落とす
string? name = null;
Console.WriteLine(name!.Length);   // ここで NullReferenceException

次に、Sysinternals の ProcDump で「未処理例外が起きたらフルダンプを書く」設定にしてこのアプリを起動します。-ma はフルダンプ、-e は「プロセスが未処理例外に遭遇したときにダンプを書く」オプション、-x <出力先フォルダー> <実行ファイル> は指定した実行ファイルを ProcDump 自身が起動して監視するオプションです。10

procdump.exe -accepteula -ma -e -x C:\CrashDumps CrashLab.exe

すでに動いているプロセスを対象にするなら、procdump.exe -accepteula -ma -e CrashLab.exe のようにプロセス名(または PID)を渡します。出力されるダンプの既定のファイル名は PROCESSNAME_YYMMDD_HHMMSS.dmp です。10

できあがった .dmp を第 2.2 節の手順で WinDbg に読ませれば、第 4 章と第 5 章の両方をこの 1 ファイルで練習できます。マネージドオンリーの調査で十分なら、dotnet-dump collect --name CrashLab でも同等のダンプが採れます(第 7 章)。ただし dotnet-dump collect は「任意のタイミングで採る」ツールなので、落ちた瞬間を捕まえたい場合は ProcDump か WER LocalDumps を使ってください。

3. SOS拡張を読み込む

.NET アプリのダンプは、ネイティブの WinDbg コマンドだけでは「マネージドヒープの中身」「C# のスタックフレーム」「例外オブジェクトの中身」が見えません。ここを埋めるのが SOS(Son of Strike)拡張です。ヒープの調査、ヒープ破損の検出、ランタイム内部のデータ型表示、実行中のマネージドコードの状態把握までを SOS コマンド経由で行います。2

3.1 ランタイムによる違い

対象アプリが .NET Framework か .NET (Core) / .NET 5+ かで、ロードすべきランタイムと SOS の由来が変わります。

対象 ランタイム本体 読み込みコマンド
.NET Framework clr.dll .loadby sos clr
.NET Core / .NET 5+ coreclr.dll .loadby sos coreclr

.loadby は、指定したモジュール(clrcoreclr)が置かれているディレクトリから、同じ場所にある拡張 DLL(sos.dll)を探して読み込むコマンドです。フルパスを打たずに、ダンプが採取された環境に対応するバージョンの SOS を確実に拾えるのが利点です。1

バージョン 10.0.18317.1001 以降の WinDbg・cdb では、対象プロセスが coreclr.dll(または Linux/macOS の libcoreclr.so)を読み込んでいることを検知すると、.NET 用の拡張を Microsoft Extension Gallery から自動で読み込みます。1 うまく自動読み込みされない場合や、古いバージョンのデバッガーを使っている場合に、上記の .loadby を手で打つ、という位置づけです。

3.2 SOSが見つからない場合

自動読み込みが効かない環境では、dotnet-sos ツールでローカルにインストールできます。

dotnet tool install --global dotnet-sos
dotnet-sos install

インストール後は WinDbg 上で次のように手動読み込みもできます(古いデバッガーではこちらが必要になる場合があります)。11

.load %USERPROFILE%\.dotnet\sos\sos.dll

3.3 読み込めたか確認する

!sos.help

または、対象が Core 系なら !Threads を、Framework 系なら !sosstatus を試し、エラーにならず何か情報が返ってくれば読み込みは成功です。ここでコマンドが Unable to find module のようなエラーで失敗する場合、ほぼシンボルパスかランタイムの不一致(ダンプ採取環境と手元のランタイムのビット数・バージョン違いなど)が原因です。次の章のコマンド以前に、ここで詰まっているケースは実務でも珍しくありません。

4. 例外とスタックを読む ── !clrstack と !pe

未処理例外でクラッシュしたダンプの、最初の一手です。

!threads

まず !Threads(lldb 環境では clrthreads エイリアス)で、マネージドスレッドの一覧と、各スレッドの Exception 列を確認します。12 例外を持っているスレッドがあれば、そのスレッドに切り替えます。

出力は 1 行 1 スレッドの表で、デバッガー上の通し番号・CLR のスレッド ID・OS のスレッド ID に加えて、実行中のアプリケーションドメインを示す Domain 列、COM のアパートメントモードを示す APT 列、そのスレッドで最後にスローされた例外を示す Exception 列が並びます。12 最初に見るのは Exception 列だけです。ここに System.NullReferenceException のような型名が入っている行が調査対象で、その行の左端にある通し番号を控えます。

~5s
!clrstack

~<番号>s は、控えた通し番号のスレッドに切り替えるコマンドです。続く !CLRStack はマネージドコードだけのスタックトレースを表示します。13 引数や変数まで見たい場合は -a(-l-p を合わせたショートカット)を付けます。

!clrstack -a

出力は次の形になります(Microsoft Learn に掲載されている clrstack の例からの抜粋です。dotnet-dump のセッション例ですが、WinDbg の !clrstack も同じ表示です)。5

OS Thread Id: 0x573d (0)
    Child SP               IP Call Site
00007FFD28B42C58 00007fb22c1a8ed9 [HelperMethodFrame_PROTECTOBJ: 00007ffd28b42c58] System.RuntimeMethodHandle.InvokeMethod(System.Object, System.Object[], System.Signature, Boolean, Boolean)
00007FFD28B42E20 00007FB1B18D33ED SymbolTestApp.Program.Foo4(System.String) [/home/mikem/builds/SymbolTestApp/SymbolTestApp/SymbolTestApp.cs @ 54]
00007FFD28B42ED0 00007FB1B18D2FC4 SymbolTestApp.Program.Foo2(Int32, System.String) [/home/mikem/builds/SymbolTestApp/SymbolTestApp/SymbolTestApp.cs @ 29]
00007FFD28B42F00 00007FB1B18D2F5A SymbolTestApp.Program.Foo1(Int32, System.String) [/home/mikem/builds/SymbolTestApp/SymbolTestApp/SymbolTestApp.cs @ 24]
00007FFD28B42F30 00007FB1B18D168E SymbolTestApp.Program.Main(System.String[]) [/home/mikem/builds/SymbolTestApp/SymbolTestApp/SymbolTestApp.cs @ 19]
00007FFD28B43210 00007fb22aa9cedf [GCFrame: 00007ffd28b43210]

どの行を読むかは次のとおりです。

  1. Call Site 列だけを、下から上に読む。下が呼び出し元、上が呼び出し先です。この例なら MainFoo1Foo2Foo4 と、どういう経路で落ちる場所まで来たかが読み取れます。
  2. 角括弧の [... .cs @ 54] がソースファイル名と行番号。ここが出ていればシンボルは解決できています。出ていないときの対処は第 8 章です。
  3. [HelperMethodFrame_...] [GCFrame: ...] の行は飛ばす。ランタイム内部のフレームで、自社コードのバグとは直接対応しません。
  4. Child SPIP の 2 列は、通常の調査では読まなくてよい。それぞれスタックポインターと命令ポインターで、!dumpstackobjects などにアドレスを渡すときにだけ使います。
  • CLRStack はCLRのメタデータからマネージドフレームを直接列挙するため、シンボルの有無はフレームが表示されるかどうかには影響しません。PDB が不足しているときに消えるのは上記 2. のソースファイル名・行番号だけで、フレーム自体が省略されることはありません。13 シンボルまわりの話は第 8 章に集約したので、行番号が出ないときはそちらを参照してください。
  • 自社コードのフレームが 1 つも見えない場合は、シンボル不足ではなく次を疑ってください。選択しているスレッドが例外を持っていない別スレッドである(スレッド選択のミス)、ネイティブ側だけで落ちておりマネージドフレームがそもそも存在しない、あるいはダンプの種類(Mini など)がその時点のスタック情報を十分に含んでいない、といった原因です。

次に例外オブジェクトそのものを見ます。

!pe

!PrintException(略称 !pe)は、指定しなければ現在のスレッドで最後にスローされた例外を表示します。型名、メッセージ、内部例外(-nested で表示)、スタックトレース文字列までが得られます。14 出力は次の形です(こちらも Microsoft Learn 掲載の例からの抜粋で、-lines を付けてソース情報も出させたものです)。5

Exception object: 00007fb18c038590
Exception type:   System.Reflection.TargetInvocationException
Message:          Exception has been thrown by the target of an invocation.
InnerException:   System.Exception, Use !PrintException 00007FB18C038368 to see more.
StackTrace (generated):
SP               IP               Function
00007FFD28B42E20 00007FB1B18D33ED SymbolTestApp.dll!SymbolTestApp.Program.Foo4(System.String)+0x15d [/home/mikem/builds/SymbolTestApp/SymbolTestApp/SymbolTestApp.cs @ 54]
00007FFD28B42F30 00007FB1B18D168E SymbolTestApp.dll!SymbolTestApp.Program.Main(System.String[])+0x6e [/home/mikem/builds/SymbolTestApp/SymbolTestApp/SymbolTestApp.cs @ 19]

StackTraceString: <none>
HResult: 80131604

読むのは上から 4 行です。Exception type が何の例外か、Message がその説明、そして InnerException が「本当の原因」です。この例のように TargetInvocationException が出ているときは、表に出ている型はリフレクション呼び出しの包み紙にすぎないので、InnerException 行に書かれたアドレスを !pe 00007FB18C038368 のように渡して内側の例外を開きます(!pe -nested でも同じ情報が得られます)。14 HResult は例外に対応する HRESULT の値、StackTrace (generated) 以降は例外が持っているスタックトレースで、!clrstack が「今のスタック」なのに対し、こちらは「例外が投げられた時点のスタック」という違いがあります。両者がずれている場合は、例外がキャッチされて別の場所で投げ直されている可能性を疑ってください。

System.NullReferenceException のように型名だけでは何も語らない例外ほど、!clrstack -a で見えるローカル変数の値と突き合わせる作業が必要になります。

5. ヒープとリークを追う ── !dumpheap -stat と !gcroot

「メモリがじわじわ増えて、数時間〜数日後に落ちる」系の調査で中心になるコマンドです。GC 待ちなのか本物のリークなのかを切り分ける前段の話は「.NETでGC待ちとメモリリークを見分ける」で詳しく書きました。この記事はその続きとして、ダンプを 1 枚読んで「何が握っているか」まで踏み込む部分に相当します。

!dumpheap -stat

-stat オプションはマネージドヒープの統計サマリーだけを表示します。型ごとの件数と合計サイズが多い順に近い形で並ぶので、まず「量で殴っている型」を特定します。15 出力は次の形です(Microsoft Learn のメモリリーク調査チュートリアルに掲載されている実際の出力です)。15

Statistics:
              MT    Count    TotalSize Class Name
00007f6c1eeefba8      576        59904 System.Reflection.RuntimeMethodInfo
00007f6c1dc021c8     1749        95696 System.SByte[]
00000000008c9db0     3847       116080      Free
00007f6c1e784a18      175       128640 System.Char[]
00007f6c1dbf5510      217       133504 System.Object[]
00007f6c1dc014c0      467       416464 System.Byte[]
00007f6c21625038        6      4063376 testwebapi.Controllers.Customer[]
00007f6c20a67498   200000      4800000 testwebapi.Controllers.Customer
00007f6c1dc00f90   206770     19494060 System.String
Total 428516 objects

どの行を読むかは次のとおりです。

  1. 一番下から読む。TotalSize(バイト)の小さい順に並ぶので、最下行がヒープを最も食っている型です。この例では System.String が約 19MB、Customer が約 4.8MB を占めています。
  2. Count 列と TotalSize 列を分けて見る。巨大な配列が数個ある(サイズだけ大きい)のか、小さいオブジェクトが何十万個もある(件数だけ大きい)のかで、疑う場所が変わります。
  3. Free 行はリークではない。回収済みでまだ再利用されていない隙間で、断片化の目安として見る行です。
  4. MT 列(MethodTable のアドレス)は控えておく。次のステップの !dumpheap -mt <MT> に渡すと、その型の個別インスタンスのアドレス一覧が出ます。

実務でよく見る型は次の 2 系統です。

  • 業務クラス自体が増えている(例: MyApp.Models.Customer が数十万件)── どこかに保持され続けている強参照がある
  • System.String や配列だけが極端に多い── 上位に見えている業務クラスの内部データが原因の場合が多く、個別のインスタンスまで見る前に、まず業務クラス側を疑うほうが早いことが多いです

対象を絞ったら、個別インスタンスのアドレスを取ります。

!dumpheap -type MyApp.Models.Customer

-type は型名の部分一致、-mt は前段で控えた MethodTable のアドレス指定です。どちらも出力は「1 行 1 インスタンス」の一覧になります。15

         Address               MT     Size
00007f6ad09421f8 00007f6c20a67498       24
00007f6ad0942210 00007f6c20a67498       24

ここで使うのは左端の Address 列だけです。この値を次のコマンドに渡します。

そして、なぜそのオブジェクトが GC で回収されずに残り続けているのかを調べます。

!gcroot 000001a2b3c4d5e0

!GCRoot はマネージドヒープとハンドルテーブルの全体を検索し、指定したオブジェクトへ到達するルート(スタック上の変数、静的フィールド、GC ハンドルなど)を洗い出します。16 出力は次の形です(こちらも Microsoft Learn 掲載の実際の出力です)。15

Thread 3f68:
    00007F6795BB58A0 00007F6C1D7D0745 testwebapi.Controllers.CustomerCache.GetAll()
        rbx:  (interior)
            ->  00007F6BDFFFF038 System.Object[]
            ->  00007F69D0033570 testwebapi.Controllers.Processor
            ->  00007F69D0033588 testwebapi.Controllers.CustomerCache
            ->  00007F69D00335A0 System.Collections.Generic.List`1[[testwebapi.Controllers.Customer]]
            ->  00007F6C000148A0 testwebapi.Controllers.Customer[]
            ->  00007F6AD0942258 testwebapi.Controllers.Customer

Found 1 root.

どの行を読むかは次のとおりです。

  1. 最初の見出し行がルートの種類。Thread 3f68: ならスレッドのスタック上の変数、HandleTable: なら GC ハンドル((pinned handle) などの種別が併記されます)がルートです。ルートが静的フィールドなら、そのフィールドを持つ型の名前がここに現れます。
  2. -> の連鎖を上から下に読む。「誰が誰を握っているか」の鎖で、末尾が指定したオブジェクトです。
  3. 犯人は末尾ではなく、鎖の途中に出てくるコレクションや長寿命オブジェクト。この例なら CustomerCacheList<Customer> を握りっぱなしにしていることが分かります。ここを直さない限り、末尾の Customer 単体をいくら見ても解決しません。
  4. 最終行の Found N root. で本数を確認する。複数のルートが出た場合、すべてを断たないと回収されません。1 つ直して終わりにしないでください。

出力にキャッシュ用の静的フィールドやイベントハンドラーの購読が見えたら、そこが解除漏れの犯人候補です。次のような「キャッシュに入れっぱなしで解放しない」コードは典型例です。

public static class CustomerCache
{
    // 解除する経路がなく、増え続ける一方の静的辞書
    private static readonly Dictionary<int, Customer> _cache = new();

    public static void Add(Customer c) => _cache[c.Id] = c;
}

!gcroot の出力に CustomerCache のような静的コンテナが現れたら、コード側では有効期限や上限件数、あるいは WeakReference への切り替えを検討する材料になります。15

6. ネイティブクラッシュの自動解析 ── !analyze -v

C++ の DLL、COM、vendor SDK が絡むネイティブクラッシュ(アクセス違反など)では、まずこのコマンドから入ります。

!analyze -v

!analyze はクラッシュ・例外の自動解析を行う拡張コマンドで、-v を付けると詳細表示になります。8 出力は数十行に及びますが、Microsoft Learn のユーザーモード解析例から主要フィールドだけを抜き出すと次のようになります。8

FAULTING_IP:
ntdll!PropertyLengthAsVariant+73
77f97704 cc               int     3

EXCEPTION_RECORD:  ffffffff -- (.exr ffffffffffffffff)
ExceptionAddress: 77f97704 (ntdll!PropertyLengthAsVariant+0x00000073)
   ExceptionCode: 80000003 (Break instruction exception)
  ExceptionFlags: 00000000

BUGCHECK_STR:  80000003

PROCESS_NAME:  MyApp.exe

STACK_TEXT:
0006b9dc 01050963 00000000 0006ba04 000603fd ntdll!PropertyLengthAsVariant+0x73
0006b9f0 010509af 00000002 0006ba04 77e1a449 MyApp!FatalErrorBox+0x55 [D:\source_files\MyApp\util.c @ 541]
0006da04 01029f4e 01069850 0000034f 01069828 MyApp!ShowAssert+0x47 [D:\source_files\MyApp\util.c @ 579]
0006ff70 01062cbf 00000001 00683ed8 00682b88 MyApp!main+0x1e6 [D:\source_files\MyApp\MyApp.c @ 263]

FOLLOWUP_IP:
MyApp!FatalErrorBox+55
01050963 5e               pop     esi

SYMBOL_NAME:  MyApp!FatalErrorBox+55

MODULE_NAME:  MyApp

IMAGE_NAME:  MyApp.exe

出力の中で特に見るべき項目は次の 3 つです。

  • EXCEPTION_CODE / BUGCHECK_STR: 何の種類の異常か(アクセス違反、スタックオーバーフロー、など)。上の例では 80000003(ブレーク命令例外)で、EXCEPTION_RECORDExceptionCode 行に人間向けの説明が併記されます。ユーザーモードのダンプでは BUGCHECK_STR にもこの例外コードが入ります(名前はカーネル由来で紛らわしいですが、バグチェック=ブルースクリーンではありません)。8
  • FAULTING_IP / FOLLOWUP_IP: 実際に落ちた命令アドレスと、対応するモジュール・関数名。FAULTING_IP は「落ちた場所」、FOLLOWUP_IP!analyze が「原因はここだろう」と推定した場所で、両者は一致しないことのほうが多いです。上の例は ntdll で落ちているのに、推定原因は自社の MyApp!FatalErrorBox になっています。
  • MODULE_NAME / IMAGE_NAME: 落ちた場所が自社モジュールか、サードパーティの DLL か。SYMBOL_NAME とあわせて、FOLLOWUP_IP の所属モジュールが書かれます。

STACK_TEXT は先頭行が最も内側(落ちた地点)、下に行くほど呼び出し元です。自社モジュール名(上の例なら MyApp!)が現れる最初の行を上から探し、その行の [ファイル名 @ 行番号] を見るのが最短の読み方になります。

自社モジュールではなくベンダーの DLL 内で落ちている場合、そこから先を追うにはベンダーの PDB が必要になります(たいてい入手できません)。実務では「呼び出し元(自社コードが最後に渡した引数)まで遡って、渡した値がおかしくなかったか」を疑うのが現実的な着地点です。!analyze -vSTACK_TEXT フィールドで、自社コードから何を呼んだ直後に落ちているかを確認してください。

!analyze は例外が起きたダンプ以外でも使えます。ハングを疑う場合は、対象スレッドを選択したうえで次を実行すると、スレッド間のブロック関係を解析してくれます。

!analyze -hang

7. WinDbgを使わない選択肢 ── dotnet-dump analyze

.NET Core / .NET 5+ のマネージドコードだけを調べたい(ネイティブ DLL や COM が絡まない)なら、WinDbg より軽量な dotnet-dump も選択肢です。

dotnet tool install --global dotnet-dump
dotnet-dump analyze C:\CrashDumps\MyApp\MyApp_20260702_101500.dmp

analyze サブコマンドは SOS が preinstall された対話セッションを開き、clrstackdumpheapgcroot などここまで紹介したコマンドの多くを ! プレフィックスなしでそのまま使えます。5

使い分けの目安は次のとおりです。

観点 WinDbg + SOS dotnet-dump analyze
ネイティブスタックフレーム 見える 見えない(マネージドのみ)5
!analyze -v によるネイティブ自動解析 使える 使えない
Linux のダンプ Windows上のWinDbgで解析可能(x64ダンプにはx64版、Arm64ダンプにはx64版、x86ダンプにはx86版を使用)17 対応(同一プラットフォームのビット数のツールを使用)17
macOS のダンプ 非対応(WinDbgのLinuxダンプサポートはmacOSを含まない) 対応(.NET 5以降)5
導入の軽さ インストーラーまたは winget dotnet global tool 1 コマンド
クロスプラットフォームCIへの組み込み 手間がかかる しやすい

「COM や P/Invoke、native DLL が絡む可能性がある」なら WinDbg、「純粋なマネージドコードのメモリリーク調査で、CI や複数プラットフォームでも動かしたい」なら dotnet-dump analyze、というのが実務上の線引きです。両者は SOS のコマンド体系を共有しているので、片方で覚えたコマンドはもう片方でもほぼそのまま使えます。macOS で採取したダンプを扱う場合は、WinDbg は選択肢に入らないため dotnet-dump(または LLDB)一択になる点に注意してください。

8. シンボルが読めなければ何も始まらない

シンボル(PDB)まわりの話は、この章に集約します。ここまでの各章では「PDB があれば行番号が出る」とだけ書いてきましたが、その中身はここだけ読めば足ります。

まず、ここまでの手順の一部は、PDB が正しく読み込めていなくてもそれなりに機能します。!clrstack!dumpheap -stat!gcroot はCLRのメタデータやヒープデータを直接読むため、PDBがなくてもフレームや型情報そのものは表示されます。PDBが欠けて失われるのは、マネージドコードのソースファイル名・行番号と、ネイティブフレーム・ネイティブモジュールのシンボル名(関数名の代わりにアドレスだけが表示される)です。

見たいもの PDB なし PDB あり
マネージドのスタックフレーム(メソッド名・引数の型) 出る 出る
マネージドのソースファイル名・行番号(!clrstack) 出ない 出る13
例外の型・メッセージ・内部例外(!pe) 出る 出る
ヒープ上の型名・件数・サイズ(!dumpheap -stat) 出る 出る
GC ルートへの参照経路(!gcroot) 出る 出る
ネイティブフレームの関数名(k / !analyze -vSTACK_TEXT) 出ない(アドレスのみ) 出る

つまり、ダンプと実行ファイルしか手元にない、といった「まずは何が起きたかだけでも掴みたい」場面では、PDB探しに詰まる前に !threads!clrstack から着手して問題ありません。とはいえ、ソース行まで追って原因箇所を特定したいなら話は別です。第 2 章で .sympath+ に自社 PDB のパスを足しましたが、実務では「配布した EXE/DLL に対応する PDB がどこにあるかわからない」というだけで、ソース行の特定にたどり着けず調査が足踏みする場面が、収集編で書いた話以上に頻繁に起きます。

PDB そのものが何を持っていて何を持っていないか、Portable PDB、そして Source Link(アセンブリの中にソース管理のメタデータを埋め込み、デバッガーが対応するコミット時点のソースを直接取得できるようにする仕組み)については「PDBとは何か」に一本まとめてあります。18 ダンプ解析を継続的な運用に組み込むなら、ビルドごとに PDB を保管し、Source Link を有効にしておくことが、収集設定そのものと同じくらい重要な準備です。ここをサボると、!clrstack の出力からソース行が一切出ず、アドレスと型名だけを手がかりに手探りする羽目になります。

9. 実例 ── ハンドルリーク調査でのダンプ解析

以前書いた「産業用カメラ長期稼働クラッシュ調査 - ハンドルリーク編」は、長時間運転後に突然落ちる産業用カメラ制御アプリの調査で、主犯がメモリリークではなくハンドルリークだった事例です。この種の調査でダンプが効くのは、次のような組み合わせのときです。

  1. !dumpheap -stat でマネージドヒープ側は正常(型ごとの件数・サイズが増え続けていない)ことを確認する
  2. それでもプロセスのハンドル数だけが増えているなら、リークしているのはマネージドオブジェクトではなく OS ハンドル(ファイル、イベント、カメラ SDK が内部で確保するハンドルなど)だと切り分けられる
  3. SafeHandle を持つマネージドラッパーオブジェクトが残っているなら、!gcroot でそのラッパーの GC ルートを追い、「解放されるべきなのに参照が残っている場所」を特定する

つまり !dumpheap -stat は「マネージドヒープの増加かどうか」を判定する分岐点として機能し、そうではないと分かった時点で、Application Verifier のようなネイティブ境界の異常検出ツールに調査の主軸が移ります。この異常系テスト基盤の作り方は「Application Verifierで作るWindows異常系テスト基盤」で扱っています。ダンプ解析は「今起きている状態」を、Application Verifier は「異常を前倒しで再現させる」役割分担になっており、両方を併用するのが長時間運転系の障害調査の定石です。

10. まとめ

クラッシュダンプは「取る」よりも「読む」ほうが習熟に時間がかかりますが、型はそれほど多くありません。

  1. WinDbg を入れ、シンボルパスに Microsoft のパブリックシンボルサーバーと自社 PDB を両方通す(第 2 章)
  2. .NET アプリなら SOS 拡張を読み込む(.loadby sos clr / .loadby sos coreclr、または自動読み込み。第 3 章)
  3. 例外由来なら !clrstack!pe、メモリ増加なら !dumpheap -stat!gcroot、ネイティブクラッシュなら !analyze -v から入る(第 4〜6 章)
  4. 用途が純粋にマネージドの調査なら、軽量な dotnet-dump analyze も検討する(第 7 章)

そしてこれらすべての土台になるのが PDB とシンボルの管理です。ダンプの収集設定を入れるタイミングで、ビルドごとの PDB 保管と Source Link の有効化も一緒に決めておくと、実際に障害が起きたときの調査時間が大きく変わります。自社での解析が難しい・時間が取れないという場合は、ダンプとログを一式お送りいただければこちらで解析することも可能です。

関連記事

関連する相談領域

合同会社小村ソフトでは、クラッシュダンプとログを組み合わせた不具合の原因調査、長期稼働後にだけ発生する障害の切り分け、ダンプ・PDB の保管や解析体制そのものの設計相談を扱っています。

参考リンク

  1. Microsoft Learn, Debugging Managed Code Using the Windows Debugger. .NET Framework のランタイムが clr.dll、.NET Core/.NET 5+ のランタイムが coreclr.dll であること、.loadby によるモジュール近傍からの拡張読み込み、WinDbg 10.0.18317.1001 以降での自動読み込みについて。  2 3

  2. Microsoft Learn, SOS debugging extension. SOS 拡張が managed heap の情報収集、ヒープ破損の検出、ランタイム内部データ型の表示に使えること、WinDbg 上での構文が ![command] であることについて。  2 3

  3. Microsoft Learn, Install the Windows debugger. WinDbg の winget / Microsoft Store 経由のインストール方法、対応 OS(Windows 10 1607 以降・Windows 11)とアーキテクチャ(x64・ARM64)、自動更新の挙動について。  2 3

  4. Microsoft Learn, Symbol path for Windows debuggers. _NT_SYMBOL_PATH 環境変数によるシンボルパスの設定方法と、.symfix コマンドでパブリックシンボルサーバーへの既定パスを設定できることについて。  2

  5. Microsoft Learn, Dump collection and analysis utility (dotnet-dump). dotnet-dump analyze が SOS コマンドをそのまま使える対話セッションを提供する一方、ネイティブなデバッガーではないためネイティブスタックフレームの表示ができないこと、macOS対応は.NET 5以降であることについて。  2 3 4 5 6

  6. Microsoft Learn, Open a Dump File with WinDbg. File メニューの Open crash dump(Ctrl+D)からダンプを開く手順と、コマンドラインの -y(シンボルパス)・-i(イメージパス)・-z(ダンプファイル)オプションについて。 

  7. Microsoft Learn, Windows Debugger WinDbg Overview. WinDbg の Command・Source code・Disassembly・Breakpoints などのツールウィンドウ構成と、File メニューからの接続設定・設定変更について。 

  8. Microsoft Learn, Using the !analyze Extension および !analyze (WinDbg). !analyze -v によるクラッシュ・例外の自動解析と、出力に含まれる FAULTING_IPMODULE_NAME などのフィールドの意味、ハング調査向けの !analyze -hang について。  2 3 4

  9. Microsoft Learn, Microsoft public symbol server. srv*DownstreamStore*https://msdl.microsoft.com/download/symbols 形式のシンボルパス構文と、.symfix によるローカルキャッシュ付き設定について。 

  10. Microsoft Learn, ProcDump - Sysinternals. -ma(フルダンプ)、-e(未処理例外の発生時にダンプを書く)、-x <Dump_Folder> <Image_File>(対象を起動して監視する)の各オプションと、既定のダンプファイル名 PROCESSNAME_YYMMDD_HHMMSS.dmp について。  2

  11. Microsoft Learn, SOS installer (dotnet-sos). dotnet-sos install によるローカルへの SOS 拡張インストールと、旧バージョンのデバッガーでの手動読み込みコマンドについて。 

  12. Microsoft Learn, SOS debugging extension - Commands. Threads(lldb 環境での別名 clrthreads)コマンドが、各スレッドの ID・ドメイン・最後にスローされた例外などを一覧表示することについて。  2

  13. Microsoft Learn, SOS debugging extension - Commands. CLRStack コマンドがマネージドコードのみのスタックトレースを表示し、-a オプションでローカル変数と引数の両方を表示すること、シンボル(SYMOPT_LOAD_LINES)はソースファイル名・行番号の表示可否のみに影響し、フレーム自体の表示には関係しないことについて。  2 3

  14. Microsoft Learn, SOS debugging extension - Commands. PrintException(pe)コマンドが、アドレス省略時は現在のスレッドで最後にスローされた例外を表示し、-nested で入れ子の例外も表示できることについて。  2

  15. Microsoft Learn, Debug a memory leak in .NET および Dump collection and analysis utility (dotnet-dump) - Analyze memory leaks and allocations. dumpheap -stat による型ごとの件数・合計サイズの統計表示と、それを起点にした調査の進め方について。  2 3 4 5

  16. Microsoft Learn, SOS debugging extension - Commands. GCRoot コマンドがマネージドヒープとハンドルテーブル全体を検索し、指定オブジェクトへの参照(ルート)を洗い出すことについて。 

  17. Microsoft Learn, Debug Linux dumps. LinuxのダンプをWindows上でWinDbgまたはdotnet-dumpで解析できること、採取環境(x64/Arm64/x86)のビット数に合わせたツールのバージョンを使う必要があることについて。  2

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

.NETアプリのクラッシュダンプはどのコマンドから読み始めればよいですか?
調査の型は3つです。未処理例外で落ちたなら!threadsで例外を持つスレッドを特定し、!clrstackでマネージドスタックを表示して!peで例外オブジェクトの型・メッセージ・内部例外を確認します。メモリが増え続ける調査なら!dumpheap -statで量の多い型を特定し、!gcrootでそのオブジェクトを握っているルート(静的フィールドやイベントハンドラーの購読)を洗い出します。ネイティブクラッシュなら!analyze -vの自動解析から入ります。
WinDbgでSOS拡張はどうやって読み込みますか?
.NET Frameworkなら.loadby sos clr、.NET Core/.NET 5+なら.loadby sos coreclrです。バージョン10.0.18317.1001以降のWinDbgは、対象プロセスがcoreclr.dllを読み込んでいることを検知するとSOSを自動で読み込みます。自動読み込みが効かない環境ではdotnet-sosツールでローカルにインストールし、手動で.loadすることもできます。読み込めたかは!sos.helpや!Threadsがエラーにならず返ってくるかで確認します。
PDB(シンボル)がなくてもダンプ解析はできますか?
ある程度はできます。!clrstack・!dumpheap -stat・!gcrootはCLRのメタデータやヒープデータを直接読むため、PDBがなくてもフレームや型情報は表示されます。失われるのはマネージドコードのソースファイル名・行番号と、ネイティブフレームのシンボル名です。ソース行まで追って原因箇所を特定したいなら、Microsoftのパブリックシンボルサーバーと自社PDBの場所を両方シンボルパスに通す必要があります。ビルドごとのPDB保管とSource Linkの有効化が運用上重要です。
dotnet-dump analyzeとWinDbgはどう使い分けますか?
COMやP/Invoke、ネイティブDLLが絡む可能性があるならWinDbg、純粋なマネージドコードの調査ならdotnet-dump analyzeが目安です。dotnet-dumpはdotnet global tool 1コマンドで導入でき、clrstackやdumpheapなどSOSコマンドの多くをそのまま使えますが、ネイティブスタックフレームは扱えず!analyze -vも使えません。macOSで採取したダンプはWinDbgでは解析できないため、dotnet-dumpまたはLLDB一択になります。両者はSOSのコマンド体系を共有しているので、覚えたコマンドはほぼ相互に使えます。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る