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

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

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

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

1. まず結論

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

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

2.1 インストール

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

winget install Microsoft.WinDbg

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

2.2 ダンプを開く

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

-z はダンプファイルを指定して起動するオプションです。GUI から「File > Open Dump File」でも同じことができます。

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

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

.symfix C:\Symbols\Microsoft
.sympath+ C:\Symbols\MyApp
.reload
  • .symfix は Microsoft のパブリックシンボルサーバー(https://msdl.microsoft.com/download/symbols)へのパスを、指定したローカルキャッシュ付きで設定するショートカットです。OS 標準 DLL のシンボルはここから自動ダウンロードされます。5
  • .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 のままなら、まだシンボルが解決されていない状態です。

3. SOS拡張を読み込む

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

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 を確実に拾えるのが利点です。6

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

3.2 SOSが見つからない場合

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

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

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

.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 列を確認します。8 例外を持っているスレッドがあれば、そのスレッドに切り替えます。

~5s
!clrstack

!CLRStack はマネージドコードだけのスタックトレースを表示します。9 引数や変数まで見たい場合は -a(-l-p を合わせたショートカット)を付けます。

!clrstack -a
  • 自社コードのメソッドが並んでいれば、そのまま「どこで」「どんな呼び出し経路で」落ちたかが読めます。ソースファイル名・行番号まで出るのは、シンボルが正しく読めている(第 2 章)からです。CLRStack はCLRのメタデータからマネージドフレームを直接列挙するため、シンボルの有無はフレームが表示されるかどうかには影響しません。 シンボルが不足している場合に失われるのはソースファイル名・行番号だけで、フレーム自体が省略されることはありません。9
  • 自社コードのフレームが 1 つも見えない場合は、シンボル不足ではなく次を疑ってください。選択しているスレッドが例外を持っていない別スレッドである(スレッド選択のミス)、ネイティブ側だけで落ちておりマネージドフレームがそもそも存在しない、あるいはダンプの種類(Mini など)がその時点のスタック情報を十分に含んでいない、といった原因です。

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

!pe

!PrintException(略称 !pe)は、指定しなければ現在のスレッドで最後にスローされた例外を表示します。型名、メッセージ、内部例外(-nested で表示)、スタックトレース文字列までが得られます。10 System.NullReferenceException のように型名だけでは何も語らない例外ほど、!clrstack -a で見えるローカル変数の値と突き合わせる作業が必要になります。

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

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

!dumpheap -stat

-stat オプションはマネージドヒープの統計サマリーだけを表示します。型ごとの件数と合計サイズが多い順に近い形で並ぶので、まず「量で殴っている型」を特定します。11 実務でよく見る型は次の 2 系統です。

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

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

!dumpheap -type MyApp.Models.Customer

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

!gcroot 000001a2b3c4d5e0

!GCRoot はマネージドヒープとハンドルテーブルの全体を検索し、指定したオブジェクトへ到達するルート(スタック上の変数、静的フィールド、GC ハンドルなど)を洗い出します。12 出力にキャッシュ用の静的フィールドやイベントハンドラーの購読が見えたら、そこが解除漏れの犯人候補です。次のような「キャッシュに入れっぱなしで解放しない」コードは典型例です。

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 への切り替えを検討する材料になります。11

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

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

!analyze -v

!analyze はクラッシュ・例外の自動解析を行う拡張コマンドで、-v を付けると詳細表示になります。13 出力の中で特に見るべき項目は次の 3 つです。

  • EXCEPTION_CODE / BUGCHECK_STR: 何の種類の異常か(アクセス違反、スタックオーバーフロー、など)
  • FAULTING_IP / FOLLOWUP_IP: 実際に落ちた命令アドレスと、対応するモジュール・関数名
  • MODULE_NAME / IMAGE_NAME: 落ちた場所が自社モジュールか、サードパーティの DLL か

自社モジュールではなくベンダーの 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 などここまで紹介したコマンドの多くを ! プレフィックスなしでそのまま使えます。4

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

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

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

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

ここまでの手順の一部は、シンボル(PDB)が正しく読み込めていなくてもそれなりに機能します。 第 4 章で書いたとおり !clrstack!dumpheap -stat!gcroot はCLRのメタデータやヒープデータを直接読むため、PDBがなくてもフレームや型情報そのものは表示されます。PDBが欠けて失われるのは、マネージドコードのソースファイル名・行番号と、ネイティブフレーム・ネイティブモジュールのシンボル名(関数名の代わりにアドレスだけが表示される)です。ダンプと実行ファイルしか手元にない、といった「まずは何が起きたかだけでも掴みたい」場面では、PDB探しに詰まる前に !threads!clrstack から着手して問題ありません。とはいえ、ソース行まで追って原因箇所を特定したいなら話は別です。第 2 章で .sympath+ に自社 PDB のパスを足しましたが、実務では「配布した EXE/DLL に対応する PDB がどこにあるかわからない」というだけで、ソース行の特定にたどり着けず調査が足踏みする場面が、収集編で書いた話以上に頻繁に起きます。

PDB そのものが何を持っていて何を持っていないか、Portable PDB、そして Source Link(アセンブリの中にソース管理のメタデータを埋め込み、デバッガーが対応するコミット時点のソースを直接取得できるようにする仕組み)については「PDBとは何か」に一本まとめてあります。15 ダンプ解析を継続的な運用に組み込むなら、ビルドごとに 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, Install the Windows debugger. WinDbg の winget / Microsoft Store 経由のインストール方法、対応 OS(Windows 10 1607 以降・Windows 11)とアーキテクチャ(x64・ARM64)、自動更新の挙動について。  2 3

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

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

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

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

  6. 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

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

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

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

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

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

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

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

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

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

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

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

著者プロフィール

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

小村 豪

合同会社小村ソフト 代表

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

ブログ一覧に戻る