【Visual Studio C++】アプリクラッシュのデバッグ方法まとめ
アプリケーションがクラッシュするような障害が見つかった場合、その原因を特定することはとても時間がかかります。ここでは、クラッシュする場合に有効なWinDbgを使った調査方法をまとめています。
ダンプを使った調査
ダンプとは?
クラッシュの原因の多くはバッファオーバーランやスタック、ヒープの破壊などであり、多くの場合、問題のコードとは別な場所でクラッシュします。そのため、再現させようと思っても再現しない、現象が毎回違うといったことが発生し、原因調査しても真の原因までたどり着くためには非常に時間がかかってしまいます。
そこで有効なのがメモリのダンプを使った調査です。ダンプとは、ある状況でのメモリのスナップショットのことです。障害が発生したときのダンプがあれば、そこからAccess Violationが起きているとか、0除算が起きているといったことが分かります。
実際のところ、問題が起きてしばらくたってからクラッシュすることが多いです。ダンプはクラッシュしたときのメモリのスナップショットでしかないので、問題のコードを実行したタイミングでのメモリの状況ではないことが多いです。なので、ダンプがあれば100%原因が特定できるというものではありません。その点は認識しておく必要があります。
100%でないとはいえダンプは非常に重要な情報です。
再現率の低い障害の場合には、何度も実行して原因を特定していくことは困難です。1度起きた時の情報をできる限り集め調査した方が効率的です。ダンプさえあれば現象を発生したときのメモリの状況を、時間をかけて調査ができます。ダンプはその時何が起きていたのかを知ることのできる重要な情報なのです。
また、現象が同じでも原因が異なるという場合がありえます。お客様の環境で起きている現象の原因と、開発チームで起きている現象は一見同じ現象でも、実は原因が違っており、せっかく修正したのにお客様の環境では修正されていない、なんてことも起きえます。
しかし、お客様の環境のダンプがあれば、間違いなくお客様で起きている現象を調査できるのです。
マップ(.map)/シンボル(.pdb)/アセンブリ(.cod) ファイルとは?
ダンプさえあれば調査を開始できるわけではありません。事前に準備をしておく必要があります。具体的には、以下になります。
- アプリケーションをビルドしたソースコードを保管しておく
- ビルドしたアプリケーションを保管しておく
- アプリケーションをビルドする際に、.map/.pdb/.cod ファイルを出力しておく
ソースコードとビルドしたアプリケーションは通常必ず保管しておくので問題ないでしょう。忘れられがちなのが、.map/.pdb/.cod ファイルです。
.map/.pdb/.cod ファイルは、アプリケーションの実行には必要ないデバッグ情報ですが、ダンプ調査の際にこれらの情報がないと進められません。マップ/シンボル/アセンブリ ファイルはデバッグの際に以下のような目的で使用します。
- マップ(.map)
- マップはテキストファイルで、これを見ると関数名と関数がロードされるアドレスが分かる。アプリケーションがクラッシュすると、Windowsのイベントログなどにクラッシュしたときのプログラムのアドレスが記録として残る場合があります。このアドレスから問題の関数名を特定する際に役立ちます。
- アセンブリ(.cod)
- .codファイルはソースコードとアセンブリを紐づけるための情報です。マップファイルでは関数名の特定までしかできませんが、.codファイルを使用すれば問題のソース行まで特定できます。
- シンボル(.pdb)
- 役割としては基本的にマップ/アセンブリファイルと一緒で、プログラムのアドレスと関数名、ソースコードを紐づけするための情報で、アドレスから関数名を特定するのに役立ちます。シンボルファイルはWinDbgやVisual Studio等のデバッガ読み込ませることができるので、読み込ませておくと自動的にアドレスから関数名を特定してくれます。
もし、アプリケーションのビルド時にこれらを出力していなければ、出力するようにプロジェクトの設定をしましょう。プロジェクトの設定方法は後述します。
事前準備
先に記載した通り、アプリケーションのビルド時に必要なファイルが出力されるようにプロジェクトの設定を行います。
マップ(.map)の出力設定
プロジェクトのプロパティを開きます。
[ソリューションエクスプローラー]で設定するプロジェクトを右クリックし、[プロパティ]を選択するとプロジェクトの設定が開きます。
[マップファイルの生成]を[はい (/MAP)]に設定します。
[リンカー]-[デバッグ]-[マップファイルの生成]を[はい (/MAP)]に設定します。
[OK]を押下し、設定は完了です。
シンボル(.pdb)の出力設定
プロジェクトのプロパティを開きます。
[ソリューションエクスプローラー]で設定するプロジェクトを右クリックし、[プロパティ]を選択するとプロジェクトの設定が開きます。
[デバッグ情報の生成]を[デバッグ情報の生成 (/DEBUG)]に設定します。
[リンカー]-[デバッグ]-[デバッグ情報の生成]を[デバッグ情報の生成 (/DEBUG)]に設定します。
[OK]を押下し、設定は完了です。
アセンブリ(.cod)の出力設定
プロジェクトのプロパティを開きます。
[ソリューションエクスプローラー]で設定するプロジェクトを右クリックし、[プロパティ]を選択するとプロジェクトの設定が開きます。
[アセンブリの出力]を[アセンブリコード、コンピューター語コード、ソースコード (/FAcs)]に設定します。
[C/C++]-[出力ファイル]-[アセンブリの出力]を[アセンブリコード、コンピューター語コード、ソースコード (/FAcs)]に設定します。
[OK]を押下し、設定は完了です。
アプリケーションのビルド、生成物の保管
以上の設定をしたうえで、アプリケーションをビルドします。ビルドしたアプリケーションの実行ファイル(.exe等)はリリースすると思いますが、ビルドした際の以下の生成物は大切に保管しておきましょう。リリース後に見つかったバグをデバッグする上で大切な情報です。
- アプリケーションの実行ファイル(.exe等)
- ソースコード
- マップファイル(.map)
- シンボルファイル(.pdb)
- アセンブリファイル(.cod)
ダンプ(.dmp)の取得方法
リリースしたアプリケーションにクラッシュするバグが見つかった場合、以下の手順でダンプを取得します。もしユーザー先でしか発生しない場合、可能であればユーザーに取得・送付してもらいます。
ダンプを取得するにはレジストリを操作する必要があります。レジストリを操作するスクリプトを用意しましたのでダウンロードしてご利用ください。「ダンプの取得開始レジストリ」を適用すると、ダンプが出力されるようになり、「ダンプの取得終了レジストリ」を適用するとダンプが出力されなくなります。(レジストリ操作を伴うので、使用は自己責任でお願いいたします。)
ダンプの取得開始レジストリ(StartDump.reg)をダブルクリックする。
ユーザーアカウント制御メッセージで[はい]を選択する。
レジストリエディターメッセージで[はい]を選択する。
ダンプ取得の準備完了です。
対象のアプリケーションを操作し、ダンプを取得します。
アプリケーションがクラッシュする操作をすると、デスクトップに「Debug」フォルダが作成され、Debugフォルダの中にダンプファイルが作成されます。ダンプの取得終了レジストリ(EndDump.reg)をダブルクリックする。
ユーザーアカウント制御メッセージで[はい]を選択する。
レジストリエディターメッセージで[はい]を選択する。
ダンプ取得の終了です。
ダンプ(.dmp)を使った調査方法
ダンプが取得できたら、アプリケーションのクラッシュ原因を調査していきます。調査で使用するツールは「WinDbg Preview」です。
以前は、「WinDbg」というツールが主流でしたがUIが独特で操作しづらいツールでしたが、WinDbg PreviewはモダンなUIでWinDbgより格段に操作しやすくなっています。またWinDbg PreviewにはTTD (Time Travel Debugging)という時間を巻き戻してデバッグできる機能が備わっています。記事の初めにも書きましたが、根本原因はクラッシュ時よりも前で起きていることが多くあります。TTDを使うと根本原因までさかのぼって調べることが可能になります。
TTDを使用するにはダンプ(.dmp)ではなく、トレース(.run)を取得する必要があります。トレースを使った調査方法は手順が異なるので後述します。
ここでは、ダンプ(.dmp)を使った調査方法を記載します。
基本的な調査手順
基本となる!analyze -v
を使った調査手順を記載します。ダンプ(.dmp)が取得できたらまず実行すべき調査です。
WinDbg Previewを起動します。
[ファイル]-[Start debugging]-[Open dump file]をクリックします。
ファイル選択ダイアログが開くのでダンプファイルを開きます。
ダンプファイルを開くと以下のような画面がでます。すでに「Access Violation」の文字が出ています。
[ファイル]-[Settings]を選択します。
[Browse…]ボタンを押し、[Source path]と[Symbol path]を選択します。
[Source path]にはアプリケーションのソースコードのルートパスを選択します。[Symbol path]にはアプリケーションのシンボル(.pdb)のあるフォルダパスを選択します。
この時、「srv*」は残したままとしてください。「srv*」はMicrosoftのシンボルサーバーを指していて、これを指定しておくとインターネットからWindows OSのシンボル(.pdb)を取得してくれます。こうしておくことで、APIの名称やOS内の関数名がデバッグ結果に表示されるようになります。ソースコードとシンボルのパスが正しいか確認します。
以下のように「OK」が出ていれば正しくパスが設定されています。
!analyze -v
を実行してダンプの解析をします。!analyze -v
の解析結果を確認します。!analyze -v
は基本的な解析となりますが、クラッシュの原因はこれで分かります。以下に実際の解析結果と確認すべき個所を示します。(確認すべき個所はマーカーをつけています)
0:000> !analyze -v
*******************************************************************************
* *
* Exception Analysis *
* *
*******************************************************************************
KEY_VALUES_STRING: 1
Key : AV.Dereference
Value: NullPtr
Key : AV.Fault
Value: Write
Key : Analysis.CPU.Sec
Value: 1
Key : Analysis.DebugAnalysisProvider.CPP
Value: Create: 8007007e on DESKTOP-XXXXXXX
Key : Analysis.DebugData
Value: CreateObject
Key : Analysis.DebugModel
Value: CreateObject
Key : Analysis.Elapsed.Sec
Value: 2
Key : Analysis.Memory.CommitPeak.Mb
Value: 65
Key : Analysis.System
Value: CreateObject
Key : Timeline.OS.Boot.DeltaSec
Value: 189091
Key : Timeline.Process.Start.DeltaSec
Value: 4
ADDITIONAL_XML: 1
NTGLOBALFLAG: 0
PROCESS_BAM_CURRENT_THROTTLED: 0
PROCESS_BAM_PREVIOUS_THROTTLED: 0
APPLICATION_VERIFIER_FLAGS: 0
CONTEXT: (.ecxr) rax=0000000000000000 rbx=0000014bebf63740 rcx=00007ffaf57e1490
rdx=00007ff6d9253270 rsi=0000000000000000 rdi=0000014bebf688a0
rip=00007ff6d9251037 rsp=00000042a454f8a0 rbp=0000000000000000
r8=0000000000000004 r9=00000042a454f768 r10=0000000000000015
r11=00000042a454f7a0 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
AccessViolation!main+0x37:
00007ff6`d9251037 c7000a000000 mov dword ptr [rax],0Ah ds:00000000`00000000=????????
Resetting default scope
EXCEPTION_RECORD: (.exr -1) ExceptionAddress: 00007ff6d9251037 (AccessViolation!main+0x0000000000000037)
==ExceptionCode: c0000005 (Access violation)==
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 0000000000000001
Parameter[1]: 0000000000000000
Attempt to write to address 0000000000000000
==PROCESS_NAME: AccessViolation.exe==
WRITE_ADDRESS: 0000000000000000
ERROR_CODE: (NTSTATUS) 0xc0000005 - 0x%p 0x%p Q B %s B
EXCEPTION_CODE_STR: c0000005
EXCEPTION_PARAMETER1: 0000000000000001
EXCEPTION_PARAMETER2: 0000000000000000
STACK_TEXT:
00000042`a454f8a0 00007ff6`d9251514 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ==AccessViolation!main+0x37==
00000042`a454f8d0 00007ffb`2fac6fd4 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : AccessViolation!__scrt_common_main_seh+0x10c
00000042`a454f910 00007ffb`304dcf31 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0x14
00000042`a454f940 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21
FAULTING_SOURCE_LINE: D:\Users\username\Documents\Visual Studio 2019\Source\AccessViolation\AccessViolation\AccessViolation.cpp
==FAULTING_SOURCE_FILE: D:\Users\username\Documents\Visual Studio 2019\Source\AccessViolation\AccessViolation\AccessViolation.cpp
FAULTING_SOURCE_LINE_NUMBER: 13==
FAULTING_SOURCE_CODE:
9:
10: int* buffer = nullptr;
11: *buffer = 10;
12:
==> 13: std::cout << "< main()" << std::endl;==
14: }
SYMBOL_NAME: AccessViolation!main+37
MODULE_NAME: AccessViolation IMAGE_NAME: AccessViolation.exe
STACK_COMMAND: ~0s ; .ecxr ; kb
FAILURE_BUCKET_ID: NULL_POINTER_WRITE_c0000005_AccessViolation.exe!main
OS_VERSION: 10.0.19041.1
BUILDLAB_STR: vb_release
OSPLATFORM_TYPE: x64
OSNAME: Windows 10
FAILURE_ID_HASH: {0c1e63ab-c745-b583-df3f-7f81d7fbdeac}
Followup: MachineOwner
---------
- ExceptionCode
「c0000005 (Access violation)」となっていることから、Access violationが起きてアプリケーションがクラッシュしていることが分かります。 - PROCESS_NAME
「AccessViolation.exe」が原因のモジュールであることが分かります。 - STACK_TEXT
STACK_TEXTはクラッシュしたときのスタックトレースです。「AccessViolation!main+0x37」となっていることから、AccessViolationというモジュールのmain()関数の中でAccessViolationが起きていることが分かります。 - FAULTING_SOURCE_FILE, FAULTING_SOURCE_LINE_NUMBER, FAULTING_SOURCE_CODE
AccessViolation.cppの13行目のコードが問題のコードであることが分かります。**このサンプルのコードでは本来なら11行目が原因コードですが、最適化によりコードの行数がずれています。**このようにコード行はずれる場合があるので注意してください。
このように、ダンプ(.dmp)があれば問題のソースコードの行数まで知ることができます。もちろん、できない場合も多々あります。その場合は、さらに突っ込んだ調査をすることになりますが、それらについては随時追加していくことにします。
Appendix
よく使うWinDbgコマンド
追加予定。
よくある原因と対処方法
追加予定。
最後まで読んでいただきありがとうございます。
また読んでくださいませ。
そんじゃーね。