Tutorial 30: Win32 Debug API part 3

win32デバッグAPIの続きだ。このチュートリアルではデバッグ対象プログラムのトレース方法に焦点を当てる。
 メインソース   実行結果 

Theory:

以前デバッガを使ったことがあれば、トレースについて多少なりとも知識があるだろう。プログラムをトレースすると、命令コード毎にプログラムは停止し、レジスタ、メモリの値を変更できるようになる。ちなみにトレースのことを正式には「シングルステップ」と呼んでいる。

シングルステップの特徴は、CPU自身がサポートしている機能であるということだ。フラグレジスタの8番目をトラップフラグと呼び、そのフラグ(正式にはbitだが)がセットされれば、CPUはシングルステップモードに突入する。CPUは各命令毎にデバッグ例外を発生させ、トラップフラグを自動的にクリアする。

デバッグ対象プログラムをシングルステップで動作させ、win32デバッグAPIを使用する方法は、以下のような手順を踏む。

  1. ContextFlagsCONTEXT_CONTROLを指定して、GetThreadContext関数をCALLし、フラグレジスタを取得する。
  2. CONTEXT構造体のregFlagにトラップビットをセットする。
  3. SetThreadContext関数をCALLする。
  4. いつものようにデバッグイベントを待つ。デバッグ対象プログラムはシングルステップモードで動作し、各命令の実行毎にEXCEPTION_DEBUG_EVENTを受け取り、同時にu.Exception.pExceptionRecord.ExceptionCodeの値がEXCEPTION_SINGLE_STEPになっている。
  5. 次の命令コードをトレースしたければ、トラップビットを再度セットする

Example:

.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.4",0 ofn OPENFILENAME <> FilterString db "Executable Files",0,"*.exe",0 db "All Files",0,"*.*",0,0 ExitProc db "The debuggee exits",0Dh,0Ah db "Total Instructions executed : %lu",0 TotalInstruction dd 0 .data? buffer db 512 dup(?) startinfo STARTUPINFO <> pi PROCESS_INFORMATION <> DBEvent DEBUG_EVENT <> context CONTEXT <> .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 wsprintf, addr buffer, addr ExitProc, TotalInstruction invoke MessageBox, 0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION .break .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT mov context.ContextFlags, CONTEXT_CONTROL invoke GetThreadContext, pi.hThread, addr context or context.regFlag,100h invoke SetThreadContext,pi.hThread, addr context invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE .continue .elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP inc TotalInstruction invoke GetThreadContext,pi.hThread,addr context or context.regFlag,100h invoke SetThreadContext,pi.hThread, addr context invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE .continue .endif .endif invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED .endw .endif invoke CloseHandle,pi.hProcess invoke CloseHandle,pi.hThread invoke ExitProcess, 0 end start

Analysis:

このプログラムを実行すると、ファイルをオープンするダイアログボックスが出現する。ユーザが実行ファイルを選択すると、そのプログラムがシングルステップモードで動作し、デバッグ対象プログラムが終了するまで命令数をカウントする。

.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT

ここで、デバッグ対象プログラムをシングルステップモードで動作させるように設定する。Windowsはプログラムを起動する直前に、EXCEPTION_BREAKPOINTを送信することを思い出そう。

mov context.ContextFlags, CONTEXT_CONTROL invoke GetThreadContext, pi.hThread, addr context

GetThreadContext関数をCALLすることにより、現在のデバッグ対象プログラムのレジスタ値をCONTEXT構造体にセットできる。具体的には現在のフラグレジスタの値が必要なのである。

or context.regFlag,100h

フラグレジスタイメージのトラップビット(8番目のビット)をセットする。

invoke SetThreadContext,pi.hThread, addr context invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE .continue

そして、CONTEXT構造体の値を反映させるため、SetThreadContext関数をCALLし、デバッグ対象プログラムの処理を再開させるために、DBG_CONTINUEフラグをセットして、ContinueDebugEvent関数をCALLする。

.elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP inc TotalInstruction

デバッグ対象プログラムの命令コードが実行されるとき、EXCEPTION_DEBUG_EVENTを受け取る。その際、u.Exception.pExceptionRecord.ExceptionCodeの値を調べなければならない。
その値がEXCEPTION_SINGLE_STEPだったら、このデバッグイベントが発生した原因はシングルステップモードで起動されたからということになる。この時、デバッグ対象プログラムは一つの命令コードを実行したので、TotalInstructionをインクリメントしておく。

invoke GetThreadContext,pi.hThread,addr context or context.regFlag,100h invoke SetThreadContext,pi.hThread, addr context invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE .continue

デバッグ例外が起こった後はトラップフラグがクリアされるので、引き続きシングルステップモードで実行したければ再度トラップフラグをセットする必要がある。

※ このサンプルを大きなプログラムに対して使用してはならない。トレースは非常に遅い処理なので、下手するとそのプログラムが終了するまで10分近く待たなければならないかもしれない。


[戻る]