以前の記事でダンプファイル(*.dmp)の解析方法を解説しました。
しかし、セキュリティ上の理由などでシンボルファイル(*.pdb)ファイルが存在しなかったり、アプリケーションのクラッシュの仕方によってソースコードの原因箇所をWinDbgで表示できない場合があります。
ここでは、上記のような場合に「mapファイル(アドレス情報が記載されているファイル)」と「codファイル(アセンブラが記載されているファイル)」を使って、ダンプファイルからソースコードの原因箇所を特定する方法を解説します。
※WindowsやLinuxなどの処理系に関わらず、コンパイル言語であれば、mapファイルとアセンブラコードは必ず出力することができます。ここではWindowsについて解説するため、「map」と「cod」というファイルとしています。
前提知識
mapファイルとは
モジュール(dllやexe)1つに対して1つ存在するファイルです。
ダンプを解析するにあたり、見なければいけないのは「実行プログラムのベースアドレス」と「関数・データなどの情報が登録されているアドレス」です。
実行プログラムのベースアドレス
mapファイルの先頭に記されています。mapファイルに記述されている値はあくまで参考値のため、実行時のものとは必ずしも一致しませんが、オフセット換算で追うことができます。
*.map
関数/データなどの情報が登録されているアドレス
ベースアドレスを起点に昇順に記されています。
関数などのロジックに係る情報はtext(code)セクションに、データはdataセクションに記されています。
*.map
codファイルとは
ソースファイル(cpp 等)1つに対して1つ存在するファイルです。
ソースコードとそのアセンブラコード(関数内の実行オフセット、バイナリコードも含む)が対で記されています。
*.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をダブルクリック等で実行します。
「%LOCALAPPDATA%\CrashDumps」以下に、ダンプファイル(*.dmp)が出力されることを確認します。
解析方法 ステップ1
ダンプファイルをWinDbgで開きます。
Command表示領域の下部の「0:000>」で「!analyze -v」と入力してEnterを押します。
出力される「STACK_TEXT」に着目します。
「STACK_TEXT」の最上位にある「component1+0x1065」が今回のクラッシュ箇所になります。component1.exeモジュールのベースアドレスから0x1065進んだ箇所でクラッシュしているという意味です。しかしアドレス表記のままでは、ソースコード上のどこでクラッシュしているか分かりません。これをmapファイルとcodファイルを利用して特定していきます。
解析方法 ステップ2
mapファイルからベースアドレスを把握します。
ベースアドレスは「0x0000000140000000」です。
これより、クラッシュ箇所である「component1+0x1065」は「0x0000000140000000+0x1065」、すなわち「0x0000000140001065」というアドレスになります。
解析方法 ステップ3
mapファイルのtext(code)セクション内から、ステップ2で得たアドレスが存在する範囲を見つけます。
上記より、「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’;」こそが、クラッシュした箇所ということになります。
以上で、シンボルが存在しない場合でもダンプファイルを解析し、ソースコード上のクラッシュ箇所を特定することができました。