Introduction
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.
Background
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.
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
int 3
). - 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.
OutputDebugString
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
EnterDebugLoop
). - The event handling API will provide information of the
string
relative to the Debuggee's process space. - Use
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:
BYTE p[]={0xcc}; ::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):
UINT EIP=0; _asm {
call f jmp finish
f: pop eax mov EIP,eax push eax ret finish:
}
BYTE *b=(BYTE*)EIP;
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).
WaitForDebugEvent(&de, INFINITE); switch (de.dwDebugEventCode)
{
case EXCEPTION_DEBUG_EVENT:
switch(de.u.Exception.ExceptionRecord.ExceptionCode)
{
case EXCEPTION_BREAKPOINT:
MessageBoxA(0,"Found break point","",0);
break;
}
break;
case OUTPUT_DEBUG_STRING_EVENT:
{
char a[100];
ReadProcessMemory(pi.hProcess,de.u.DebugString.lpDebugStringData,
a,de.u.DebugString.nDebugStringLength,NULL); printf("output from debug string is: %s",a);
}
break;
}
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.