VC++ シンボルが存在しない場合のダンプファイル解析方法

vc-windbg-non-symbolのアイキャッチ画像 Windows

以前の記事でダンプファイル(*.dmp)の解析方法を解説しました。
しかし、セキュリティ上の理由などでシンボルファイル(*.pdb)ファイルが存在しなかったり、アプリケーションのクラッシュの仕方によってソースコードの原因箇所をWinDbgで表示できない場合があります。

ここでは、上記のような場合に「mapファイル(アドレス情報が記載されているファイル)」と「codファイル(アセンブラが記載されているファイル)」を使って、ダンプファイルからソースコードの原因箇所を特定する方法を解説します。

※WindowsやLinuxなどの処理系に関わらず、コンパイル言語であれば、mapファイルとアセンブラコードは必ず出力することができます。ここではWindowsについて解説するため、「map」と「cod」というファイルとしています。

前提知識

mapファイルとは

モジュール(dllやexe)1つに対して1つ存在するファイルです。
ダンプを解析するにあたり、見なければいけないのは「実行プログラムのベースアドレス」と「関数・データなどの情報が登録されているアドレス」です。

実行プログラムのベースアドレス

mapファイルの先頭に記されています。mapファイルに記述されている値はあくまで参考値のため、実行時のものとは必ずしも一致しませんが、オフセット換算で追うことができます。

*.map

mapファイル①

関数/データなどの情報が登録されているアドレス

ベースアドレスを起点に昇順に記されています。
関数などのロジックに係る情報はtext(code)セクションに、データはdataセクションに記されています。

*.map

mapファイル②

codファイルとは

ソースファイル(cpp 等)1つに対して1つ存在するファイルです。
ソースコードとそのアセンブラコード(関数内の実行オフセット、バイナリコードも含む)が対で記されています。

*.cod

codファイル①

解析方法

アプリケーションの用意

こちらに従ってプロジェクトファイルを作成し、アプリケーション(exe)を作成します。main.cppは以下のように書き換え、Release|x64でビルドして下さい。

src/components/component*/main.cpp

#include <stdio.h>

void func3()
{
	::printf("5 \n");
	::printf("4 \n");
	::printf("3 \n");
	::printf("2 \n");
	::printf("1 \n");
	char* p = NULL;
	*p = 'a';
}

void func2()
{
	func3();
}

void func1()
{
	func2();
}

int main(int argc, const char** argv)
{
	::printf("start\n");
	func1();
	::printf("end\n");

	return 0;
}

ビルドすると、mapファイルとcodファイルは以下に出力されます。

  • mapファイル … C:\src\build2019\components\bin\x64\Release\component*.map
  • codファイル … C:\src\components\component*\2019\x64\Release\main.cod

ダンプファイルの取得

作成したexeファイルのみを、ビルドした環境とは異なる環境にコピペします。
exeを配置した環境で、以前の記事の通り設定し、ダンプファイルが出力されるようにします。
exeをダブルクリック等で実行します。

ランタイムエラー

このようなランタイムエラーや「VCRUNTIME140.dll が見つからない…」のようなエラーが出る場合、当該環境にVC2019のランタイムがインストールされていません。
Microsoftの公式ページからランタイムのインストーラー「vc_redist.x64.exe」をダウンロードして、インストールして下さい。

「%LOCALAPPDATA%\CrashDumps」以下に、ダンプファイル(*.dmp)が出力されることを確認します。

解析方法 ステップ1

ダンプファイルをWinDbgで開きます。
Command表示領域の下部の「0:000>」で「!analyze -v」と入力してEnterを押します。
出力される「STACK_TEXT」に着目します。

WinDbg

「STACK_TEXT」の最上位にある「component1+0x1065」が今回のクラッシュ箇所になります。component1.exeモジュールのベースアドレスから0x1065進んだ箇所でクラッシュしているという意味です。しかしアドレス表記のままでは、ソースコード上のどこでクラッシュしているか分かりません。これをmapファイルとcodファイルを利用して特定していきます。

解析方法 ステップ2

mapファイルからベースアドレスを把握します。

mapファイル③

ベースアドレスは「0x0000000140000000」です。
これより、クラッシュ箇所である「component1+0x1065」は「0x0000000140000000+0x1065」、すなわち「0x0000000140001065」というアドレスになります。

解析方法 ステップ3

mapファイルのtext(code)セクション内から、ステップ2で得たアドレスが存在する範囲を見つけます。

mapファイル④

上記より、「0x0000000140001065」は「0x0000000140001010(main関数の起点アドレス)」~「0x0000000140001080(printf関数の起点アドレス)」の範囲に存在していることがわかります。

「0x0000000140001065」から「0x0000000140001010(main関数のベースアドレス)」を引くと、「0x55」になります。
すなわち、クラッシュ箇所である「0x0000000140001065」は、main関数の起点から「0x55」進んだ箇所ということになります。

解析方法 ステップ4

main関数の起点から「0x55」進んだ箇所をcodファイルから探します。

「main」で検索し、直後にある波括弧({)からmain関数が始まります。
そこを起点に、オフセットが00000 ⇒ 00004 ⇒ 0000b ⇒ … と同関数が進んでいきます。
そしてオフセット「0x55(00055)」が該当する箇所になります。同箇所に記載されているソースコード「*p = ‘a’;」こそが、クラッシュした箇所ということになります。

codファイル②

以上で、シンボルが存在しない場合でもダンプファイルを解析し、ソースコード上のクラッシュ箇所を特定することができました。

タイトルとURLをコピーしました