Tutorial 28: Win32 Debug API Part 1

今回のチュートリアルでは、Win32で提供されているデバッグに関する基礎知識を紹介する。この章を読み終われば、どうすればプログラムをデバッグできるかがわかるようになるだろう。
 メインソース   実行結果1   実行結果2   実行結果3   実行結果4 

Theory:

Win32にはデバッグするために必要なAPIがいくつか用意されており、それらは、Win32デバッグAPIと呼ばれている。これらのAPIを使用すれば、以下のようなことが可能だ。

つまり、これらのAPIを使用すれば簡単なデバッガを作成できるということである。
今回のテーマは非常に膨大な内容なので、3つのパートに分けている。このチュートリアルは1つ目で、基本的なコンセプトとWin32デバッグAPIの使用方法を説明する。

Win32デバッグAPIの使用方法は以下のような手順となっている。

  1. プロセスを起動し、起動中のプログラムへアタッチする

    これが1番目のステップである。今回のプログラムはデバッガとして実行されるので、デバッグ対象のプログラムが必要だ。そのようなプログラムのことをデバッグ対象プログラムと呼ぶ。デバッグ対象プログラムを取得する方法は以下の2通りだ。

    • CreateProcess関数を使用してデバッグ対象プログラムを起動する。その際、デバッグする証としてDEBUG_PROCESSフラグを指定しなければならない。これによりWindowsにデバッグすることを宣言でき、Windowsはこのプログラムに発生したデバッグに関するイベントを通知してくれるようになる。
      デバッグ対象プロセスはデバッグプログラムの準備が完了するまで停止されたままとなる。もしデバッグ対象プロセスが子プロセスを起動する場合は、それらの子プロセスに対して発生したデバッグイベントも全て送信してくれる。このような動作が望ましいものでなければ、DEBUG_ONLY_THIS_PROCESSフラグも指定すればよい。

    • DebugActiveProcessで起動中のプログラムにアタッチできる。
  2. デバッグイベントの監視

    デバッグ対象プログラムが決まった後、そのデバッグ対象プログラムのプライマリスレッドは停止した状態となっており、WaitForDebugEvent関数をCALLするまでずっと停止している。この関数は他のWaitForXXX関数と同じように動作し、何かイベントが発生するまでブロックしているのだが、この場合はWindowsからデバッグイベントが送信されるのを待っている。
    では定義を見ていこう。

    WaitForDebugEvent proto lpDebugEvent:DWORD, dwMilliseconds:DWORD

    • lpDebugEventDEBUG_EVENT構造体へのポインタで、デバッグ対象プログラムで発生したイベントに関する情報が格納されている。
    • dwMillisecondsに指定されているミリ秒だけデバッグイベントが発生するまで待つことになっている。もしこの期間中にデバッグイベントが発生しなければ、WaitForDebugEventが呼び出し側に返ることになっている。一方、INFINITEを指定すれば、デバッグイベントが発生するまで制御は返ってこない。

    ではDEBUG_EVENT構造体についてさらに詳しく見てみよう

    DEBUG_EVENT STRUCT 
       dwDebugEventCode dd ?
       dwProcessId dd ?
       dwThreadId dd ?
       u DEBUGSTRUCT <>
    DEBUG_EVENT ENDS

    • dwDebugEventCodeには発生したデバッグイベントのタイプが格納されている。つまり、イベントタイプは非常に多くのタイプがあるので、このフィールドをチェックすることによりどのイベントが発生したのかを知ることができる。
      取り得る値は以下のようになっている。

      意味
      CREATE_PROCESS_DEBUG_EVENT プロセスが作成されたことを表す。このイベントは、デバッグ対象プロセスが作成された瞬間(まだ実行してはいない)、もしくはDebugActiveProcess関数で起動中のプログラムにアタッチした瞬間に発生する。このイベントは一番最初に発生するイベントである。
      EXIT_PROCESS_DEBUG_EVENT プロセスが終了したことを表す。
      CREATE_THREAD_DEBUG_EVENT デバッグ対象プロセスで新しいスレッドが作成されたこと、もしくは起動中のプロセスに一番初めにアタッチしたこと表す。ただし、デバッグ対象プログラムのメインスレッドが作成されたときにこのイベントをおそらく受け取らないことに注意しなければならない。
      EXIT_THREAD_DEBUG_EVENT デバッグ対象プログラムのスレッドが終了したことを表す。ただし、このイベントはメインスレッドが終了したときに受け取ることはないだろう。つまり、メインスレッドはデバッグ対象プロセス自身と同等の扱いになるので、EXIT_PROCESS_DEBUG_EVENTが発生したと言うことは、メインスレッドにとってのEXIT_THREAD_DEBUG_EVENTが発生したということである。
      LOAD_DLL_DEBUG_EVENT デバッグ対象プログラムがDLLをロードしたことを表す。このイベントを受け取るのは、PEローダがDLLへのリンクを解決したとき(デバッグ対象プログラムがCreateProcess関数をCALLしたとき)と、デバッグ対象プログラムがLoadLibrary関数をCALLしたときである。
      UNLOAD_DLL_DEBUG_EVENT デバッグ対象プログラムからDLLがアンロードされたことを表す。
      EXCEPTION_DEBUG_EVENT デバッグ対象プログラムで例外が発生したことを表す。
      ※重要 このイベントはデバッグ対象プログラムが一番最初の命令を実行する時に一度だけ発生する。その例外はデバッグブレーク(int 3h)である。デバッグ対象プログラムの実行を続ける場合、DBG_CONTINUEを指定して、ContinueDebugEvent関数をCALLすればよい。決してDBG_EXCEPTION_NOT_HANDLEDフラグを指定してはならず、もし指定すると、WindowsNTではデバッグ対象プログラムが動かなくなってしまう(Win98ではOK)。
      OUTPUT_DEBUG_STRING_EVENT DebugOutputString関数をCALLしてメッセージを送信することを表す。
      RIP_EVENT システムデバッグエラーが発生したことを表す。

    • dwProcessIddwThreadIdはそれぞれ、デバッグイベントが発生したプロセスIDとスレッドIDである。このIDを用いてデバッグ対象のプログラムを指定する。CreateProcess関数を使用してデバッグ対象プログラムを起動した場合、PROCESS_INFO構造体からデバッグ対象プログラムのプロセスIDとスレッドIDを取得できることを覚えておこう。
      これらのIDはデバッグ対象プログラムと、そのプログラムの子プロセスとを判別するために(DEBUG_ONLY_THIS_PROCESSフラグを指定しなかった場合)使用できる。
    • uにはデバッグイベントについてより詳しい情報が格納されている。上記のdwDebugEventCodeがどういう値をとるかによって、以下の構造体のどれかになる。

      dwDebugEventCodeの値uの解釈
      CREATE_PROCESS_DEBUG_EVENT CREATE_PROCESS_DEBUG_INFO構造体
      EXIT_PROCESS_DEBUG_EVENT EXIT_PROCESS_DEBUG_INFO構造体
      CREATE_THREAD_DEBUG_EVENT CREATE_THREAD_DEBUG_INFO構造体
      EXIT_THREAD_DEBUG_EVENT EXIT_THREAD_DEBUG_EVENT構造体
      LOAD_DLL_DEBUG_EVENT LOAD_DLL_DEBUG_INFO構造体
      UNLOAD_DLL_DEBUG_EVENT UNLOAD_DLL_DEBUG_INFO構造体
      EXCEPTION_DEBUG_EVENT EXCEPTION_DEBUG_INFO構造体
      OUTPUT_DEBUG_STRING_EVENT OUTPUT_DEBUG_STRING_INFO構造体
      RIP_EVENT RIP_INFO構造体

    このチュートリアルでは、CREATE_PROCESS_DEBUG_INFO構造体以外については詳しい解説は行わないことにする。
    このプログラムではWaitForDebugEvent関数をCALLし、その後制御が返ってきたと仮定しよう。まず初めに、dwDebugEventCodeの値により、どういうデバッグイベントが発生したかを判別しよう。例えば、CREATE_PROCESS_DEBUG_EVENTだったらメンバ変数uCreateProcessInfo構造体として扱うことができる。

  3. 発生したデバッグイベントに対して何か処理を行うWaitForDebugEventから制御が帰れば、デバッグ対象プログラムでデバッグイベントが発生したか、もしくはタイムアウトしたということである。その後、dwDedbugEventCodeの値を調べ、どう対応するかをプログラムしていく。
    この処理は、Windowsメッセージと同じように、自分の処理したいイベントだけを対象とすればよい。
  4. デバッグ対象プログラムの実行を再開させる。デバッグイベントが発生すれば、Windowsはデバッグ対象プログラムを停止させる。デバッグイベントに対する処理が終われば、再開させる必要があるので、ContinueDebugEvent関数をCALLしなければならない。

    ContinueDebugEvent proto dwProcessId:DWORD, dwThreadId:DWORD, dwContinueStatus:DWORD

    この関数は、デバッグイベントが発生したことにより停止していたプログラムを再開させることができる。dwProcessIddwThreadIdは再開させたいプログラムのプロセスハンドルとスレッドハンドルとなる。これら2つの値はDEBUG_EVENT構造体から取得することになる。

    dwContinueStatusの値により、どのようにスレッドを実行させるかを指定する。DBG_CONTINUEDBG_EXCEPTION_NOT_HANDLEDという2つの値をとることができる。デバッグイベント全般に言えることだが、これら2つの値はいずれもスレッドを再開する。

    例外はEXCEPTION_DEBUG_EVENTだ。スレッドで例外デバッグイベントが発生すれば、デバッグ対象スレッドで例外が発生したということである。DBG_CONTINUEを指定すれば、スレッド自身は例外処理を無視し、処理を続けることになる。
    この場合、スレッドを再開する前にデバッグプログラム中で例外に対する処理を行わなければならない。でないと、その例外が延々発生し続けることになる。
    DBG_EXCEPTION_NOT_HANDLEDを指定すれば、例外を処理しないことになり、その場合はWindowsがデフォルトの例外処理を適用することになる。

    よって、デバッグイベントがデバッグ対象プロセス中の例外を参照するような場合は、例外の発生を抑えられない場合は、DBG_CONTINUEを引数にしてContinueDebugEvent関数をCALLしなければならない。
    もしくは、DBG_EXCEPTION_NOT_HANDLEDフラグを指定して、ContinueDebugEvent関数をCALLしなければならない。ただし、ある1つのケースである、ExceptionCodeメンバの値がEXCEPTION_BREAKPOINEXCEPTION_DEBUG_EVENTが初めて発生した場合においては常にDBG_CONTINUEフラグを指定しなければならない。

    というのは、デバッグ対象プログラムが一番最初の命令を実行しようとする際、例外デバッグイベント(デバッグブレーク:int 3h)が発生するのだが、DBG_EXCEPTION_NOT_HANDLEDフラグを指定してContinueDebugEvent関数をCALLすると、Windowsはデバッグ対象プログラムの実行を続けられなくなってしまう(例外イベントを処理するプログラムがないため)。

    なので、Windowsがスレッドの実行を続けられるよう、DBG_CONTINUEフラグを指定しなければならない。

  5. デバッグ対象プログラムが終了するまでこの繰り返し作業を行い続ける。必然的に、メッセージループに良く似た、以下のような無限ループが必要になる。

    .while TRUE
       invoke WaitForDebugEvent, addr DebugEvent, INFINITE
       .break .if DebugEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
       <Handle the debug events>
       invoke ContinueDebugEvent, DebugEvent.dwProcessId, DebugEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
    .endw

    問題は、いったんデバッグプログラムが起動されれば、デバッグ対象プログラムが終了するまでデバッグ対象プログラムから切り離せないことである。

おさらいしておこう

  1. プロセスを作成するか起動中のプログラムにアタッチする
  2. デバッグイベントの発生を待つ
  3. 待ち構えていたデバッグイベントが発生したら何か処理をする
  4. デバッグ対象プログラムの実行を継続させる
  5. デバッグ対象プロセスが終了するまで2から4を繰り返す

Example:

このサンプルはwin32プログラムをデバッグし、プロセスハンドルやプロセスID、イメージベースといった重要な情報を表示する

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
.data
AppName db "Win32 Debug Example no.1",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
             db "All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0
NewThread db "A new thread is created",0
EndThread db "A thread is destroyed",0
ProcessInfo db "File Handle: %lx ",0dh,0Ah
            db "Process Handle: %lx",0Dh,0Ah
            db "Thread Handle: %lx",0Dh,0Ah
            db "Image Base: %lx",0Dh,0Ah
            db "Start Address: %lx",0
.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
.code
start:
mov ofn.lStructSize,sizeof ofn
mov ofn.lpstrFilter, offset FilterString
mov ofn.lpstrFile, offset buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE,
   DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi\
.while TRUE
   invoke WaitForDebugEvent, addr DBEvent, INFINITE
   .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
      invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
      .break
  .elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
      invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile,\
          DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread,\
          DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress 
      invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION    
  .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
      .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT 
          invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
         .continue
      .endif
  .elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
      invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
  .elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
      invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
  .endif
  invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
.endif
invoke ExitProcess, 0
end start

Analysis:

上記プログラムでは、OPENFILENAME構造体に値をセットし、ユーザにデバッグするプログラムを選択させるためにGetOpenFileName関数をCALLしている。

invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE,\
   DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi

ユーザがプログラムを選択したら、CreateProcess関数をCALLしてプログラムをロードする。CALLする際に引数として、STARTUPINFO構造体が必要になるので、事前にGetStartupInfo関数で初期化しておく。

.while TRUE
   invoke WaitForDebugEvent, addr DBEvent, INFINITE

デバッグ対象プログラムがロードされれば、WaitForDebugEvent関数をCALLし無限デバッグループに入る。WaitForDebugEvent関数の第2引数にINFINITEを指定しているので、デバッグイベントが発生するまで返ってこない。
デバッグイベントが発生したらWaitForDebugEvent関数は制御を返し、DBEvent変数にデバッグイベントに関するデータが入っていることになる。

.if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT 
   invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
   .break

まずはdwDebugEventCodeの値をチェックする。もしEXIT_PROCESS_DEBUG_EVENTなら"The debuggee exits"というメッセージボックスを出し、デバッグループを抜け出す。

.elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT 
   invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile,\
      DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread,\
      DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress 
   invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION

dwDebugEventCodeの値がCREATE_PROCESS_DEBUG_EVENTならデバッグ対象プログラムに関する情報をメッセージボックスで表示する。 それらの情報はu.CreateProcessInfoの値を参照する。CreateProcessInfoはCREATE_PROCESS_DEBUG_INFO構造体の変数で、Win32APIリファレンスにこの構造体に関する詳しい説明がのってある。

.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT 
   .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
      invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
      .continue
   .endif

dwDebugEventCodeEXCEPTION_DEBUG_EVENTだったら、より詳しく例外の型をチェックしなければならない。 このコードを見てわかるように、構造体の構造体の構造体の・・・というように複雑なっているが、最終的にはExceptionCode変数から例外の種類が取得できる。 もしExceptionCodeEXCEPTION_BREAKPOINTで、かつ、それが初めて(無いとは思うがint3h命令がデバッグ対象プログラムに無いことを仮定している)だった場合は、 デバッグ対象プログラムが本当に最初の命令を実行しようとしている時に発生した例外だと仮定してよい。
この例外に対して何か処理を行う場合、デバッグ対象プログラムの処理を継続させるために、DBG_CONTINUEフラグを指定して、ContinueDebugEvent関数をCALLしなければならない。 その後、次のデバッグイベントの発生まで待ち続けることになる。

.elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT 
   invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
   invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
.endif

dwDebugEventCodeCREATE_THREAD_DEBUG_EVENTEXIT_THREAD_DEBUG_EVENTの場合は、その通りのメッセージを出力する。

   invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw

上記のEXCEPTION_DEBUG_EVENTの場合を除いて、デバッグ対象プログラムの処理を継続させるために、DBG_EXCEPTION_NOT_HANDLEDを引数に指定して、 ContinueDebugEvent関数をCALLしている。

invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread

デバッグ対象プログラムが終了すれば、デバッグループを抜け出し、その後デバッグ対象プログラムのプロセスハンドルとスレッドハンドル、両方とも閉じなければならない。 ハンドルを閉じるというのはプロセスやスレッドをkillすることではなく、単にこのプログラムでそれらのハンドルの値を使用しないということを表しているだけである。



[戻る]