デバッガを作ってみよう
プロジェクトの作成
VC++を立ち上げたら、メニューの[ファイル]-[新規作成]-[プロジェクト]をクリックします。[新しいプロジェクト]ダイアログボックスが表示されたら、[Win32コンソール アプリケーション]を選択し、プロジェクト名を「EasyDebugger」などとして[OK]ボタンをクリックします。続けてウィザードが表示されますが、[完了]ボタンをクリックしてください。
これで空のプロジェクトが作成されます。
次に、メニューの[プロジェクト]-[EasyDebuggerのプロパティ]をクリックします。構成を[すべての構成]にし、[構成プロパティ]の[全般]にある[文字セット]を[マルチバイト文字セットを利用する]にセットして[OK]ボタンをクリックします。
「stdafx.h」のコーディング
// TODO: プログラムに必要な追加ヘッダーをここで参照してください。 #include <windows.h> #pragma comment(lib, "user32.lib")
特段ポイントとなる部分はありませんが、Win32APIを利用するため、「windows.h」をインクルードすると共に、「user32.lib」の利用を宣言しておきます。
「EasyDebugger.cpp」のコーディング
CreateProcess関数によるプロセス生成、WaitForDebugEvent関数による待機処理、各種イベント処理、ContinueDebugEvent関数による継続処理を記述していきます。
// EasyDebugger.cpp : コンソール アプリケーションのエントリ ポイントを定義します。 // #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { //引数が1つ以下の場合は終了 if(argc<=1) return 0; char *lpszExeFilePath; lpszExeFilePath=argv[1]; /////////////////////// // プロセスを生成 /////////////////////// STARTUPINFO si; PROCESS_INFORMATION pi; memset(&si,0,sizeof(STARTUPINFO)); si.cb=sizeof(STARTUPINFO); CreateProcess(lpszExeFilePath,"",NULL,NULL,0, NORMAL_PRIORITY_CLASS|DEBUG_ONLY_THIS_PROCESS,NULL,NULL,&si,&pi); HANDLE hDebugProcess; hDebugProcess=pi.hProcess; DEBUG_EVENT de; de.dwProcessId=pi.dwProcessId; de.dwThreadId=pi.dwThreadId; //デバッグ用のイベントループ char temporary[1024]; DWORD dwAccessByte; BOOL bFirstBreak=1; while(WaitForDebugEvent(&de,INFINITE)){ switch(de.dwDebugEventCode){ case LOAD_DLL_DEBUG_EVENT: WCHAR wcBuf[MAX_PATH]; LONG_PTR lpData; ReadProcessMemory(hDebugProcess,de.u.LoadDll.lpImageName, &lpData,sizeof(LONG_PTR),&dwAccessByte); ReadProcessMemory(hDebugProcess,(void *)lpData,wcBuf, sizeof(WCHAR)*MAX_PATH,&dwAccessByte); if(de.dwThreadId,de.u.LoadDll.fUnicode) WideCharToMultiByte(CP_ACP,0,wcBuf,-1,temporary, 255,NULL,NULL); else lstrcpy(temporary,(char *)wcBuf); printf("\"%s\" をロードしました。\r\n",temporary); break; case CREATE_PROCESS_DEBUG_EVENT: //プロセス開始 break; case CREATE_THREAD_DEBUG_EVENT: //スレッド開始 break; case EXIT_PROCESS_DEBUG_EVENT: //プロセス終了 printf("スレッド(&H%X)はコード &H%X で終了しました。\r\n", de.dwThreadId,de.u.ExitProcess.dwExitCode); printf("プログラムはコード &H%X で終了しました。\r\n", de.u.ExitProcess.dwExitCode); break; case EXIT_THREAD_DEBUG_EVENT: //スレッド終了 printf("スレッド(&H%X)はコード &H%X で終了しました。\r\n", de.dwThreadId,de.u.ExitThread.dwExitCode); break; case OUTPUT_DEBUG_STRING_EVENT: //任意のデバッグ文字列 ReadProcessMemory(hDebugProcess, (void *)de.u.DebugString.lpDebugStringData, temporary,de.u.DebugString.nDebugStringLength, &dwAccessByte); printf(temporary); break; case EXCEPTION_DEBUG_EVENT: if(de.u.Exception.ExceptionRecord.ExceptionCode== EXCEPTION_ACCESS_VIOLATION){ printf("スレッド(&H%X)でアクセス違反がありました(EPI=&H%08X)。\r\n", de.dwThreadId, (LONG_PTR)de.u.Exception.ExceptionRecord.ExceptionAddress); goto ExitDebugProcess; } else if(de.u.Exception.ExceptionRecord.ExceptionCode ==EXCEPTION_BREAKPOINT){ if(bFirstBreak) bFirstBreak=0; else{ printf("スレッド(&H%X)のブレーク ポイント(EPI=&H%08X)。\r\n", de.dwThreadId, (LONG_PTR)de.u.Exception.ExceptionRecord.ExceptionAddress); if(MessageBox(0,"処理を継続しますか?","EasyDebugger",MB_YESNO)==IDNO){ goto ExitDebugProcess; } } } else if(de.u.Exception.ExceptionRecord.ExceptionCode== STATUS_INTEGER_DIVIDE_BY_ZERO){ printf("0による除算が行われました。"); printf("スレッド(&H%X) ブレーク ポイント(EPI=&H%08X)。\r\n", de.dwThreadId, (LONG_PTR)de.u.Exception.ExceptionRecord.ExceptionAddress); goto ExitDebugProcess; } else{ printf("例外処理\ncode:%X",de.u.Exception.ExceptionRecord.ExceptionCode); goto ExitDebugProcess; } break; default: break; } if(de.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT){ ExitDebugProcess: //プロセスが終了したとき break; } ContinueDebugEvent(de.dwProcessId,de.dwThreadId,DBG_CONTINUE); } CloseHandle(pi.hProcess); CloseHandle(pi.hThread); MessageBox(0,"デバッグ対象のプロセスが終了しました。","EasyDebugger",MB_OK); return 0; }
各種イベントでは、しばしばデバッグ対象のプロセス空間に存在するデータを読み込みたい場面に出くわします。そのようなときはReadProcessMemory関数を使ってデータを読み込んでいる点に注意しましょう。
デバッグ対象のプロセスの実行がスタートすると、まず初めに初回のEXCEPTION_DEBUG_EVENTイベントが発生します。これはプロセススタート時に必ず一回呼び出されるため、何もせずにコンティニューを行います。初回の例外に関してのみ、bFirstBreak変数を利用してスキップします。
デバッグしてみよう
簡易デバッガを使って、何かアプリケーションをデバッグ実行してみましょう。方法は簡単。コマンドプロンプト上で「EasyDebugger.exe」を立ち上げ、第1引数にデバッグしたいアプリケーションのパスを指定します。
何の変哲もない「notepad.exe」ですが、こうやってみると、さまざまなDLLファイルをロードしていることが分かります。デバッグ中のメモ帳を×ボタンで終了すると、アプリケーションからの戻り値が0となり、正常にスレッド・プロセスが終了したことが伺えます。
デバッギング技術に欠かせない関数
今回解説したデバッギング技術は複雑な内容を省いた、最小限の構成になっています。より多機能なデバッガを作成されたい方は、さらに下記のAPI関数を活用することをお勧めします。
| 関数名 | 説明 |
DebugActiveProcess | 既に実行中のプロセスをデバッグ用にアタッチします。 |
DebugBreak | デバッグされる側のプログラムで呼び出します。ブレークポイントの役割を果たします。 |
GetThreadContext | 実行中スレッドのコンテキストを取得するための関数です。コンテキストとは、CPUレジスタなどのことをいいます。 |
SetThreadContext | 実行中スレッドのコンテキストに任意のデータを設定します。 |
IsDebuggerPresent | 自身のプログラムがデバッガによりデバッギングされているかどうかを知ることができます。 |
OutputDebugString | デバッグされる側のプログラムがデバッガに文字列を送信するために呼び出します。デバッガ側ではOUTPUT_DEBUG_STRING_EVENTイベントが発生します。 |
WriteProcessMemory | ReadProcessMemoryの逆です。任意のプロセス空間にデータを書き込むことができます。 |
おわりに
筆者が開発するActiveBasicも当然のことながら、プロセスデバッガを搭載しています。今回紹介したデバッグプログラムに付け加える形で、ソースコード追尾機能や変数ウォッチ機能を搭載していますが、外部からの指令で任意のタイミングにブレークをかける技術や機械語単位でのトレース実行が実現できていないなど、まだまだ発展途上だと言えます。
デバッギングを行うことで得た情報を活用してリファクタリング(ソースコードの整備)に役立てたり、クロスプラットフォームを想定したりと課題は山積みです。
将来的には.NET技術などをさらに洗練し、例えば、「オブジェクト参照ミスによるちょいミスをコーディング中にバックグラウンドでデバッギングを行ってプログラマに教えてあげる機能」などが要求されてくるのだと思います。コンパイラ・デバッガ開発に興味がある方は、ここらへんの手法が現時点でどれほど見出されているのか、また、さらに良い方法がないかなどを検討してみるのも面白いのかもしれません。

