スタックダンプの読み方

レジスタのEBPを基準にして読み解いていきます。EBPで示されるアドレスの手前4バイトが呼び出し元アドレス、EBP以降が引数です。EBPで示される場所に格納されている4バイトが関数呼び出し前のEBPアドレスです。
EBPが0x00C00018の場合を例としてスタックを読み解きます。

0x00C00010 : 00 00 00 04 // 引数、または、スタック変数
0x00C00014 : 00 00 00 03 // 引数、または、スタック変数
0x00C00018 : 00 C0 00 28 // 手前のEBPアドレス
0x00C0001C : 00 40 10 40 // 呼び出し元アドレス
0x00C00020 : 00 00 00 02 // 引数、または、スタック変数
0x00C00024 : 00 00 00 01 // 引数、または、スタック変数
0x00C00028 : 00 C0 00 40 // 手前のEBPアドレス
0x00C0002C : 00 40 10 80 // 呼び出し元アドレス

cdecl呼び出し規約や、stdcall呼び出し規約では、引数は右に書かれているものから順にスタックに格納されます。したがってアドレスの大きいものから順に、右から左の引数となります。Pascal呼び出し規約では逆に左から順に格納されます。
C++の場合this ポインタがスタックに追加されます。thisポインタはすべての引数をスタックに格納した後、最後にスタックに積まれます。Windowsでは関数が引数を伴わない場合には、thisポインタをスタックに積まずに、ECXレジスタに格納して関数を呼び出す場合があります。
スタック変数の積まれる順序は保障されません。最適化の結果、順不同にスタックに積まれます。

スタックの取得(ネィテブデバッガを作る2)

デバッグを行う上で実行位置に続いて重要なのがスタックの情報です。
別プロセスのスタック情報を取得するには、ReadProcessMemory APIを使用します。「ネィティブデバッガを作る」で取得したCONTEXT構造体のEspレジスタで示されるアドレス以降のデータを読み出します。

BYTE stackPointer[64];
DWORD readedSize(0);
HANDLE hProcess(0);
hProcess = ::OpenProcess(PROCESS_VM_READ, FALSE, debugEvent->dwProcessId);
if (NULL != hProcess)
{
// スタック領域からメモリを読み出す
if (::ReadProcessMemory(hProcess, (LPVOID )context.Esp, &readBuff, sizeof(readBuff), &readedSize))
{
// スタックダンプを表示する
}
}

ReadProcessMemory APIで確保されていないメモリ領域にアクセスすると、デバッグ対象となっているプロセスで不正なメモリアクセスがおこりDebugEventが発生します。ReadProcessMemoryで不正なメモリアクセスを起こし、DebugEventで再びReadProcessMemoryを呼び出し・・・という無限ループに陥らないように注意が必要です。
ReadProcessMemoryの第二パラメータや第三パラメータで割り当てられていないメモリを指定しても、ReadProcessMemoryはエラーを返さずに正常終了します。ReadProcessMemoryがエラーになるか否かで、正しくメモリを読み出せたか判断することはできません。

ネィティブデバッガを作る

WindowsOSやMozillaなどアプリケーションのデバッグ情報を送信する機能を備えたソフトウェアが増えています。Debugging Functions APIを使って実行中の不正なメモリアクセスなどを監視してデバッグ情報を取得する機能を確認してみましょう。
まずデバッグを行うにはCreateProcess APIを使ってデバッガからデバッグ対象のプロセスをDEBUG_PROCESSオプション付きで起動するか、あるいはデバッグ対象のプロセスからデバッガをDEBUG_ONLY_THIS_PROCESSオプション付きで起動する必要があります。
続いてWaitForDebugEvent APIを使用してデバッグ対象プロセス内でイベントが発生するのを待ちます。ここでは不正なメモリアクセスエラーだけを対象として考えます。dwDebugEventCodeがEXCEPTION_DEBUG_EVENT、ExceptionCodeがEXCEPTION_ACCESS_VIOLATIONの場合にエラーをトラップしていみます。これ以外にDLLのLoad/Unload、プロセスやスレッドの開始/停止、デバッグ標準出力へのメッセージや、プロセス内で発生した各種例外を取得することができます。
デバッグ対象プロセスの状態を取得するために、GetThreadContext APIを使用します。これによりデバッグ対象プロセス内で不正なメモリアクセスが発生した時のCPUレジスタの状態を取得できます。取得されるCONTEXT構造体の内容は使用しているCPUのアーキテクチャによって異なります。デバッグにこの情報を利用するためには、各種CPIアーキテクチャ毎にレジスタがどのように扱われるのか熟知している必要があります。
今回はx86 CPUアーキテクチャを対象としますので、CONTEXT構造体のeipレジスタに注目してください。eipレジスタの値が不正なメモリアクセスが発生した時に実行されていたアプリケーションコードの場所をしめします。あとはMAPファイルなどを確認して問題箇所を特定します
デバッグイベントに対する処理が終わったら、ContinueDebugEvent APIを呼び出します。このAPIの呼び出しによりデバッグイベントの発生により一時停止していしるデバッグ対象プロセスの処理を再開させます。

// デバッグ対象のプロセスを開始する
STARTUPINFO startupInfo;
PROCESS_INFORMATION processInfo;
::memset(&processInfo, 0x00, sizeof(processInfo));
::memset(&startupInfo, 0x00, sizeof(startupInfo));
startupInfo.cb		= sizeof(startupInfo);
if (CreateProcess(NULL, m_cmdLineInfo.m_runAppName.GetBuffer(), NULL, NULL, FALSE, DEBUG_PROCESS, NULL, NULL, &startupInfo, &processInfo))
{
// デバッグイベントを待つ
DEBUG_EVENT debugEvent;
while (WaitForDebugEvent(&debugEvent, INFINITE))
{
if (EXCEPTION_DEBUG_EVENT == debugEvent.dwDebugEventCode)
{
if (EXCEPTION_ACCESS_VIOLATION == debugEvent.u.Exception.ExceptionRecord.ExceptionCode)
{
// スレッドハンドルの取得
HANDLE hThread(0);
hThread = ::OpenThread(THREAD_GET_CONTEXT, FALSE, debugEvent->dwThreadId);
if (NULL != hThread)
{
// スレッドの状態を取得
CONTEXT		threadContext;
threadContext.ContextFlags	= CONTEXT_ALL;
if (GetThreadContext(hThread, &threadContext))
{
// ここでデバッグ処理をおこなう
}
}
}
}
ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
}
}

codファイルを使用したデバッグ

リリース後のアプリケーションで例外が発生した場合、codファイルを使用して例外の発生した行を特定することができます。
コンパイル時に生成された、アセンブリ言語のコードと、コンピュータ語のコードを保存することができます。プロジェクトのプロパティを開いて、C/C++の出力ファイルにある「アセンブリの出力」を変更して、「アセンブリコード、コンピュータ語コード、ソースコード」を選択します。これでビルド時に拡張子codのファイルが生成されます。
マップファイルを使用することで、例外オフセットから障害が発生したアドレスを調べられることは「マップファイルを使用したデバッグ」で解説しました。

Address Publics by Value Rva+Base Lib:Object
0000:00000001 ___safe_se_handler_count 00000001
0000:00000000 ___ImageBase 00400000
0001:00000000 _wmain 00401000 f crashTest.obj
0001:00000020 ?funcA@@YAHXZ 00401020 f crashTest.obj
0001:00000050 ?funcB@@YAHH@Z 00401050 f crashTest.obj
0001:00000090 ?funcC@@YAXXZ 00401090 f crashTest.obj

ここで例外の発生している関数funcBと例外オフセットアドレスの差を求めます。この場合、例外オフセットが0x1064、 関数の先頭アドレスが0x1050で、差分は0x14(20)バイトです。ここでcodファイルのfuncBの場所を参照します。

PUBLIC ?funcB@@YAHH@Z ; funcB
; Function compile flags: /Odtp
; COMDAT ?funcB@@YAHH@Z
_TEXT SEGMENT
_val$ = -4 ; size = 4
_arg1$ = 8 ; size = 4
?funcB@@YAHH@Z PROC ; funcB, COMDAT

; 29 : {

00000 55 push ebp
00001 8b ec mov ebp, esp
00003 51 push ecx

; 30 : char *val = NULL;

00004 c7 45 fc 00 00
00 00 mov DWORD PTR _val$[ebp], 0

; 31 : strcpy(val, “Hello World”); // 1.ソースコードの31行目で例外が発生している

0000b 8b 45 fc mov eax, DWORD PTR _val$[ebp]
0000e 8b 0d 00 00 00
00 mov ecx, DWORD PTR $SG-6
00014 89 08 mov DWORD PTR [eax], ecx // 2.ここで例外が発生している
00016 8b 15 04 00 00
00 mov edx, DWORD PTR $SG-6+4
0001c 89 50 04 mov DWORD PTR [eax+4], edx
0001f 8b 0d 08 00 00
00 mov ecx, DWORD PTR $SG-6+8
00025 89 48 08 mov DWORD PTR [eax+8], ecx

; 32 : printf(“call funcB”);

00028 68 00 00 00 00 push OFFSET $SG-7
0002d ff 15 00 00 00
00 call DWORD PTR __imp__printf
00033 83 c4 04 add esp, 4

; 33 : return 0;

00036 33 c0 xor eax, eax

; 34 : }

00038 8b e5 mov esp, ebp
0003a 5d pop ebp
0003b c3 ret 0
?funcB@@YAHH@Z ENDP ; funcB
_TEXT ENDS

0x0014バイトの個所を探すと、2.の個所で例外が発生していることがわかります。これは1.で示されるソースコード上の31行目にあたります。この作業でリリース後のアプリケーションでもソースコード状の何処で例外が発生しているのか特定することができます。

VisualStudio 2005 Remote Debugの設定

・OSの設定
アプリケーションを実行するコンピュータにユーザアカウントを追加します。開発環境を動作させているコンピュータにログインする時に使用している同じアカウント名とパスワードで管理者ユーザを作成します。
コントロールパネルの管理ツールにあるローカルセキュリティポリシーを起動して、ローカルポリシーのセキュリティオプションにある「ローカルアカウントの共有とセキュリティモデル」の設定を「クラシック」に変更します。
ファイアウォールが有効になっている場合にはリモートデバッグを実行できません。必ず無効にしてください。
・Remote Debuggerのインストール
アプリケーションを実行するコンピュータに対してVisual Studio 2005インストールCDのvsRemote Debugger\x86\rdbgsetup.exeを実行してリモートデバッガをインストールしてください。
・デバッグ用ランタイムDLLのインストール
VC++で作成したネィテブアプリケーションのデバッグにはランタイムDLLが必要です。ランタイムDLLの導入を行わないと、デバッグ版のアプリケーションを実行することができません。以下のフォルダにあるデバッグ用ランタイムDLLを、デバッグするアプリケーションの実行ファイルと同じ場所にコピーします。このときDLLファイルだけではなく、マニュフェストファイルも一緒にコピーしてください。
C:\Program Files\Microsoft Visual Studio 8\VC\redist\Debug_NonRedist\x86\Micorosft.VC80.DebugOpenMP
C:\Program Files\Microsoft Visual Studio 8\VC\redist\Debug_NonRedist\x86\Microsoft.VC80.DebugCRT
C:\Program Files\Microsoft Visual Studio 8\VC\redist\Debug_NonRedist\x86\Microsoft.VC80.DebugMFC
・デバッグの手順
アプリケーションを実行するコンピュータにて「VisualStudio 2005 リモートデバッガ」を起動します。
その後、VisualStudioの開発環境からデバッグのプロセスにアタッチを実行してください。修飾子にアプリケーションを実行するコンピュータのコンピュータ名を指定すると、実行されているプロセスの一覧が表示されるので、デバッグするプロセスを選択します。
サービスのデバッグを行いた場合には、「VisualStudio 2005 リモートデバッガ構成ウィザード」を起動してリモートデバッガがサービスとして動作するように設定します。
・デバッガをサービスとして動作させる
ログイン前のアプリケーションの動作や、サービスとして登録したアプリケーションをデバッグするには、デバッガをサービスとして動作させる必要があります。プログラムメニューから「Visual Studio 2005 リモートデバッガ構成ウィザード」を実行することでサービスとして登録できます。ですが、これだけではまだサービスとしてリモートデバッガを利用することはできません。
リモートデバッガをサービスとして動作させるには、コントロールパネルの管理ツールからサービスを起動してください。Visual Studio 2005 Remote Debuggerというサービスが登録されています。サービスのプロパティを開いて自動起動に変更してください。同時にサービスを実行するアカウントに管理者権限を持つユーザアカウントを指定します。このあと再起動すると、リモートデバッガはサービスとして起動します。

メモリリーク個所の特定

VisualC++に付属しているCランタイムライブラリにはヒープメモリのリークを検出する機能が備わっています。その機能をもちいて、メモリリーク個所を特定してみましょう。
メモリリークの検出
メモリリークを検出するには_CrtDumpMemoryLeaksを呼び出します。このとき未解放のメモリがあればデバッグコンソールにそのメモリアドレスと先頭16バイトのダンプが表示されます。

Dumping objects ->
{108} normal block at 0x00143FC0, 256 bytes long.
Data: 48 65 6C 6C 6F 20 57 6F 72 6C 64 00 CD CD CD CD
Object dump complete.

この中の{}で囲まれた数字は、起動後に何番目に確保さえたメモリかを示します。
メモリリーク個所の特定
メモリリーク箇所を特定するには_CrtSetBreakAllocを使用します。_CrtSetBreakAllocに先ほどの{}で囲まれた数字を指定します。これによりリークしたメモリを確保した個所でアプリケーションを中断することができます。
しかしこの方法ではアプリケーションの起動後、メモリを確保する順番が変動しないことが保障されていないと場所を特定できません。
そういった場合には_CrtMemCheckpointと_CrtMemDumpAllObjectsSinceを組み合わせて使用します。

int breakCnt;
_CrtMemState stateOld;
_CrtMemCheckpoint(&stateOld);
//ここで意図的にメモリを確保してリークさせる
_CrtMemDumpAllObjectsSince(&stateOld); // (1)
_CrtSetBreakAlloc(breakCnt);
//ここでデバッグ対象となる処理をおこなう。
_CrtMemDumpAllObjectsSince(&stateOld); // (2)

処理を行う前のメモリステータスをCrtMemCheckpoint関数で保存し、_CrtMemCheckpoint以降に確保したメモリのうち解放されていない分が_CrtDumpMemoryLeaksと同じように出力されます。
(1)で出力された{00}の番号と、(2)で出力された{00}の番号の差分を算出しておき、次回実行時に(1)の出力を確認したうえでデバッガから_CrtSetBreakAllocのパラメータを設定します。

アプリケーション側からデバッガを呼び出す。

プロセスを起動してからデバッガでプロセスにアタッチする方法では、プロセスの初期処理部分のデバッグを行えません。そのような場合にはDebugBreak APIを使用します。アプリケーション内部でDebugBreak APIを呼び出すと、DebugBreak APIを呼び出した個所でデバッガがアタッチするまで待たせることができます。
マネージドコードアプリケーションの場合には、DebugBreakの代わりにDebugger.Launch()を使用します。

マップファイルを使用したデバッグ

リリース後のアプリケーションにおいて不正なメモリアクセスなどのエラーが起こった場合には、マップファイルを用いてデバッグをおこないます。

マップファイルの作成

プロジェクトプロパティを開き、リンカのデバッグにあるマップファイルの生成を「はい」にしてください。これでビルド時にマップファイルが生成されます。

実行位置の特定

Windows Vistaでアプリケーション実行中に下のような画面が表示されます。詳細をクリックして詳しい情報を参照してください。
crash_vista.png
下記は実際に発生したエラーの詳細です。例外コードは例外の種類を表します。C00000005はWrite Protection Errorです。次の例外オフセットが例外が発生したコードの場所を示しています。マップファイルと照らし合わせて見てみます。

問題の署名:
問題イベント名: APPCRASH
アプリケーション名: crashTest.exe
アプリケーションのバージョン: 0.0.0.0
アプリケーションのタイムスタンプ: 498a7b3c
障害モジュールの名前: crashTest.exe
障害モジュールのバージョン: 0.0.0.0
障害モジュールのタイムスタンプ: 498a7b3c
例外コード: c0000005
例外オフセット: 00001064
OS バージョン: 6.0.6000.2.0.0.256.1
ロケール ID: 1041
追加情報 1: 1172
追加情報 2: 7912d3cce3b67ed49e553844317cb1a3
追加情報 3: c6b1
追加情報 4: 3dc30d5ca3a80f458b372b9da0535a3a

マップファイルには下記のような内容が含まれています。Publics by Valueが関数名。Rva+Baseがコードのアドレスです。ベースアドレス00400000+オフセットアドレス00001064=00401064が問題のおこったコードアドレスになります。

Address Publics by Value Rva+Base Lib:Object

0000:00000001 ___safe_se_handler_count 00000001
0000:00000000 ___ImageBase 00400000
0001:00000000 _wmain 00401000 f crashTest.obj
0001:00000020 ?funcA@@YAHXZ 00401020 f crashTest.obj
0001:00000050 ?funcB@@YAHH@Z 00401050 f crashTest.obj
0001:00000090 ?funcC@@YAXXZ 00401090 f crashTest.obj

Rva+Baseと照らし合わせると、00401064は「?funcB@@YAHH@Z」と「?funcC@@YAXXZ」の間にあることがわかります。Rva+Baseは関数の先頭を示すアドレスですから、00401064は「?funcB@@YAHH@Z」の内部だということがわかります。
ひとつだけ注意しなければならない点があります。コンパイラの最適化により関数がインラインに展開されたりした場合には、呼び出し元関数内でエラーが発生しているように表示されます。たとえば今回の場合「funcB」がインライン展開されていた場合、funcBの呼び出し元である「_tmain」がエラーの発生個所となります。

参考資料

KB196755 特定の場所、障害が発生コード内で表示されるエラー メッセージでアドレスを使用する方法

VisualStudio 2003 Remote Debugの設定

・OSの設定
アプリケーションを実行するコンピュータにユーザアカウントを追加します。開発環境を動作させているコンピュータにログインする時に使用している同じアカウント名とパスワードで管理者ユーザを作成します。
コントロールパネルの管理ツールにあるローカルセキュリティポリシーを起動して、ローカルポリシーのセキュリティオプションにある「ローカルアカウントの共有とセキュリティモデル」の設定を「クラシック」に変更します。
ファイアウォールが有効になっている場合にはリモートデバッグを実行できません。必ず無効にしてください。
・Remote Debuggerのインストール
Visual Studio 2003のインストーラを起動し、Visual Studio .Net Setupの画面から「リモートコンポーネントセットアップ」をクリックします。表示されるHTMLにリモートデバッグコンポーネントの導入手順が書かれているので、それに従ってインストールしてください。

vs2003_setup.png
vs2003_remote.png

・デバッグの手順
リモートデバッガはサービスとして起動しているので、別途起動する必要はありません。
VisualStudioの開発環境からデバッグのプロセスにアタッチを実行してください。修飾子にアプリケーションを実行するコンピュータのコンピュータ名を指定すると、実行されているプロセスの一覧が表示されるので、デバッグするプロセスを選択します。