Click here to Skip to main content
Click here to Skip to main content

Write Your Own Debugger to Handle Breakpoints

, 17 May 2011
Rate this:
Please Sign up or sign in to vote.
Basic debugger, Breakpoints, OutputDebugString

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}; 		//0xcc=int 3            
::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;  		//declare some variable
_asm {
    call f       		//this will push the current eip value on to stack
    jmp finish
    f: pop eax   		//get the last value from the stack, in our case value of eip
    mov EIP,eax  		//store the value of eip in some memory 
    push eax    		//restore the stack
    ret         		//return
    finish:
}

// print the memory dump 
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); //will wait till a debug event is triggered
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);	//mentioned earlier to read memory 
						//from another process.
        printf("output from debug string is: %s",a);
    }
    break;
}

ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE); // After the debug event
						// is handled, Debugger must call

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

No Biography provided

Comments and Discussions

 
GeneralMy vote of 1 PinmemberMember 891132816-Jan-13 5:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140721.1 | Last Updated 17 May 2011
Article Copyright 2011 by Asif Bahrainwala
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid