手動トラクションブレークポイント(x86アーキテクチャの場合)

デバッガーを調べたことのあるプログラマーは、ブレークポイント(ブレークポイント、ブレークポイントとも呼ばれます)の概念に精通しています。 グラフィカルインターフェイスでマウスを使用してブレークポイントを数回クリックするか、デバッガーのコンソールでコマンドを実行するよりも簡単なことはないように思えますが、システムプログラマーの生活は必ずしも単純ではなく、プログラム内から自動的にブレークポイントを設定することが必要になる場合があります。


命令ブレークポイント



[このタイプのブレークポイントについては、 2年前に詳細に記述されていることが判明したため、一般的な考慮事項のみを簡単に引用しました]

JITコンパイラを記述し、生成するコード内にブレークポイントを設定するとします。 このためには、プログラムの実行を停止したい場所に、 int 3命令を1つだけコードストリームに挿入すれば十分です。 プロセッサがこの命令を検出すると、適切な割り込みを生成します。これは、OSのカーネルが処理し、たとえばLinuxでSIGTRAP信号に変換します。 フリースイムで起動されたプログラムはint 3に遭遇すると単純に落ちますが、デバッガーはこの信号をキャッチし、プログラムを停止して、その状態を調べることができます。

ちなみに、デバッガー自体もこの命令を使用します。つまり、デバッガーにブレークを要求するときに、単にメモリー内の命令に置き換えます。 そのため、 int 3は、プログラム割り込みint X0xCD imm8 )を生成するための残りの命令のように、2バイトではなく1バイト( 0xCC )でエンコードされます。そうでない場合、 int 3はシングルバイト命令の置換には適しません。

ご覧のとおり、命令ブレークポイントの手動配置に複雑なことはありません。 インタラクティブなオンオフを実装することもできます。位置を覚えて、不要なものを空のnop0x90 )命令に0x90ください。

さらに興味深いのは、別の種類のブレークポイント-メモリアクセスブレークポイントの場合です。

ブレークポイントにアクセスする



コピーガベージコレクタを使用してランタイムでメモリ破損をデバッグしているとしましょう。コピーガベージコレクタは、ヒープを常に圧縮し、他のすべての方法でメモリ内のオブジェクトをシャッフルします。 どのオブジェクトのどのフィールドが破損しているかをおおよそ知ることができましたが、プログラムをデバッガにロードしてこのフィールドへのアクセスにブレークポイントを設定するだけでは動作しません。 したがって、善良なガベージコレクター自身がこのガベージを公開/更新することを合理的に望んでいます。

言い換えれば、私たちは彼の処分で機能を置きたい

void SetAccessBreak(void* addr);

ここで、良い仲間が救助に来ます。デバッグdr0dr1dr2dr3と、おじさんのChernomor dr7 、制御フラグが含まれています。

それらの使用は非常に簡単です:レジスターdr0 - dr3いずれかで、監視する必要があるアドレスdr3ロードし、 dr7で、対応するレジスターのブレークポイントがアクティブかどうかを決定する適切なフラグdr7設定します、それが続くイベント(実行/読み取り/読み取りまたは書き込み)このアドレスで)、データサイズ(1バイト、2バイト、4バイト、8バイト)。 フラグエンコーディングルールのあいまいな口頭の説明にスペースを無駄にしないために、2つのユーティリティ関数のコードをすぐに提供します。特定のデバッグレジスタのフラグをdr7で使用される形式でdr7このレジスタへ(すべてのフラグをクリアしたい場合、同様のマスクが必要です)。

enum DebugRegister { <br/>
kDR0 = 0 ,<br/>
kDR1 = 2 ,<br/>
kDR2 = 4 ,<br/>
kDR3 = 6 <br/>
} ; <br/>
<br/>
enum BreakState { <br/>
kDisabled = 0 , // disabled - 00 <br/>
kEnabledLocally = 1 , // task local - 01 <br/>
kEnabledGlobally = 2 , // global - 10 <br/>
kBreakStateMask = 3 // mask 11 <br/>
} ; <br/>
<br/>
enum Condition { <br/>
kWhenExecuted = 0 , // on execution - 00 <br/>
kWhenWritten = 1 , // on write - 01 <br/>
kWhenWrittenOrReaden = 3 , // on read or write - 11 <br/>
kConditionMask = 3 // mask 11 <br/>
} ; <br/>
<br/>
enum Size { <br/>
kByte = 0 , // 1 byte - 00 <br/>
kHalfWord = 1 , // 2 bytes - 01 <br/>
kWord = 3 , // 4 bytes - 11 <br/>
kDoubleWord = 2 , // 5 bytes - 10 <br/>
kSizeMask = 3 // mask 11 <br/>
} ; <br/>
<br/>
<br/>
uint32_t MakeFlags ( DebugRegister reg, BreakState state, Condition cond, Size size ) { <br/>
return ( state | cond << 16 | size << 24 ) << reg ; <br/>
} <br/>
<br/>
<br/>
uint32_t MakeMask ( DebugRegister reg ) { <br/>
return MakeFlags ( reg, kBreakStateMask, kConditionMask, kSizeMask ) ; <br/>
} <br/>


これらの機能を備えているため、フォードを知らずに急いで水にSetAccessBreak 、簡単な組み込みアセンブラでSetAccessBreakを実装しようとすることができます。

bool SetAccessBreak ( void * addr,<br/>
DebugRegister reg,<br/>
Condition cond,<br/>
Size size ) { <br/>
const uint32_t control = MakeFlags ( reg, kEnabledLocally, cond, size ) ; <br/>
__asm__ ( "movl %0, %%dr0 \n " <br/>
"movl %1, %%dr7 \n " : : "r" ( addr ) , "r" ( control ) : ) ; <br/>
}


ただし、この試みは失敗する運命にあります。デバッグレジスタへのアクセスは、ゼロ保護リングからのみ可能です。 コアから。 ただし、これらのプラスプラスレジスタは便利です(最新のデバッガーはこれらを使用します)。したがって、OSは通常これらのレジスタにアクセスするためのAPIを提供します。 たとえば、Mac OS Xでは、 thread_get_state / thread_set_stateを使用してこれらのレジスタを読み書きできます。 それらを介して必要なレジスタにアクセスできSetAccessBreakを簡単に実装しSetAccessBreak

bool SetAccessBreak ( pthread_t target_thread,<br/>
void * addr,<br/>
DebugRegister reg,<br/>
Condition cond,<br/>
Size size ) { <br/>
x86_debug_state dr ; <br/>
mach_msg_type_number_t dr_count = x86_DEBUG_STATE_COUNT ; <br/>
<br/>
// POSIX MACH . <br/>
mach_port_t target_mach_thread = pthread_mach_thread_np ( target_thread ) ; <br/>
<br/>
// . <br/>
kern_return_t rc = thread_get_state ( target_mach_thread,<br/>
x86_DEBUG_STATE,<br/>
reinterpret_cast < thread_state_t > ( & dr ) ,<br/>
& dr_count ) ; <br/>
<br/>
// <br/>
if ( rc ! = KERN_SUCCESS ) return false ; <br/>
<br/>
// , . <br/>
switch ( reg ) { <br/>
case kDR0 : dr. uds . ds32 .__dr0 = reinterpret_cast < unsigned int > ( addr ) ; break ; <br/>
case kDR1 : dr. uds . ds32 .__dr1 = reinterpret_cast < unsigned int > ( addr ) ; break ; <br/>
case kDR2 : dr. uds . ds32 .__dr2 = reinterpret_cast < unsigned int > ( addr ) ; break ; <br/>
case kDR3 : dr. uds . ds32 .__dr3 = reinterpret_cast < unsigned int > ( addr ) ; break ; <br/>
} <br/>
<br/>
// . <br/>
dr. uds . ds32 .__dr7 & = ~MakeMask ( reg ) ; <br/>
<br/>
// . <br/>
dr. uds . ds32 .__dr7 | = MakeFlags ( reg, kEnabledLocally, cond, size ) ; <br/>
<br/>
// . <br/>
rc = thread_set_state ( target_mach_thread,<br/>
x86_DEBUG_STATE,<br/>
reinterpret_cast < thread_state_t > ( & dr ) ,<br/>
dr_count ) ; <br/>
<br/>
// . <br/>
if ( rc ! = KERN_SUCCESS ) return false ; <br/>
<br/>
// . <br/>
return true ; <br/>
}


以上です! これで、優れた汚れたJanitor-Garbage Collectorがブレークポイントを管理できます。 誰も小さなテストプログラムを書くことができないと信じています:

static int16_t foo = 0 ; <br/>
static int32_t bar = 0 ; <br/>
<br/>
int main ( int argc, char * argv [ ] ) { <br/>
foo = 1 ; <br/>
bar = 1 ; <br/>
SetAccessBreak ( pthread_self ( ) , & bar, kDR0, kWhenWritten, kWord ) ; <br/>
foo = 2 ; <br/>
bar = 2 ; <br/>
SetAccessBreak ( pthread_self ( ) , & foo, kDR0, kWhenWritten, kHalfWord ) ; <br/>
foo = 3 ; <br/>
bar = 3 ; <br/>
return 0 ; <br/>
} <br/>


デバッガーで実行すると、それぞれが適切な場所で中断されるようになります。 メモリ領域にアクセスする命令に続く命令で:

(gdb) r
Starting program: /Users/mraleph/test
Reading symbols for shared libraries +++. done

Program received signal SIGTRAP, Trace/breakpoint trap.
main (argc=1, argv=0xbffff9f8) at test.cc:107
106 bar = 2; <= triggered SIGTRAP -- mr.aleph
107 SetAccessBreak(pthread_self(), &foo, kDR0, kWhenWritten, kHalfWord);
(gdb) c
Continuing.

Program received signal SIGTRAP, Trace/breakpoint trap.
main (argc=1, argv=0xbffff9f8) at test.cc:109
108 foo = 3; <= triggered SIGTRAP -- mr.aleph
109 bar = 3;
(gdb) c
Continuing.

Program exited normally.

Source: https://habr.com/ru/post/J103073/


All Articles