Hatena::ブログ(Diary)

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

2007/11/20 デバッガの作り方

たまにはハックっぽいことをしたいなと思って。

簡単なデバッガ(のコアだけ)を作ってみた。


デバッガを作るために必要なものは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 );
}

基本はこんな感じ。

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

シングルステップ実行の仕方 シングルステップ実行の仕方を含むブックマーク シングルステップ実行の仕方のブックマークコメント

x86CPUフラグレジスタには、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 ブレークポイント - 2を含むブックマーク ブレークポイント - 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 になったら止まって!
トラックバック - http://d.hatena.ne.jp/Hossy/20071120