📜 ⬆️ ⬇️

Hand held breakpoints (for x86 architecture)

Any programmer who has ever looked into the debugger is familiar with the concept of a breakpoint (aka breakpoint). It would seem that nothing could be simpler than setting a breakpoint with a couple of mouse clicks in the graphical interface or a command in the debugger console, but not always the life of a system programmer is so simple and sometimes it becomes necessary to set breakpoints automatically — from within the program itself.


Instruction breakpoints



[this type of breakpoint was written in sufficient detail two years ago , so I briefly cited only general considerations]
')
Suppose we write a JIT compiler and want to set breakpoints inside the code it generates. It turns out for this purpose it is enough to insert into the code stream just one int 3 instruction in those places where we would like to stop the execution of the program. When the processor hits this instruction, it will generate a corresponding interrupt, which the OS kernel will process and turn, for example, under Linux into a SIGTRAP signal. A free-running program will simply crash when it encounters int 3 , but the debugger will catch this signal, stop the program and allow it to investigate its state.

By the way, the debuggers themselves also use this instruction: they simply replace the instructions in the memory when we ask them to break. That is why int 3 is encoded with one byte ( 0xCC ) and not two, like the rest of the instructions for generating a program interrupt int X ( 0xCD imm8 ) - otherwise int 3 would not be suitable for substituting single-byte instructions.

As we can see, there is nothing difficult in the manual placement of instruction breakpoints. You can even implement their interactive on-off: just remember their positions and replace unnecessary with an empty nop ( 0x90 ) instruction.

Much more interesting is the case with a different type of breakpoints - breakpoints for memory access.

Access breakpoints



Suppose we are debugging memory corruption in the runtime environment with a copying garbage collector, which constantly compactifies the heap and shuffles objects in any other ways. We were able to find out approximately which field in which object is damaged, but simply loading the program into the debugger and putting them at a breakpoint on access to this field does not work, because the GC is constantly underfoot with its movements. Therefore, we have a rational desire for the good-natured Garbage Collector to set up / upgrade this bryak.

In other words, we want to give it a function

void SetAccessBreak(void* addr);

Here we are dr0 debugging registers dr0 , dr1 , dr2 , dr3 and their uncle Chernomor dr7 , containing control flags.

It’s quite simple to use them: in one of the dr0 - dr3 registers dr0 load the address to be monitored, and in dr7 set the appropriate checkboxes to determine whether the corresponding register breakpoint is activated or not, it monitors the event (execution / read / read-or-write at this address), data size (1 byte, 2 bytes, 4 bytes, 8 bytes). In order not to waste space on vague verbal explanations of flag encoding rules, I will immediately cite two utility functions: MakeFlags , which encodes flags for a given debug register in the format used in dr7 , and MakeMask , which for a given register calculates a bit mask covering all flags related to this register (a similar mask is needed if we want to reset all flags).

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/>


Armed with these functions, you can rush into the water without knowing the ford and try to implement SetAccessBreak with a simple inline assembler:

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/>
}


However, this attempt is doomed to failure: access to debug registers is possible only from the zero protection ring, i.e. from the core. However, the registers are plus-useful (any modern debugger uses them), so the OS usually provides an API for accessing these registers. For example, on Mac OS X you can read and write these registers through the thread_get_state / thread_set_state . Having got access to the necessary registers through them, we easily implement 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/>
}


That's all! Now the good uncle of the Janitor-Garbage Collector can manage breakpoints by himself. Who does not believe, can write a small test program:

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/>


and running it under the debugger will make sure that each execution is interrupted in the right places - i.e. on the instructions following the one that accesses the memory area:

(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/103073/


All Articles