【2023年】これからブログを始める人へおすすめの書籍

Kissy

【Visual Studio C++】アプリクラッシュのデバッグ方法まとめ

作成: 更新:

【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)の出力設定

  1. プロジェクトのプロパティを開きます。
     [ソリューションエクスプローラー]で設定するプロジェクトを右クリックし、[プロパティ]を選択するとプロジェクトの設定が開きます。
     ソリューションエクスプローラー
  2. [マップファイルの生成]を[はい (/MAP)]に設定します。
     [リンカー]-[デバッグ]-[マップファイルの生成]を[はい (/MAP)]に設定します。
     マップファイルの生成
  3. [OK]を押下し、設定は完了です。

シンボル(.pdb)の出力設定

  1. プロジェクトのプロパティを開きます。
     [ソリューションエクスプローラー]で設定するプロジェクトを右クリックし、[プロパティ]を選択するとプロジェクトの設定が開きます。
     ソリューションエクスプローラー
  2. [デバッグ情報の生成]を[デバッグ情報の生成 (/DEBUG)]に設定します。
     [リンカー]-[デバッグ]-[デバッグ情報の生成]を[デバッグ情報の生成 (/DEBUG)]に設定します。
     デバッグ情報の生成
  3. [OK]を押下し、設定は完了です。

アセンブリ(.cod)の出力設定

  1. プロジェクトのプロパティを開きます。
     [ソリューションエクスプローラー]で設定するプロジェクトを右クリックし、[プロパティ]を選択するとプロジェクトの設定が開きます。
     ソリューションエクスプローラー
  2. [アセンブリの出力]を[アセンブリコード、コンピューター語コード、ソースコード (/FAcs)]に設定します。
     [C/C++]-[出力ファイル]-[アセンブリの出力]を[アセンブリコード、コンピューター語コード、ソースコード (/FAcs)]に設定します。
     アセンブリの出力
  3. [OK]を押下し、設定は完了です。

アプリケーションのビルド、生成物の保管

以上の設定をしたうえで、アプリケーションをビルドします。ビルドしたアプリケーションの実行ファイル(.exe等)はリリースすると思いますが、ビルドした際の以下の生成物は大切に保管しておきましょう。リリース後に見つかったバグをデバッグする上で大切な情報です。

  • アプリケーションの実行ファイル(.exe等)
  • ソースコード
  • マップファイル(.map)
  • シンボルファイル(.pdb)
  • アセンブリファイル(.cod)

ダンプ(.dmp)の取得方法

リリースしたアプリケーションにクラッシュするバグが見つかった場合、以下の手順でダンプを取得します。もしユーザー先でしか発生しない場合、可能であればユーザーに取得・送付してもらいます。

ダンプを取得するにはレジストリを操作する必要があります。レジストリを操作するスクリプトを用意しましたのでダウンロードしてご利用ください。「ダンプの取得開始レジストリ」を適用すると、ダンプが出力されるようになり、「ダンプの取得終了レジストリ」を適用するとダンプが出力されなくなります。(レジストリ操作を伴うので、使用は自己責任でお願いいたします。)

  1. ダンプの取得開始レジストリ(StartDump.reg)をダブルクリックする。
  2. ユーザーアカウント制御メッセージで[はい]を選択する。
  3. レジストリエディターメッセージで[はい]を選択する。
     レジストリエディターメッセージ
  4. ダンプ取得の準備完了です。
  5. 対象のアプリケーションを操作し、ダンプを取得します。
     アプリケーションがクラッシュする操作をすると、デスクトップに「Debug」フォルダが作成され、Debugフォルダの中にダンプファイルが作成されます。
  6. ダンプの取得終了レジストリ(EndDump.reg)をダブルクリックする。
  7. ユーザーアカウント制御メッセージで[はい]を選択する。
  8. レジストリエディターメッセージで[はい]を選択する。
  9. ダンプ取得の終了です。

ダンプ(.dmp)を使った調査方法

ダンプが取得できたら、アプリケーションのクラッシュ原因を調査していきます。調査で使用するツールは「WinDbg Preview」です。

以前は、「WinDbg」というツールが主流でしたがUIが独特で操作しづらいツールでしたが、WinDbg PreviewはモダンなUIでWinDbgより格段に操作しやすくなっています。またWinDbg PreviewにはTTD (Time Travel Debugging)という時間を巻き戻してデバッグできる機能が備わっています。記事の初めにも書きましたが、根本原因はクラッシュ時よりも前で起きていることが多くあります。TTDを使うと根本原因までさかのぼって調べることが可能になります。

TTDを使用するにはダンプ(.dmp)ではなく、トレース(.run)を取得する必要があります。トレースを使った調査方法は手順が異なるので後述します。

ここでは、ダンプ(.dmp)を使った調査方法を記載します。

基本的な調査手順

基本となる!analyze -vを使った調査手順を記載します。ダンプ(.dmp)が取得できたらまず実行すべき調査です。

  1. WinDbg Previewを起動します。
     WinDbg Previewを起動する
  2. [ファイル]-[Start debugging]-[Open dump file]をクリックします。
     ダンプファイルを開く
  3. ファイル選択ダイアログが開くのでダンプファイルを開きます。
     ダンプファイルを選択する
     ダンプファイルを開くと以下のような画面がでます。すでに「Access Violation」の文字が出ています。
     ダンプファイルを開いた状態
  4. [ファイル]-[Settings]を選択します。
     Settings
  5. [Browse…]ボタンを押し、[Source path]と[Symbol path]を選択します。
     [Source path]にはアプリケーションのソースコードのルートパスを選択します。[Symbol path]にはアプリケーションのシンボル(.pdb)のあるフォルダパスを選択します。
     ソースとシンボルのパスを選択する
     この時、「srv*」は残したままとしてください。「srv*」はMicrosoftのシンボルサーバーを指していて、これを指定しておくとインターネットからWindows OSのシンボル(.pdb)を取得してくれます。こうしておくことで、APIの名称やOS内の関数名がデバッグ結果に表示されるようになります。
  6. ソースコードとシンボルのパスが正しいか確認します。
     以下のように「OK」が出ていれば正しくパスが設定されています。
     ソースコードとシンボルのパスが正しいか確認する
  7. !analyze -vを実行してダンプの解析をします。
     ダンプの解析を行う
  8. !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
---------

  1. ExceptionCode
     「c0000005 (Access violation)」となっていることから、Access violationが起きてアプリケーションがクラッシュしていることが分かります。
  2. PROCESS_NAME
     「AccessViolation.exe」が原因のモジュールであることが分かります。
  3. STACK_TEXT
     STACK_TEXTはクラッシュしたときのスタックトレースです。「AccessViolation!main+0x37」となっていることから、AccessViolationというモジュールのmain()関数の中でAccessViolationが起きていることが分かります。
  4. FAULTING_SOURCE_FILE, FAULTING_SOURCE_LINE_NUMBER, FAULTING_SOURCE_CODE
     AccessViolation.cppの13行目のコードが問題のコードであることが分かります。**このサンプルのコードでは本来なら11行目が原因コードですが、最適化によりコードの行数がずれています。**このようにコード行はずれる場合があるので注意してください。

このように、ダンプ(.dmp)があれば問題のソースコードの行数まで知ることができます。もちろん、できない場合も多々あります。その場合は、さらに突っ込んだ調査をすることになりますが、それらについては随時追加していくことにします。

Appendix

よく使うWinDbgコマンド

追加予定。

よくある原因と対処方法

追加予定。

最後まで読んでいただきありがとうございます。
また読んでくださいませ。
そんじゃーね。

関連記事

SPONSORED LINK
SPONSORED LINK