Data Breakpoints

The Visual Studio debugger supports a kind of breakpoint called Data Breakpoint, sometimes it is also called watchpoint. Data breakpoint is architecture dependant, as it requires hardware support provided by CPU. For x86, this will be the DR (Debug Register).

The following code demonstrates how to use the x86 debug register by implementing a very simple native debugger.


 #define WIN32_LEAN_AND_MEAN

#include <Windows.h>
#include <stdio.h>

HMODULE    g_hModule;                            /* linear address of exe   */
LPVOID     g_pOEP;                               /* original entry point    */
BYTE       g_bBreakPoint;
BYTE       g_bINT3 = 0xcc;                       /* debug break instruction */
BOOL       g_fDebugRegisterSupported = FALSE;    /* hardware DR supported   */

int WINAPI ExeEntry()
{
  if(IsDebuggerPresent())
  {
    g_hModule = GetModuleHandle(NULL); /* HMODULE is always 4 bytes aligned */
    printf("HMODULE: %p\n", g_hModule /* insert a space here... */,
           *(INT32*)(g_hModule) /* read 4 bytes */);
  }
  else
  {
    CONTEXT context;
    context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
    PROCESS_INFORMATION processInformation;
    STARTUPINFO startupInfo = {sizeof(STARTUPINFO)};
    if(CreateProcess(NULL, GetCommandLine(), NULL, NULL, FALSE, DEBUG_PROCESS,
                     NULL, NULL, &startupInfo, &processInformation))
    {
      DWORD dwContinueDebugStatus = DBG_CONTINUE;
      while(dwContinueDebugStatus)
      {
        DEBUG_EVENT debugEvent;
        WaitForDebugEvent(&debugEvent, INFINITE);
        switch(debugEvent.dwDebugEventCode)
        {
        case CREATE_PROCESS_DEBUG_EVENT:
          g_pOEP = (LPVOID)(debugEvent.u.CreateProcessInfo.lpStartAddress);
          g_hModule = (HMODULE)(debugEvent.u.CreateProcessInfo.lpBaseOfImage);
          CloseHandle(debugEvent.u.CreateProcessInfo.hFile);
          printf("CREATE_PROCESS_DEBUG_EVENT @%p OEP=%p\n", g_hModule, g_pOEP);
          break;
        case EXCEPTION_DEBUG_EVENT:
          printf("EXCEPTION_DEBUG_EVENT PID=%d TID=%d @%p\n",
                 debugEvent.dwProcessId, debugEvent.dwThreadId,
                 debugEvent.u.Exception.ExceptionRecord.ExceptionAddress);
          GetThreadContext(processInformation.hThread, &context);
          switch(debugEvent.u.Exception.ExceptionRecord.ExceptionCode)
          {
          case EXCEPTION_BREAKPOINT:
            if(debugEvent.u.Exception.dwFirstChance)
            {
              if(debugEvent.u.Exception.ExceptionRecord.ExceptionAddress == g_pOEP)
              {
                LPVOID IP = (LPVOID)(--context.Eip);
                WriteProcessMemory(processInformation.hProcess, IP,
                                   &g_bBreakPoint, 1, NULL);
                FlushInstructionCache(processInformation.hProcess, IP, 1);
                context.Dr0 = (DWORD)(g_hModule);
                context.Dr7 = 0x000f0101;
              }
              else
              {
                printf("\tbp $exentry\n");
                ReadProcessMemory(processInformation.hProcess, g_pOEP,
                                  &g_bBreakPoint, 1, NULL);
                WriteProcessMemory(processInformation.hProcess, g_pOEP,
                                  &g_bINT3, 1, NULL);
                FlushInstructionCache(processInformation.hProcess, g_pOEP, 1);
              }
            }
            break;
          case EXCEPTION_SINGLE_STEP:
            printf("EXCEPTION_SINGLE_STEP DR6=%08X\n", context.Dr6);
            g_fDebugRegisterSupported = TRUE;
            break;
          }
          context.Dr6 = 0;
          SetThreadContext(processInformation.hThread, &context);
          break;
        case EXIT_PROCESS_DEBUG_EVENT:
          dwContinueDebugStatus = 0;
          printf("EXIT_PROCESS_DEBUG_EVENT\n");
          break;
        case LOAD_DLL_DEBUG_EVENT:
          CloseHandle(debugEvent.u.LoadDll.hFile);
          break;
        }
        ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId,
                           dwContinueDebugStatus);
      }
      CloseHandle(processInformation.hThread);
      CloseHandle(processInformation.hProcess);
    }
    printf("Debug Register Test: %s\n", g_fDebugRegisterSupported ?
           "Hardware DR supported" : "Hardware DR not supported");
  }
  return ERROR_SUCCESS;
}

To compile the source code, you may use Visual Studio or either of the following compilers (x86 32bit):

cl.exe watchpoint.cpp kernel32.lib msvcrt.lib /GS- /link /ENTRY:ExeEntry /NODEFAULTLIB /SUBSYSTEM:CONSOLE

gcc.exe -fno-exceptions -fno-rtti -s -Os -o watchpoint watchpoint.cpp -Wl,--stack,65536

A few things to mention:

  1. Data breakpoints respect the CPU working mode, which means the linear address is used if paging enabled, and physical address used if paging disabled.

  2. Each CPU core has its own set of debug registers. This wouldn't be a problem for user mode as the operating system maintains context switching, but it will be very different if you are implementing kernel mode driver which runs above dispatch level, or custom interrupt vector.

  3. Virtual machine sometimes does not implement DR, this is true for Virtual PC and VMware if you don't have hardware virtualization enabled.

  4. To make the sample easier, our trivial debugger assumes we only have one debugee and the debugee is single-threaded. For the real debugger, PID and TID should be used to get the correct handle (and should have it cached) in order to support multi-threaded debugee, as well as debugging multiple programs in a single debugger instance.

  5. A little modification is required in order to support 64bit:

                   if(debugEvent.u.Exception.ExceptionRecord.ExceptionAddress == g_pOEP)
                  {
    #if defined(_M_IX86)
                    LPVOID IP = (LPVOID)(--context.Eip);
    #elif defined(_M_X64)
                    LPVOID IP = (LPVOID)(--context.Rip);
    #endif
                    WriteProcessMemory(processInformation.hProcess, IP,
                                       &g_bBreakPoint, 1, NULL);
                    FlushInstructionCache(processInformation.hProcess, IP, 1);
    #if defined(_M_IX86)
                    context.Dr0 = (DWORD)(g_hModule);
    #elif defined(_M_X64)
                    context.Dr0 = (DWORD64)(g_hModule);
    #endif
                    context.Dr7 = 0x000f0101;
                  }
    
  6. ExeEntry is used as the entry point instead of the one provided by the C Runtime, which is not a good practice, as this might cause subtle CRT initialization problem. The main reason of doing this is that CRT initialization code reads from the module header just as we did, which would also trigger our watchpoint. To verify this:

    1. Change the following code by inserting a blank space:

           printf("HMODULE: %p\n", g_hModule /* insert a space here... */,
                 *(INT32*)(g_hModule) /* read 4 bytes */);
      
    2. After the change, your code would look like:

           printf("HMODULE: %p\n", g_hModule /* insert a space here... * /,
                 *(INT32*)(g_hModule) /* read 4 bytes */);
      
    3. Now change ExeEntry to int __cdecl main() , and recompile without the /ENTRY:ExeEntry flag.

    4. Run and see what happened, on my machine, this would look like:

       CREATE_PROCESS_DEBUG_EVENT @00250000 OEP=00251978
      EXCEPTION_DEBUG_EVENT PID=1560 TID=4852 @77DD04F6
               bp $exentry EXCEPTION_DEBUG_EVENT PID=1560 TID=4852 @00251978
      EXCEPTION_DEBUG_EVENT PID=1560 TID=4852 @002518CA
      EXCEPTION_SINGLE_STEP DR6=FFFF0FF1
      EXCEPTION_DEBUG_EVENT PID=1560 TID=4852 @0025104F
      EXCEPTION_SINGLE_STEP DR6=FFFF0FF1 HMODULE: 00250000
      EXCEPTION_DEBUG_EVENT PID=1560 TID=4852 @67D278FC
      EXCEPTION_SINGLE_STEP DR6=FFFF0FF1
      EXIT_PROCESS_DEBUG_EVENT
      Debug Register Test: Hardware DR supported
      

Homework:

  1. What if multiple watchpoints (either Read, Write or Execute, for 1, 2 or 4 bytes) were added to a single address?
  2. What if the target address is not aligned?
  3. What if the data access triggers a GPF (General Protection Fault)?
  4. What would happen if a watchpoint was applied on an INT3?
  5. Why are we setting the watchpoint when the debugee reaches OEP, instead of setting it at the first DebugBreak?
  6. You might have noticed that we decreased EIP by 1 when we received the EXCEPTION_BREAKPOINT debug event, why? What would happen if the interrupt was caused by opcode CD 03 instead of CC?

For the homework, you would want to either check the Intel x86 specification and do verification by playing around the code.