Hardware breakpoints on Windows

| categories: windows, debugging

The Intel documentation on DRX breakpoints is at times confusing and complicated and (obviously) doesn't really give a clue what the implementation on Windows looks like. This article hopefully clears up some of the issues you might be faced with while implementing hardware breakpoints in your debugger/application.

Note: this is not an introduction to the topic, nor to be used as a reference. For solid information get a copy of the "Intel 64 and IA-32 Architectures Software Developer's Manual", specifically Volume 3B ("System Programming Guide, Part 2"), Chapter 18 ("Debugging and Performance Monitoring").

Local only

On Windows, only local hardware breakpoints work, ie. they only exist in a thread's context, not system wide. Kinda makes sense considering each thread has its own set of registers. Global hardware breakpoints aren't global to the process, but global to the whole system.

Windows will make sure you don't enable system-wide breakpoints, at least more recent versions.

Basics

When a hardware breakpoint is hit, an EXCEPTION_SINGLE_STEP exception is raised. This is kind of confusing at first, but since both DRX breakpoints and single step are so called "debug exceptions" (which translates to INT1) it is the same type of exception.

You differentiate between the two by examining DR6. It's possible for both to be reported in one event.

According to the Intel manual you have to clear DR6 after every debug exception. However, it looks like Windows does that for us. I would recommend clearing DR6 yourself though, because I'm not sure that this is guaranteed on all Windows versions (at least XP SP3 and Vista SP2 do clear it).

If the conditions specified in DR7 are fullfilled and one the breakpoints is triggered, you receive an EXCEPTION_SINGLE_STEP_EVENT. It's possible that the exception is raised because of several simultaneous breakpoints. You will get seperate events for single step and hardware breakpoints on execution. This is not the case for single step and breakpoint on read/write and with breakpoints of the same type. The safe way is to examine all bits in DR6 and act accordingly. If 2 breakpoints are identical, DR6 reports both. Be aware that the bits for a breakpoint are set in DR6 even if the breakpoint is not enabled in DR7. To be perfectly sure, also check if the enable bit is set in DR7.

Since breakpoints on read/write are trap exceptions (ie. they break after the exception that caused the breakpoint to trigger) you can just continue execution. Breakpoints on execution are faults, they break before the instruction is executed. If you would just continue to execute, the breakpoint would trigger again as it executes the same address again and cause the breakpoint to trigger.

The fairly easy solution is to set the resume flag in the EFlags register (bit 16). It temporarily disables breakpoints on execution for one instruction. It doesn't affect any other type of breakpoints.

If only it was that easy...

There is only one drawback which made me avoid this. VMWare (Workstation 6.5) seems to ignore or disable the trap flag so that you'll never get past this instruction. This might be a minor issue to some people but many use VMWare for reverse engineering or just for running stuff you don't necessarily want to run on your normal box.

I'm not sure if Virtual Box or other VMs are affected though.

Single step to the rescue!

Now, what else can we do? The only feasible solution is to disable the breakpoint in DR7, set the trap flag to break on the next instruction and re-enable it after the single step exception.

However, be aware that there might be more than 1 breakpoint you have to re-enable after a single step event.

Okay, summarizing, here's some pseudo code:

// ... Awesome debug loop goes here ...
switch(Info->ExceptionRecord.ExceptionCode)
{
case EXCEPTION_SINGLE_STEP:
{
GetContext(CONTEXT_DEBUG_REGISTERS);
cDR6 DR6(Context.Dr6);
cDR7 DR7(Context.Dr7);
Context.Dr6 = DR6.Clear();
SetContext();

// Singlestep after last breakpoint(s)
if(DR6.SingleStep)
{
if(CurBreaks.size()) // Re-enable breakpoints
{
do{
CurBreaks.top()->Enable();
CurBreaks.pop();
}
while(CurBreaks.size());
Handled = true;
}
}
// DRX breakpoints
for(int i = 0; i < 4; i++)
{
if(DR6.DRX[i] && DR7.DRX[i].Enabled)
{
// You might want to check if this is a breakpoint you actually set
CurBreak = cHardwareBreakpoints::Find(i);

// ... Do whatever notification here ...

if(DR7.DRX[i].Type == BpHwOnExec)
{
//SetResumeFlag(true); // Doesn't work in VMWare
CurBreak->Disable();
CurBreaks.push(CurBreak);
SetSingleStep(true);
}
Handled = true;
}
}
}
break;
}

If you don't mind breaking VMWare compatibility, you can remove the whole

if(DR6.SingleStep)

block, uncomment

SetResumeFlag(true);

and remove anything else inside that block.

What else?

A few things worth noting:

To enable a hardware breakpoint process-wide, you have to set the debug registers in every spawned thread.

If you temproarily disable the breakpoint, you only have to disable it in the current thread. Other threads still have their breakpoints enabled and break. This is different from, say, INT3 breakpoints which are enabled or disabled in the whole process (they require changed code after all).

I guess that's it for the moment. I didn't want to blow this up by showing how to enable breakpoints in DR7 or parsing DR6. I might post some source for doing that if I get round to cleaning up the code. For the time being, the Intel manual is your best friend :)