ほっしーの技術ネタ備忘録

技術ネタの備忘録です。基本的に私が忘れないためのものです。他の人の役にも立つといいなぁ。

デバッガの作り方

たまにはハックっぽいことをしたいなと思って。
簡単なデバッガ(のコアだけ)を作ってみた。


デバッガを作るために必要なものは2つ。

  1. アタッチする
  2. デバッグイベントを処理する

デバッガとしてアタッチする


まずデバッガからプログラムを起動する場合は

PROCESS_INFORMATION pcInfo;
STARTUPINFO supInfo = { sizeof(STARTUPINFO) };
BOOL bResult = CreateProcess( NULL, lpCmdLine, NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS | CREATE_SUSPENDED | DEBUG_PROCESS, NULL, NULL, &supInfo, &pcInfo );
if( !bResult ) throw std::runtime_error( "CreateProcess" );

こんな感じで CreateProcess API を呼ぶときに DEBUG_PROCESS フラグを立てるだけ。
他はオプション。お好みで。


も1つ、既に動いてるプロセスにアタッチしたい場合。

DebugActiveProcess( hProcess );


簡単ですね。

デバッグイベントの処理


デバッガとしてアタッチしている状態で WaitForDebugEvent API を呼ぶと、
次のデバッグイベントが発生するまで待機します。


返されたイベントを処理後、ContinueDebugEvent で継続できます。

for(;;) {
	DEBUG_EVENT evDebug;
	WaitForDebugEvent( &evDebug, INFINITE );

	switch( evDebug.dwDebugEventCode ) {
	case CREATE_PROCESS_DEBUG_EVENT: printf("CREATE_PROCESS_DEBUG_EVENT\n"); break;
	case CREATE_THREAD_DEBUG_EVENT:  printf("CREATE_THREAD_DEBUG_EVENT\n"); break;
	case EXCEPTION_DEBUG_EVENT:      printf("EXCEPTION_DEBUG_EVENT\n"); break;
	case EXIT_PROCESS_DEBUG_EVENT:   printf("EXIT_PROCESS_DEBUG_EVENT\n"); return;
	case EXIT_THREAD_DEBUG_EVENT:    printf("EXIT_THREAD_DEBUG_EVENT\n"); break;
	case LOAD_DLL_DEBUG_EVENT:       printf("LOAD_DLL_DEBUG_EVENT\n"); break;
	case OUTPUT_DEBUG_STRING_EVENT:  printf("OUTPUT_DEBUG_STRING_EVENT\n"); break;
	case RIP_EVENT:                  printf("RIP_EVENT\n"); break;
	case UNLOAD_DLL_DEBUG_EVENT:     printf("UNLOAD_DLL_DEBUG_EVENT\n"); break;
	}

	ContinueDebugEvent( evDebug.dwProcessId, evDebug.dwThreadId, DBG_CONTINUE );
}

基本はこんな感じ。

簡単すぎてビックリですね。

シングルステップ実行の仕方

x86 系 CPU のフラグレジスタには、Trap Flag というフラグがあって、
これを立てると次の1命令を実行してトラップ例外を起こします。

void SetSingleStepMode( HANDLE hThread )
{
	CONTEXT ctx = { CONTEXT_CONTROL };
	GetThreadContext( hThread, &ctx );
	ctx.EFlags |= 0x00000100; // TrapFlag
	SetThreadContext( hThread, &ctx );
}

スレッドのコンテキストには、その時の CPU のレジスタなどの
情報が格納されています。EFlags がフラグレジスタ
CONTEXT_INTEGER を付けるとおなじみ EAX〜EDX レジスタも見れます。


さて、TrapFlag を立てておくと1命令実行後、EXCEPTION_SINGLE_STEP 例外が発生するので

	case EXCEPTION_DEBUG_EVENT:
		if( evDebug.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_SINGLE_STEP ) {
			SetSingleStepMode( hThread );
		}
		break;

こんな風に拿捕します。
一回発生するとフラグが落ちるので、毎回設定しなおします。


ほんとは Get/SetThreadContext する前に Suspend する必要がありますが、
WaitForDebugEvent ループ中は実行停止されているので問題なし。
CreateProcess 直後にするなら CREATE_SUSPENDED フラグを。

ブレークポイントの仕掛け方


デバッガで必要な機能もう1つ。ブレークポイントですね。
これは x86 CPU のデバッグレジスタを利用して実装します。


まず仕掛け。

void SetBreakPoint( HANDLE hThread, DWORD dwAddress )
{
	CONTEXT ctx = { CONTEXT_DEBUG_REGISTERS };
	GetThreadContext( hThread, &ctx );
	ctx.Dr0 = dwAddress;
	ctx.Dr7 |= 0x00000001;
	SetThreadContext( hThread, &ctx );
}


Dr0, Dr1, Dr2, Dr3 と、4つのブレークポイントを仕掛けることが出来ます。
Dr7 はコントロール用のレジスタで、下位16bitで有効・無効を切り替え、上位16bitで用法を決めます。


まず下位16bit。Dr0〜Dr3 がそれぞれ、0x0001, 0x0004, 0x0010, 0x0040 を立てると有効になります。
上位16bitはとりあえず 0 にしておけば実行時ブレークポイントとして働きます。
(ちなみに 0x00030001 とかやると Dr0 のアドレスのメモリアクセスに対してブレークポイントになります)

       2bits 2bits 2bits 2bits
    --+-----+-----+-----+-----+
Dr7   | Dr3 | Dr2 | Dr1 | Dr0 |
    --+-----+-----+-----+-----+

さて、デバッグレジスタを設定できたら実行再開します。
ブレークポイントに引っかかったら先ほど同様 EXCEPTION_SINGLE_STEP 例外が発生するので

	case EXCEPTION_DEBUG_EVENT:
		if( evDebug.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_SINGLE_STEP ) {
			print("BreakPoint!!\n");
		}
		break;

こんな感じで拿捕してあげます。
Dr6 レジスタブレークポイントの状態で、下位4bitでヒットしたブレークポイントが分かります。
Dr0〜Dr3 がそれぞれ 0x1, 0x2, 0x4, 0x8 に対応していて、立っているビットが今回ヒットしたブレークポイントです。

       1bits 1bits 1bits 1bits
    --+-----+-----+-----+-----+
Dr6   | Dr3 | Dr2 | Dr1 | Dr0 |
    --+-----+-----+-----+-----+
      <- どれにヒットしたか  ->


シングルステップと同じ例外なので、併用する場合は

	case EXCEPTION_DEBUG_EVENT:
		printf("EXCEPTION_DEBUG_EVENT\n");
		if( evDebug.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_SINGLE_STEP ) {
			CONTEXT ctx = { CONTEXT_CONTROL | CONTEXT_DEBUG_REGISTERS };
			GetThreadContext( hThread, &ctx );
			if( ctx.Dr6 & 0x00004000 ) { // SingleStep フラグ
				printf("EIP: 0x%08X\n", ctx.Eip );
				SetSingleStepMode( hThread );
			} else {
				printf("BreakPoint.  Dr6: 0x%08X\n", ctx.Dr6);
				ctx.Dr6 = 0x00000000; // DebugStatus はクリアされない
				ctx.Dr7 = 0x00000000; // とりあえず全部クリア
				SetThreadContext( hThread, &ctx );
			}
		}
		break;

このように、Dr6 レジスタのフラグを見て判定します。
Dr6 は自動でクリアされないので、ブレークポイントの場合は手動でクリアします。
また、このままだと連続してブレークポイントにかかるため、とりあえず全部停止します。

ブレークポイント - 2


普通のデバッガにあるような実行時に止めるブレークポイントは上のように出来ます。
Dr7 の上位16bitを弄ると少し特殊な

  • 特定のアドレスに書き込んだとき
  • 特定のアドレスに読み書きしたとき

ブレークポイントを仕掛けることができます。

     4bits 4bits 4bits 4bits
    +-----+-----+-----+-----+--
Dr7 | Dr3 | Dr2 | Dr1 | Dr0 |
    +-----+-----+-----+-----+--
     <-- ブレークのタイプ -->

上位16bitを下から4bitずつ Dr0〜Dr3 に対応していて、

   <-    Dr0    ->
                                        • -
| 長さ | タイプ |
                                        • -
タイプ(2bit) 0: 実行時 1: メモリへの書き込み時 3: メモリへの読み書き タイプが 1 or 3 の場合はそのアクセス長で 長さ(2bit) 0: BYTE 1: WORD 3: DWORD

このように色々選べます。
うーん、高機能。


上手く使うと以下のようなことが出来ます。

  • この変数、いつの間に NULL になってるんだろう…?
  • この変数が 0 になったら止まって!