はじめに
「デバッガ」とはデバッグを支援するソフトウェアであり、実行中のプログラムの変数の内容を参照したり、トレース実行(ソースコードと照らし合わせながら1ステップずつ実行するモードのこと)を行うためのアプリケーションです。
前回まではEXEファイルやネイティブコードの構造について迫りましたが、それらの動作の流れを把握する上で、デバッグ技術は重要になってきます。
なお、ここで対象としているデバッガとは、実行中プロセスの動きを把握するためのプロセスデバッガです。インタプリタに搭載されるデバッガとは異なり、ネイティブコードのみのEXE、DLLなどをデバッグすることができます。
使用するソフトウェア
今回は「Visual C++ 2005 Express Edition」(以下、VC++)を使用してプログラミングを行います。VC++は、マイクロソフトのサイトからダウンロードできます。
デバッガの処理の流れ
有名なプロセスデバッガには、VC++に搭載されるタイプのものや、WinDbgが存在します。両方ともMS製です。
おなじみ、VC++のデバッグ画面です。どんなEXE/DLLでもデバッグ可能です。強力なソースコード参照機能、ウォッチ機能が付いており、安定性が高い点も評価できます。デバッガはメモリ違反の位置を特定しなければならず、どうしてもOSの安定性が低下せざるを得ない状況におかれます。筆者の経験上、VS 2005に搭載されているプロセスデバッガは、品質的にかなり高い位置にあると言えると思います。
アプリ開発をされている技術者の方であれば、一度は目にしたことがあるかもしれません。このWinDbgはx64バージョンも正式に配布されているので、VS 2005がリリースされる前は筆者もx64用ソフトウェアのデバッグ時にかなりの頻度で活用していました。いまや、VC++に付属するデバッガの方が高機能ではありますが、アセンブラレベルでEXE/DLLをデバッグしたいときは、WinDbgは捨てがたい存在だと言えます。
今回、製作に踏み切るデバッガはコマンドプロンプト上で動作する簡単なものですが、処理の流れは上記の有名なデバッガと同等のことを行います。劣る点と言えば、ネイティブコードになる前のソースコードとの相互性(行番号)の管理、変数ウォッチなどの機能が搭載されないなどが挙げられます。
そもそも、トレース実行時のソースコードとの照らし合わせ、変数のウォッチングに関しては、対象プロセスのメモリを監視することで実現できます。今回は、それらの技術の基本となるプロセスデバッガのイベントループ部分を重点的に突き詰めていきます。
必要なデバッグ用API関数
CreateProcess関数
まずデバッガは、対象プロセスを実行するためにCreateProcess
を呼び出す必要があります。CreateProcess
はデバッガ専用の関数ではないので、重要な第6パラメータの説明のみに留めます。
MSDNヘルプによると、CreateProcess
の第6パラメータはdwCreationFlags
となっており、「プロセス作成に関する制御フラグと、優先順位クラスを指定します」とあります。このパラメータに、DEBUG_ONLY_THIS_PROCESS
フラグを含ませることにより、生成されたプロセスをデバッグすることが可能となります。
WaitForDebugEvent関数
デバッグ対象となるプロセスからデバッグイベントが送られてくるのを待機するための関数です。
BOOL WaitForDebugEvent( LPDEBUG_EVENT lpDebugEvent, DWORD dwMilliseconds );
lpDebugEvent
にはDEBUG_EVENT
構造体へのポインタを指定します。構造体のdwProcessId
メンバ、dwThreadId
メンバにはデバッグ対象となるプロセスID、スレッドIDを指定しておく必要があります。dwMilliseconds
にはタイムアウト時間をミリ秒単位で指定します。INFINITE
を指定しておくと無制限になります。- 関数が成功すると0以外の値が、失敗すると0が返ります。
ContinueDebugEvent関数
デバッグイベントをデバッガが処理し終えたときに、デバッギングを継続させるときに呼び出します。
BOOL ContinueDebugEvent( DWORD dwProcessId, DWORD dwThreadId, DWORD dwContinueStatus );
dwProcessId
、dwThreadId
にはそれぞれデバッグ対象のプロセスID、スレッドIDを指定します。dwContinueStatus
にDBG_CONTINUE
を指定するとWindows標準の例外処理が実行されず、スレッドが再開します。DBG_EXCEPTION_NOT_HANDLED
を指定するとWindows
標準の例外処理が実行されます。EXCEPTION_DEBUG_EVENT
デバッグイベント以外のイベント時にコンティニューするときは、このフラグに関係なくスレッドが再開します。
DEBUG_EVENT構造体
typedef struct _DEBUG_EVENT { // de DWORD dwDebugEventCode; DWORD dwProcessId; DWORD dwThreadId; union { EXCEPTION_DEBUG_INFO Exception; CREATE_THREAD_DEBUG_INFO CreateThread; CREATE_PROCESS_DEBUG_INFO CreateProcessInfo; EXIT_THREAD_DEBUG_INFO ExitThread; EXIT_PROCESS_DEBUG_INFO ExitProcess; LOAD_DLL_DEBUG_INFO LoadDll; UNLOAD_DLL_DEBUG_INFO UnloadDll; OUTPUT_DEBUG_STRING_INFO DebugString; RIP_INFO RipInfo; } u; } DEBUG_EVENT;
dwDebugEventCode
には、デバッグイベントの識別コードが下記の値で指定されます。
値 | 説明 |
EXCEPTION_DEBUG_EVENT | メモリの不正アクセス、0割り、ブレークポイントなどの例外処理時に発生します。u.Exception が有効になります。 |
CREATE_THREAD_DEBUG_EVENT | スレッドが生成されたときに発生します。u.CreateThread が有効になります。 |
CREATE_PROCESS_DEBUG_EVENT | プロセスが生成されたときに発生します。u.CreateProcessInfo が有効になります。 |
EXIT_THREAD_DEBUG_EVENT | スレッドが終了したときに発生します。u.ExitThread が有効になります。 |
EXIT_PROCESS_DEBUG_EVENT | プロセスが終了したときに発生します。u.ExitProcess が有効になります。 |
LOAD_DLL_DEBUG_EVENT | DLLがプロセス空間にロードされたときに発生します。u.LoadDll が有効になります。 |
UNLOAD_DLL_DEBUG_EVENT | DLLがプロセス空間からアンロードされたときに発生します。u.UnloadDll が有効になります。 |
OUTPUT_DEBUG_STRING_EVENT | OutputDebugString 関数により、デバッガに文字列が送信されたときに発生します。u.DebugString が有効になります。 |
RIP_EVENT | システムデバッグエラーが生じたときに発生します。u.RipInfo が有効になります。 |
ReadProcessMemory関数
Windowsではデバッグする側、される側の異なるプロセス空間同士では一般的なデータの読み書きを行うことができません。異なるプロセス内のデータを読み込むときは、この関数を利用する必要があります。
BOOL ReadProcessMemory( HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead );
hProcess
には、読み込みたいデータが存在するプロセスのハンドルを指定します。lpBaseAddress
には、データが存在する対象プロセス内のアドレスを指定します。lpBuffer
、nSize
には、読み込んだデータを保存するためのバッファと有効サイズを指定します。lpNumberOfBytesRead
には、DWORD
型データへのポインタを指定します。ここに実際に読み込まれたバイト数が格納されます。