Software Breakpoints

Line breakpoints can be implemented either in hardware or software.  This article discusses the latter in detail.

It is very useful to be able to break execution of code at a line number of your choice.  Breakpoints are provided in debuggers to do exactly that.  It is fun getting to the root of the problem by setting breakpoints in a debug session.  It is even more fun to know how do breakpoints work in the first place.

Software breakpoints work by  inserting a special instruction in the program being debugged.  This special instruction on the Intel platform is “int 3”.  When executed it calls the debugger’s exception handler.

Example

Let us look at a very simple example that inserts a breakpoint in a program at compile time and not through a debugger.  The code uses the Intel instruction “int 3” and you may need to figure out the equivalent instruction for a non-Intel platform.

// The code below works well with Visual Studio.
int main()
{
__asm int 3;
printf("Hello World\n");
return 0;
}

// The code below works well with gcc + gdb
int main()
{
asm("int $3");
printf("Hello World\n");
return 0;
}


If you run this program in Visual Studio, you get a dialog saying “helloworld.exe has triggered a breakpoint“.

In gdb you get the message “Program received signal SIGTRAP, Trace/breakpoint trap.

In the example above, a call to “int 3” invokes the debugger’s exception handler.

It is also interesting to note the assembly instructions generated for the program above.

In Visual Studio, right click on the code and click on “Show Disassembly”. Also ensure that “Show Code Bytes” is on in the same context menu.

Visual Studio 2008 Disassembly

In gdb type disassemble at the gdb command.

disassemble

0x0040107a <main+42>:   int3

Now obtain the opcode of the int3 instruction using the x (examine memory) command

(gdb) x/x main+42

0x40107a <main+42>:     0xcc

As seen above, the breakpoint opcode we inserted during compilation is 0xCC .

How Do Debuggers Insert Breakpoints?

For practical reasons, it is unwise to ask for a recompilation whenever a breakpoint is added or deleted.  Debuggers change the loaded image of the executable in memory and insert the “int 3” instruction at runtime.  The common steps a debugger performs to provide the functionality of a line breakpoint to a user are as follows –

  1. When a user inserts a breakpoint in a line of code, the debugger saves the opcode at that given location and replaces it with 0xCC (int 3).
  2. When the program is run and it executes the “int 3” instruction, control is passed to the debugger’s exception handler.
  3. The debugger notifies the user that a breakpoint has been hit. Say that the user instructs the debugger to resume execution of the program.
  4. The debugger replaces the opcode 0xCC with the one it had saved earlier.  This is done to restore the instructions to their original state.
  5. The debugger then single steps the program.
  6. It then resaves the original instruction and re-inserts the opcode 0xCC.  If this step were not done, the breakpoint would have been lost.  Temporary breakpoints on the other hand skip this step.
  7. The debugger then resumes execution of the program.

Hardware breakpoints are limited in number but debuggers are able to provide unlimited breakpoints by implementing them through software.

Knowing what goes behind the scenes makes debugging a bit easier.  A debugger may defer setting a breakpoint if the module is not loaded in memory yet.  It needs to replace some opcode with 0xCC and that can happen only when the module is in memory.  Likewise, a mismatch between a binary, its sources and its debug symbols (or the lack of it) may cause breakpoints to be hit at unexpected locations because the debugger is not able to correctly map the source line to the opcode that it needs to replace with 0xCC.  At times debuggers complain about the mismatch and refuse to set the breakpoints.

Many of the setup issues with breakpoints become obvious once we know how they work internally.  And when all else fails and release build breakpoints  adamantly refuse to work, you always have the option of compiling an “int 3” breakpoint right into your code.

9 Replies to “Software Breakpoints”

  1. @Rohit,
    I am assuming you are asking this for the Windows platform.
    Inline assembly (__asm) is not allowed in Visual Studio’s x64 bit compilers. You can either create your own asm file with a function that invokes “int 3” and call that function from your C/C++ code or simply invoke the ::DebugBreak() Windows API that internally invokes “int 3”.

  2. For an alternative approach on a unix platform, assuming gdb, one can use raise(SIGTRAP). In fact, using a signal gives you a way to only stop execution if you are within gdb:

    struct sigaction oldAct;
    struct sigaction newAct;

    newAct.sa_handler = SIG_IGN;
    sigaction(SIGTRAP, &newAct, &oldAct);
    raise(SIGTRAP);
    sigaction(SIGTRAP, &oldAct, NULL);

    Wrap that up in a function which takes a bool, and the process will send itself a signal that it will ignore. Crucially, gdb will *not* ignore it, and will take a break.

  3. At the run time, we are attaching a process to the GDB and at time time we are setting a break point (in a specified file and a specfic line number), how the GDB know the exact opcode in the ELF file.is there any mapping application is there inside GDB

  4. In case one is writing a debugger,
    our option would be to replace the instruction at an addr ‘x’ with the opcode ‘0xcc’.
    At the same time we should be having a debug exception handler in place.
    so with the above opcode should we be replacing a ‘call our_debug_handler’ instruction.
    Our debug handler let says shows some values and then makes sure that the replaced instructions are brought back into the memory for resumed execution.
    Am I right?

  5. I guess the above thing I mentioned wont work, because, cc would call some routine described by segment descriptor in IDT for int3.
    Will it be possible to modify the segment selector and offset for int3 segment and point it to our code.

    1. User level debugger requires permissions from the OS to be able to debug a child program (Windows debugging APIs / ptrace on Unix). Whenever an int 3 instruction is executed, the control is passed to the operating system’s interrupt vector. The OS then notifies the debugger that the child it was monitoring has hit a breakpoint. WaitForDebugEvent (Win) and waitpid (on Unix) calls are some ways how the OS notifies the debugger. The OS acts as the authority in order to grant debugging permissions and also routes the notifications back to the debugger.

Leave a Reply

Your email address will not be published. Required fields are marked *