Building your very own debugger is a great way to understand the workings of a commercially available debugger. In this article, the reader will be exposed to certain aspects of the OS and CPU opcode (x86-32-bit only). This article will show the working of breakpoints and working of
OutputDebugString (since we will be handling these two events only) used commonly while debugging. Readers are urged to investigate conditional breakpoint and step wise execution (line by line) that are commonly supported by most debuggers. Run to cursor is similar to breakpoint.
Before we start, the reader will require basic knowledge of OS. Discussion related to OS is beyond the scope of this article. Please feel free to refer to other articles (or write to me) while reading this. The reader would be required to be exposed to commercially available debuggers (for this article: VS2010) and have debugged applications before using break points.
Breakpoint allows users to place a break in the flow of a program being debugged. The user may do this to evaluate certain conditions at that point in execution.
The debugger adds an instruction:
int 3 (opcode : 0xcc) at the particular address (where break point is desired) in the process space of the executable being debugged. After this instruction is encountered:
- The EIP is moved to the interrupt service routine (in this case
- The service routine will save the CPU registers (all Interrupt service routines must do this), signal the attached debugger, the program that called
DebugActiveProcess(process ID of the exe being debugged) look up MSDN for this API.
- The debugger will run a debug loop (mentioned in code as
EnterDebugLoop() in file Debugger.cpp). The signal from the service routine will trigger
WaitForDebugEvent(&de, INFINITE), the debug loop (mentioned in the code as
EnterDebugLoop) will loop through every debug signal encountered by
WaitForDebugEvent. After processing the debug routine, the debugger will restore the instruction by replacing
0xcc (int 3) with the original instruction and return from the service routine by calling
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE). (Before placing the break point, the debugger must use
ReadProcessMemory to get the original BYTE at that memory location).
- When it returns from an interrupt service routine (using
IRET), EIP will point to the next byte to be executed, but we want it to point to the previous byte (the one we restored), this is done while handling the break point. Although a break point service routine is being processed (its EIP is pointing somewhere in a service routine),
GetThreadContext will return the value of registers before EIP moves to
int 3 service routine. Subtract EIP by 1, use
SetThreadContext to set the EIP.
This API is used to display a
string on the debug console, the user may use this to display certain state related information or trace.
- When this API is occurs,
OUTPUT_DEBUG_STRING_EVENT event is triggered.
- An attached debugger will handle this event in the debug loop (mentioned in the code as
- The event handling API will provide information of the
string relative to the Debuggee's process space.
ReadProcessMemory to acquire the
string (memory dump) from another process.
Using the Code
The attached code must be referred to at all times while reading this article. The break point (opcode: 0xcc) is introduced by:
::WriteProcessMemory(pi.hProcess,(void*)address_to_set_breakpoint, p, sizeof(p), &d);
The second parameter which requires the address where the break point instruction is placed is looked up in a .PDB file (debug symbol file).
Through the .PDB file, VS2010 can accurately place the break point at a memory location corresponding to the line of code responsible for generating instructions at that memory location.
The above method is commented out, the reason being that I cannot accurately place the break point since I am not using any debug symbols, instead I use
::DebugBreak(); to cause a break point in the process being debugged, refer to the code.
Readers are encouraged to try using
WriteProcessMemory API instead, I cannot use it for this article as the value of the address :2nd parameter in
WriteProcessMemory is not known unless you compile the code (and hope that the OS will allocate the same value for EIP).
Break Point Created by VS2010
To readers who have to debug (any) application using VS2010 - if the break point is placed in code (its executable is created with debug setting) using VS2010 IDE (by pressing F9), the memory debug view will not show 0xcc. Reader will have to dump the memory at the point where the break point is created, of course the address location will have to be looked up through the disassembly (since you are currently debugging the application, you could press ALT-8).
In the attached code, I have used the value of EIP, we get the value of EIP from the following code (code comments make this self explanatory):
f: pop eax
for(int i=0; i<200; i++) printf("%x : %x \n",EIP+i,b[i]);
The main loop (used by the debugger) refers to
void EnterDebugLoop() in file Debugger.cpp.
WaitForDebugEvent API is used to handle debug events for any process attached to the callers process using
DebugActiveProcess (debuggee's process ID).
MessageBoxA(0,"Found break point","",0);
printf("output from debug string is: %s",a);
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE);
ContinueDebugEvent will enable the debugger to continue (that thread) that reported the debug event.
Points of Interest
Now we know that it's easy to write your very own debugger / profiling tools. After the basics of writing a simple debugger, readers are encouraged to write more complex debuggers.