Windowsでデバッガーを作成する[パート1]

エントリー





私たち全員が、デバッガを使用してプログラムをデバッグすることがあります。 デバッガーは、C ++、C#、Java、および他の数百の言語で使用できます。 外部(WinDbg)または開発環境(Visual Studio)に埋め込まれます。 しかし、デバッガがどのように機能するか疑問に思ったことはありますか?

そして、あなたは幸運です。 この一連の記事では、内部からデバッグがどのように機能するかを理解します。 この記事では、Windows用のデバッガーの作成についてのみ説明します。 コンパイラ、リンカー、その他の複雑なシステムなし。 したがって、外部デバッガーを作成するため、実行可能ファイルのみをデバッグできます。 この記事を読むには、マルチスレッドの基本を理解する必要があります。



プログラムをデバッグする方法:

  1. フラグDEBUG_ONLY_THIS_PROCESSまたはDEBUG_PROCESSを使用してプロセスを開始します。
  2. メッセージとイベントをキャッチするデバッグループを開始します。


始める前に、覚えておいてください:



デバッグフラグを使用してプロセスを開始する


CreateProcess関数を使用してプロセスを開始し、6番目のパラメーター(dwCreationFlags)でフラグDEBUG_ONLY_THIS_PROCESSを指定します。 このフラグは、デバッグを開始するプロセスを準備するようWindowsに指示します(イベントのデバッグ、プロセスの開始/終了、例外など)。 より詳細な説明は後で。 正確にDEBUG_ONLY_THIS_PROCESSを使用することに注意してください。 これは、実行するプロセスのみをデバッグし、それによって生成されるのではないことを意味します。

STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory( &si, sizeof(si) ); si.cb = sizeof(si); ZeroMemory( &pi, sizeof(pi) ); CreateProcess ( ProcessNameToDebug, NULL, NULL, NULL, FALSE, DEBUG_ONLY_THIS_PROCESS, NULL,NULL, &si, &pi );
      
      





その後、タスクマネージャーに新しいプロセスが表示されますが、実際にはまだ開始されていません。 新しく作成されたプロセスはまだ凍結されています。 いいえ、彼らは推測しませんでした。ResumeThreadを呼び出す必要はありませんが、デバッグサイクルを記述します。

デバッグループ



デバッグループはデバッガーの中心であり、WaitForDebugEvent関数を中心に構築されます。 DEBUG_EVENT構造体へのポインターとタイムアウト(DWORD)の2つのパラメーターを受け取ります。 INFINITEをタイムアウトとして指定します。 この関数はkernel32.dllに含まれているため、追加のライブラリをリンクする必要はありません。

 BOOL WaitForDebugEvent(DEBUG_EVENT* lpDebugEvent, DWORD dwMilliseconds);
      
      





DEBUG_EVENT構造には、イベントコード、プロセスID、ストリームID、適用されたイベント情報など、多くのデバッグ情報が含まれています。 WaitForDebugEventが完了して返されるとすぐに、デバッガーメッセージを取得し、ContinueDebugEventを呼び出してコードの実行を続行します。 以下に、最小限のデバッグサイクルを示します。

 DEBUG_EVENT debug_event = {0}; for(;;) { if (!WaitForDebugEvent(&debug_event, INFINITE)) return; ProcessDebugEvent(&debug_event); // User-defined function, not API ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, DBG_CONTINUE); }
      
      





ContinueDebugEventを呼び出して、OSにOPの実行を継続するように依頼します。 dwProcessIdおよびdwThreadIdは、プロセスとスレッドを示します。 これらの値は、WaitForDebugEventから取得しました。 最後のパラメーターは、実行を継続するかどうかを示します。 このパラメーターは、デバッグで例外がスローされた場合にのみ関連します。 これについては後で検討します。 それまでは、DBG_CONTINUEを使用します(別の可能な値はDBG_EXCEPTION_NOT_HANDLEDです)。

デバッグイベントの受信



例外カテゴリには、9つの主要なデバッグイベントと20のサブイベントがあります。 最も簡単なものから始めてこれを検討してください。 DEBUG_EVENTの構造は次のとおりです。

 struct DEBUG_EVENT { 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; };
      
      





WaitForDebugEventが成功すると、この構造にデータが設定されます。 dwDebugEventCodeは、どのデバッグイベントが到着したかを示します。 このコードに応じて、union'a uのメンバーの1つにイベントに関する情報が含まれます。 たとえば、dwDebugEventCode == OUTPUT_DEBUG_STRING_EVENTの場合、OUTPUT_DEBUG_STRING_INFOのみが正しく入力されます。

OUTPUT_DEBUG_STRING_EVENTの処理



出力にテキストを出力するには、開発者は通常OutputDebugString関数を使用します。 使用している言語/フレームワークに応じて、TRACE / ATLTRACEマクロに精通している必要があります。 .NET開発者はSystem.Diagnostics.Debug.Print / System.Diagnostics.Trace.WriteLineに精通している場合があります。 ただし、_DEBUGマクロが宣言され、デバッガーがメッセージを受信した場合、これらのメソッドはすべてOutputDebugStringを呼び出します。

デバッグメッセージを受信すると、DebugStringを処理します。 OUTPUT_DEBUG_STRING_INFOの構造は次のとおりです。

 struct OUTPUT_DEBUG_STRING_INFO { LPSTR lpDebugStringData; // char* WORD fUnicode; WORD nDebugStringLength; };
      
      





nDebugStringLengthフィールドには、終端のヌルを含む文字列の長さが含まれます。 fUnicodeフィールドは、文字列がANSIの場合はゼロであり、ユニコードの場合はゼロ以外です。 この場合、nDebugStringLength x2バイトを読み取る必要があります。 注意! lpDebugStringDataには、メッセージのある行へのポインターが含まれていますが、ポインターは、デバッガーではなく、 デバッグ中のプログラムのメモリに関するデータ参照しています。

別のプロセスのメモリからデータを読み取るには、ReadProcessMemoryを呼び出す必要があり、これを行う権限が必要です。 デバッグ用のプロセスを作成したため、解決に問題はありません。

 case OUTPUT_DEBUG_STRING_EVENT: { CStringW strEventMessage; // Force Unicode OUTPUT_DEBUG_STRING_INFO & DebugString = debug_event.u.DebugString; WCHAR *msg=new WCHAR[DebugString.nDebugStringLength]; // Don't care if string is ANSI, and we allocate double... ReadProcessMemory(pi.hProcess, // HANDLE to Debuggee DebugString.lpDebugStringData, // Target process' valid pointer msg, // Copy to this address space DebugString.nDebugStringLength, NULL); if ( DebugString.fUnicode ) strEventMessage = msg; else strEventMessage = (char*)msg; // char* to CStringW (Unicode) conversion. delete []msg; // Utilize strEventMessage }
      
      





メモリの読み取り中にopが終了した場合はどうなりますか?


さて、これは起こりませんdebugデバッグメッセージの処理中に、デバッガーがOPのすべてのスレッドをフリーズすることを思い出させてください。 したがって、プロセス自体は完了できません。また、タスクマネージャー(標準または非標準)もプロセスを完了できません。 試みると、次のメッセージで、デバッガーはEXIT_PROCESS_DEBUG_EVENTイベントを受け取ります。

CREATE_PROCESS_DEBUG_EVENTの処理



OPが開始されたばかりのときにイベントが表示されます。 これは、デバッガーが受信する最初のメッセージです。 このメッセージの場合、対応するDEBUG_EVENTフィールドはCreateProcessInfoになります。 以下に、CREATE_PROCESS_DEBUG_INFO自体の構造を示します。

 struct CREATE_PROCESS_DEBUG_INFO { HANDLE hFile; // The handle to the physical file (.EXE) HANDLE hProcess; // Handle to the process HANDLE hThread; // Handle to the main/initial thread of process LPVOID lpBaseOfImage; // base address of the executable image DWORD dwDebugInfoFileOffset; DWORD nDebugInfoSize; LPVOID lpThreadLocalBase; LPTHREAD_START_ROUTINE lpStartAddress; LPVOID lpImageName; // Pointer to first byte of image name (in Debuggee) WORD fUnicode; // If image name is Unicode. };
      
      





hProcessとhThreadは、PROCESS_INFORMATIONで取得したものと異なる場合があることに注意してください。 プロセスIDとスレッドIDは同じでなければなりません。 Windowsから取得する各ハンドルは異なります。 これにはさまざまな理由があります。

lpImageNameのようなhFileは、OPファイルの名前を取得するために使用できます。 確かに、このファイルの名前を知っているのは、それを起動したからです。 ただし、LOAD_DLL_DEBUG_EVENTメッセージを受け取ったときは、ライブラリの名前を知っておくとよいので、EXEまたはDLLの場所を知ることが重要です。

MSDNで読むことができるように、lpImageNameには完全なファイル名が含まれることはなく、OPメモリに含まれます。 さらに、完全なファイル名がOPメモリにもあるという保証はありません。 また、ファイル名が不完全な場合があります。 したがって、hFileからファイル名を取得します。

hFileからファイル名を取得する方法



残念ながら、 MSDNで説明されているメソッドを使用する必要があります。このメソッドには約10個の関数呼び出しが含まれています。 以下は短縮版です。

 case CREATE_PROCESS_DEBUG_EVENT: { CString strEventMessage = GetFileNameFromHandle(debug_event.u.CreateProcessInfo.hFile); // Use strEventMessage, and other members // of CreateProcessInfo to intimate the user of this event. }
      
      





お気付きかもしれませんが、この構造のいくつかのフィールドについては考慮していません。 次のパートでは、これらすべてを徹底的に検討します。

LOAD_DLL_DEBUG_EVENTの処理



このイベントはCREATE_PROCESS_DEBUG_EVENTに似ており、ご想像のとおり、このイベントはOSがDLLをロードしたときに発生します。 このイベントは、DLLが明示的または暗黙的に読み込まれるたびに発生します。 デバッグ情報には、DLLがロードされた時間とその仮想アドレスのみが含まれます。 イベントを処理するには、union'aフィールドLoadDllを使用します。 タイプはLOAD_DLL_DEBUG_INFOです

 struct LOAD_DLL_DEBUG_INFO { HANDLE hFile; // Handle to the DLL physical file. LPVOID lpBaseOfDll; // The DLL Actual load address in process. DWORD dwDebugInfoFileOffset; DWORD nDebugInfoSize; LPVOID lpImageName; // These two member are same as CREATE_PROCESS_DEBUG_INFO WORD fUnicode; };
      
      





ファイル名を取得するには、CREATE_PROCESS_DEBUG_EVENTで使用したのと同じGetFileNameFromHandle関数を使用します。 UNLOAD_DLL_DEBUG_EVENTについて話すときにこのコードを表示します。 UNLOAD_DLL_DEBUG_EVENTイベントには、DLLライブラリの名前に関する完全な情報が含まれていません。

CREATE_THREAD_DEBUG_EVENTの処理



このイベントは、OPが新しいスレッドを作成するときに生成されます。 CREATE_PROCESS_DEBUG_EVENTとほぼ同様に、このイベントは新しいスレッドが開始される前に発生します。 このイベントに関する情報を取得するには、CreateThreadフィールドを使用します。 CREATE_THREAD_DEBUG_INFOの構造は次のとおりです。

 struct CREATE_THREAD_DEBUG_INFO { // Handle to the newly created thread in debuggee HANDLE hThread; LPVOID lpThreadLocalBase; // pointer to the starting address of the thread LPTHREAD_START_ROUTINE lpStartAddress; };
      
      





スレッドIDはDEBUG_EVENT :: dwThreadIdで利用できるため、スレッドに関するすべての情報を簡単に表示できます。

 case CREATE_THREAD_DEBUG_EVENT: { CString strEventMessage; strEventMessage.Format(L"Thread 0x%x (Id: %d) created at: 0x%x", debug_event.u.CreateThread.hThread, debug_event.dwThreadId, debug_event.u.CreateThread.lpStartAddress); // Thread 0xc (Id: 7920) created at: 0x77b15e58 }
      
      





lpStartAddress-デバッガーではなく、OPに関連するストリーム関数の開始アドレス。 完全を期すために表示するだけです。 このイベントは、メインのOPスレッドが動作を開始したときには生成されず、新しいメインスレッドが作成されたときにのみ生成されることに注意してください。

EXIT_THREAD_DEBUG_EVENTを処理しています



このイベントは、子スレッドが完了し、システムに戻りコードを返すとすぐに生成されます。 DEBUG_EVENTのdwThreadIdフィールドには、終了スレッドのIDが含まれています。 CREATE_THREAD_DEBUG_EVENTからストリームおよびその他の情報のハンドルを取得するには、この情報を何らかの配列に格納する必要があります。 このイベントに関する情報を取得するには、EXIT_THREAD_DEBUG_INFOタイプのExitThreadフィールドを使用します。

 struct EXIT_THREAD_DEBUG_INFO { DWORD dwExitCode; // The thread exit code of DEBUG_EVENT::dwThreadId };
      
      





以下は、イベントハンドラのコードです。

 case EXIT_THREAD_DEBUG_EVENT: { CString strEventMessage; strEventMessage.Format( _T("The thread %d exited with code: %d"), debug_event.dwThreadId, debug_event.u.ExitThread.dwExitCode); // The thread 2760 exited with code: 0 }
      
      





UNLOAD_DLL_DEBUG_EVENTの処理



もちろん、イベントには、OPのメモリからの情報とページ化されたDLLが含まれます。 しかし、それほど単純ではありません! FreeLibraryが呼び出されたときにのみ生成され、システム自体がライブラリをアンロードするときは生成されません。 詳細については、UnloadDll(UNLOAD_DLL_DEBUG_INFO)を使用してください。

 struct UNLOAD_DLL_DEBUG_INFO { LPVOID lpBaseOfDll; };
      
      





ご覧のとおり、ライブラリのベースアドレスのみが利用可能です。 そのため、LOAD_DLL_DEBUG_EVENTのコードについてすぐに説明しませんでした。 DLLの読み込み中に、lpBaseOfDllも取得します。 Mapを使用して、住所に加えてライブラリの名前を保存できます。

すべてのライブラリロードイベントがアンロードイベントを受け取るわけではないことに注意することが重要です。 ただし、LOAD_DLL_DEBUG_EVENTはライブラリのロード方法に関する情報を提供しないため、すべてのライブラリ名を保存する必要があります。

以下は、両方のイベントを処理するコードです。

 std::map < LPVOID, CString > DllNameMap; ... case LOAD_DLL_DEBUG_EVENT: { strEventMessage = GetFileNameFromHandle(debug_event.u.LoadDll.hFile); // Storing the DLL name into map. Map's key is the Base-address DllNameMap.insert( std::make_pair( debug_event.u.LoadDll.lpBaseOfDll, strEventMessage) ); strEventMessage.AppendFormat(L" - Loaded at %x", debug_event.u.LoadDll.lpBaseOfDll); } break; ... case UNLOAD_DLL_DEBUG_EVENT: { strEventMessage.Format(L"DLL '%s' unloaded.", DllNameMap[debug_event.u.UnloadDll.lpBaseOfDll] ); // Get DLL name from map. } break;
      
      





EXIT_PROCESS_DEBUG_EVENTを処理しています



これは最も単純なイベントの1つであり、ご想像のとおり、OPプロセスの終了時に呼び出されます。 このイベントは、プロセスがどのように終了したかを示します。通常または緊急(たとえば、タスクマネージャーを使用)、またはデバッグされたプログラムがクラッシュしました。 EXIT_PROCESS_DEBUG_INFO ExitProcessから取得した情報。

 struct EXIT_PROCESS_DEBUG_INFO { DWORD dwExitCode; };
      
      





このイベントを受け取ったらすぐに、デバッグサイクルを中断し、デバッグストリームを完了する必要があります。 これを行うために、デバッグの完了を知らせるフラグを設定できます。

 bool bContinueDebugging=true; ... case EXIT_PROCESS_DEBUG_EVENT: { strEventMessage.Format(L"Process exited with code: 0x%x", debug_event.u.ExitProcess.dwExitCode); bContinueDebugging=false; } break;
      
      







EXCEPTION_DEBUG_EVENTを処理しています



これは、すべてのデバッグイベントで最も驚くべき複雑なことです。 MSDNから:

このイベントは、デバッグされたプロセスで例外が発生したときに生成されます(0で除算したとき、配列の境界を越えたとき、int 3命令を実行したとき、またはSEHで説明したその他の例外)。 DEBUG_EVENT構造には、EXCEPTION_DEBUF_INFO構造が含まれています。 例外を説明するのは彼女です。


このイベントの処理の説明には、完全にそれを伝えるための別の記事が必要です(はい、部分的であっても)。 したがって、例外の1つのタイプについて説明します。

例外フィールドには、発生したばかりの例外に関する情報が含まれています。 以下に、EXCEPTION_DEBUG_INFO構造の説明を示します。

 struct EXCEPTION_DEBUG_INFO { EXCEPTION_RECORD ExceptionRecord; DWORD dwFirstChance; };
      
      





ExceptionRecordフィールドには、例外に関する詳細情報が含まれています。

 struct EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; // 15 };
      
      





EXCEPTION_RECORDを掘り下げる前に、EXCEPTION_DEBUG_INFO :: dwFirstChanceと話し合いたいと思います。

プロセスがデバッグ中の場合、デバッガーは常にOPが取得する前に例外を取得します。 C ++でアプリケーションをデバッグしているときに、「SomeModuleの0x00412882での最初の例外」というエントリが表示されているはずです。 これはファーストチャンスの例外を指します。 同じ例外は、セカンドチャンスの例外であってもなくてもかまいません。

OPが例外をスローした場合、セカンドチャンスとして扱われます。 OPはこの例外を処理する場合もあれば、単に落下する場合もあります。 これらの例外はC ++例外ではなく、Windows SEHエンジンに属します。 記事の次の部分でもう少し明らかにします。

まず、デバッガーは例外メッセージ(ファーストチャンス例外)を受け取ります。これは、OPよりも速く例外を処理するのに役立ちます。 一部のライブラリは、デバッガーが作業を行うのを支援するために、最初の例外をスローします。

ContinueDebugEventについてもう少し



この関数の3番目のパラメーター(dwContinueStatus)は、例外を受け取るためだけに重要です。 他のイベントの場合、このパラメーターは無視されます。

例外を受け取った後、ContinueDebugEventを次のように呼び出す必要があります。



DBG_CONTINUEを返すことができないデバッグイベント中にDBG_CONTINUEが返されると、デバッガーでまったく同じ例外がスローされ、同じイベントが即座に発生することに注意してください。 ただし、デバッガの記述を始めたばかりなので、銃ではなく安全なパチンコで遊びましょう。EXCEPTION_NOT_HANDLEDを返します。 この記事の例外はint 3(ブレークポイント)で、これについては後で説明します。

例外コード





  1. EXCEPTION_ACCESS_VIOLATION
  2. EXCEPTION_ARRAY_BOUNDS_EXCEEDED
  3. EXCEPTION_BREAKPOINT
  4. EXCEPTION_DATATYPE_MISALIGNMENT
  5. EXCEPTION_FLT_DENORMAL_OPERAND
  6. EXCEPTION_FLT_DIVIDE_BY_ZERO
  7. EXCEPTION_FLT_INEXACT_RESULT
  8. EXCEPTION_FLT_INVALID_OPERATION
  9. EXCEPTION_FLT_OVERFLOW
  10. EXCEPTION_FLT_STACK_CHECK
  11. EXCEPTION_FLT_UNDERFLOW
  12. EXCEPTION_ILLEGAL_INSTRUCTION
  13. EXCEPTION_IN_PAGE_ERROR
  14. EXCEPTION_INT_DIVIDE_BY_ZERO
  15. EXCEPTION_INT_OVERFLOW
  16. EXCEPTION_INVALID_DISPOSITION
  17. EXCEPTION_NONCONTINUABLE_EXCEPTION
  18. EXCEPTION_PRIV_INSTRUCTION
  19. EXCEPTION_SINGLE_STEP
  20. EXCEPTION_STACK_OVERFLOW


落ち着いて、私はそれらすべてを説明するつもりはありません。 EXCEPTION_BREAKPOINTのみ:

 case EXCEPTION_DEBUG_EVENT: { EXCEPTION_DEBUG_INFO& exception = debug_event.u.Exception; switch( exception.ExceptionRecord.ExceptionCode) { case STATUS_BREAKPOINT: // Same value as EXCEPTION_BREAKPOINT strEventMessage= "Break point"; break; default: if(exception.dwFirstChance == 1) { strEventMessage.Format(L"First chance exception at %x, exception-code: 0x%08x", exception.ExceptionRecord.ExceptionAddress, exception.ExceptionRecord.ExceptionCode); } dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED; } break; }
      
      





ブレークポイントとは何かを認識する必要があります。 標準的な観点を超えて、DebugBreak APIを使用するか、アセンブリ命令{int 3}を使用して、ブレークポイントを呼び出すことができます。 .NETでは、System.Diagnostics.Debugger.Breakを使用して作成できます。 デバッガーはコードSTATUS_BREAKPOINTを受け取ります(EXCEPTION_BREAKPOINTと同じ)。 デバッガは通常、このイベントを使用して現在のプロセスを停止し、イベントが発生した場所のソースコードを表示できます。 ただし、デバッガの開発は始まったばかりなので、ソースコードなしで基本的な情報のみをユーザーに表示します。

デバッガーの下にないアプリケーションでブレークポイントが呼び出されると、単純にクラッシュします。 次の構成を使用できます。

 if ( !IsDebuggerPresent() ) AfxMessageBox(L"No debugger is attached currently."); else DebugBreak();
      
      





結論として、簡単なデバッグイベントEXCEPTION_DEBUG_EVENTを提供したいと思います。 このイベントは常に開催されます。 Visual Studioなどのデバッガーは無視しますが、WinDbgは無視しません。

おわりに



DebugMeには任意のデバッガーを使用します。



第二部はさらにおもしろくなり、彼女は途中です!

UPD: パート2



All Articles