.NETアプリのクラッシュダンプをイベントビューアとWinDBGで解析する

ダンプ/デバッグ関連記事
https://tera1707.com/entry/2022/02/06/144447#WindowsDump

やりたいこと

.NET6(C#)で作ったアプリについて、
アプリがクラッシュした(落ちた)ときに採取したダンプファイルを、
WinDBGを使って解析して、
自分のコードのどこでなぜ落ちたのかを調べたい。

※個人的に、再現が簡単なものであれば、デバッグビルド版に差し替えて再現させて、その時のexe、pdb、コード一式とダンプファイルをVisualStudioでデバッグするのがお手軽だと思うのだが、そうもいかないときのために上記のやり方を調べたい。

そうもいかないときとは、、、
今知っている範囲で、VisualStudioでダンプを解析するには、「落ちたときのexeをビルドしたときにできたpdb」がペアで必要っぽい。(あとから同じコードをビルドしてできたpdbではダメ!→そうでもなさそう?後から作ったpdbでも、ソースコード名と行数まで見えてる気がする)

そういうときに、アプリのクラッシュダンプを調べるのは、今知ってる範囲では、WinDBG一択っぽい。
(WinDBGだと、別で同じコードから作ったpdbでも、強制的にマッチさせられるらしい?) →うーん、VSでも、後から作ったpdbで、停止個所のコードで、その時の変数等も見えてる気がする?開発機でやってるから?要調査。 →やっぱりVSでも、後から作ったpdbを呼んでくれるし、コードのフォルダを指定したら、普通にデバッグできた。これは.NETだからなのか?(pdbの強制マッチングみたいなのは.NETでは不要なのか?非.NETなら必要なのか?調べたい。)

なので、仕方なくWinDBGでのダンプ調査をやりたい。(本当に強制的にマッチさせられるのかどうかも含めて) (上に書いた通り、VSでもデバッグできそう。だが、せっかくWinDBGやってみたので一応最後まで調べようと思う)

つかったもの

.NETアプリクラッシュ時の調査方法の例

.NETアプリが、どこでなぜ落ちたかをWinDBGで練習で追いかけたときにやったことのメモ。

以下を、順番にやっていった。

ダンプが残るようWindowsに設定を行う

まずは、アプリクラッシュ時にダンプを出力するように事前に設定を行っておく。

ダンプ出力には、Windows Error Reportingという仕組みを利用する。

やり方は以前の記事を参照。

https://qiita.com/tera1707/items/ec33fd8f0e2515c9ad4c

イベントビューアで、起きた例外が何かを見る

アプリが落ちたときには、イベントログが残っていることがある。

イベントビューアのログは、上のダンプを残すためのWERを行っていなくても残るので、 まずは、いきなりダンプに行かずに、イベントログが残っているかを見る。

イベントビューアーは、Windowsキーを押してスタートメニューを出して、「event」と入れると出てくる。

イベントビューアーを開いて、 [Windowsログ] > [Application] を選ぶと、アプリのイベントの一覧が出てくる。 その中から、自分のアプリの落ちたログを探す。

今回の場合だと、下記のようなログが残っていた。

ログの中には「例外コード」の表記があり、そこに「0xc0000094」とある。

そういう例外コードでおちたのだな、とここでまずわかる。

例外コードがどういう例外を示してるのか調べる

下記に、Windowsの例外コードの一覧がある。

https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/1bc92ddf-b79e-413c-bbaa-99a5281a6c90

とりあえず、一番新しい「Published Version」のpdfをダウンロードして、見る。

pdfの中で、上のコードが何かを、検索する。

今回の場合の例外コードは「0除算」のものであることがここで分かる。

WinDBGデバッグする

ダンプ調査に使う、WinDBGのコマンドは下記にメモした。

https://tera1707.com/entry/2023/04/08/005435

ここから、WinDBGを使って例外おきたときの状況を見ていく。

!threads

まず!threadsで、アプリ落ちたときに動いていたスレッドを見る。

その中で、例外の表示が出ている奴がいることがわかる。

また、その例外が「System.DivideByZeroException」なので、0除算であることがわかる。
この時点で、イベントビューアで見た例外と一致するので、こいつがクサいな、とわかる。

~0s

~<スレッド番号>sで、そのクサいスレッドに移動する。

!pe

!peで、選択中のスレッドで最後に発生した例外(イコールおそらくアプリが落ちたときの例外)のコールスタックを見る。

こんな感じで出てくる。
これを見ると、一番上が「SubFunc」というメソッドなので、あぁそのメソッドで落ちたんだな、とわかる。

で、今回はそこが自分の作ったメソッドなので、そいつが呼ばれたときにどういう状態だったのか?を調べることにする。

!clrstack -a

!clrstack -a で、コールスタックにあるメソッドが、どういう引数で、どういうローカル変数の値だったのかを見る。

とりあえず、そういう値だったんだなとわかる。

!u <メソッドのコールアドレス>

!u <メソッドのコールアドレス>で、落ちたときに呼ばれていたメソッドの逆アセンブルをする。

!clrstack で出ていた「IP」の部分の値を使うので、今回は「!u 00007ff805b5b3cc」を実行する。

そうすると、これが出てくる。

この中の「>>>」がついている

>>> 00007ff8`05b5b3cc f77d20          idiv    eax,dword ptr [rbp+20h]

が、クラッシュした部分。なので、このメソッドに入ったところから、落ちた部分に至るまでに何が起きていたかをアセンブラで追いかければ、どうやってクラッシュしたのかがわかる。

アセンブラの見方

下記を参照

https://tera1707.com/entry/2023/04/09/222221

グローバル変数の見方

別途調べる。。。

参考

.NETアプリのダンプ方法
めちゃわかりやすい

https://troushoo.blog.fc2.com/blog-entry-61.html

レジスタの説明など(x64)

https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/x64-architecture

アセンブラ命令の説明(x64)

https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/x64-instructions

アセンブラ命令の説明(x86)

https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/x86-instructions

レジスタの説明

https://ja.wikibooks.org/wiki/X86%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%A9/x86%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3

SOSコマンド

https://learn.microsoft.com/ja-jp/dotnet/framework/tools/sos-dll-sos-debugging-extension

例外とそれが起きてるスレッドの調べかた

https://troushoo.blog.fc2.com/blog-entry-9.html

WERでダンプをとる方法

https://qiita.com/tera1707/items/ec33fd8f0e2515c9ad4c

ダンプ調査に使う、WinDBGのコマンド

https://tera1707.com/entry/2023/04/08/005435