Tuesday, January 1, 2013

GDB and Breakpoints on x86

This is a short post summarizing my findings on how some key features of gdb work on the x86 platform. Initially I was under the impression that gdb relies exclusively on the hardware support provided by x86 processors, but it turned out that was not the case.

x86 debug registers

First some background on the x86 debug support. There are 8 debug registers available on the x86 architecture, named DR0 through DR7. The function of these registers is as follows:
  • DR0 - DR3: Hold upto 4 addresses that can be watched
  • DR4, DR5: Obsolete synonyms for DR6 and DR7
  • DR6: Status register
  • DR7: Control register
The registers DR0, DR1, DR2 and DR3 can be used to implement hardware assisted breakpoints. These are 64-bit registers and can be accessed using the regular MOV instructions. Upto 4 addresses can be loaded in these registers. The processor compares the contents of the address bus with each of these debug registers for every memory access. Note that this comparison occurs before any virtual address translation, and so the debug registers should contain virtual, i.e. untranslated addresses. The CPU may be performing any of these operations on the address:
  1. Fetching an instruction from the address
  2. Reading a piece of data from the address
  3. Writing a piece of data to the address
Control bits in the DR7 register determine which type of memory access needs to be watched. If there is an address match, and the memory access corresponds to one that needs to be watched as per DR7, then a debug exception is generated which eventually transfers control to the debugger. This feature can be used to implement watchpoints that are invaluable in hunting down memory corruption issues.

To implement user-defined breakpoints to stop at certain designated locations in a program, the processor should compare the contents of the eip register (the program counter) with each of the debug registers DR0 - DR3, and generate a debug exception if there is a match. The debugger can determine which debug condition has occurred (for e.g. which of the four addresses was matched) by examining the DR6 status register.

So much for the x86 debug registers. So why doesn't gdb normally use these registers for implementing user-defined breakpoints? One problem is that the debug registers can be accessed only in ring 0 in protected mode. In other words, they can be accessed only in kernel mode but not in user mode. What then is the alternative? Enter ptrace.

Ptrace

Ptrace is a very elegant and versatile system call provided by the Linux kernel that can be used to examine and control other processes. Debuggers such as gdb and system call tracing tools such as strace rely heavily on this system call to debug and monitor other programs.

#include <sys/ptrace.h>

long ptrace(enum __ptrace_request request, pid_t pid,
                   void *addr, void *data);

The first parameter to ptrace indicates the operation requested. Ptrace supports a number of monitoring and control operations, such as attaching and detaching from processes, examining and modifying data at specified addresses in the target process, examining and modifying the target process registers, stopping, resuming and killing the target process, and so on. The pid argument specifies the process id of the target process. The last two parameters, addr and data have significance specific to the requested operation.

Gdb can either start and control a program from the outset, or attach to an already running process. The main event notification mechanism in play is the wait system call. The debugger starts or attaches to the process and sits in a wait call. Whenever a signal is sent to the target process, the process is stopped and the wait returns to the debugger which may then examine and/or modify the process state. The debugger may then either continue the process or kill it. One exception to this event notification rule is the SIGKILL signal which kills the target process rather than simply stopping it.

Software assisted breakpoints

When the user requests a breakpoint at a specified address, gdb needs a way to gain control just before the target process executes the instruction at that address. The usual way to accomplish this is to leverage the x86 int 3 instruction. The int instruction raises a software interrupt to the processor which immediately transfers control to the registered interrupt handler in the interrupt descriptor table (IDT).

Why int 3 and not any other interrupt number? Intel has defined a special one byte opcode for the int 3 instruction (0xcc). Thus gdb can simply replace the byte at the breakpoint address with this opcode (using ptrace of course) and remember the original byte in the program. When control reaches that address in the program, the software interrupt gets generated as intended, the process is stopped and the debugger gets control. When the user has finished examining the process state and gives the command to continue the program, gdb substitutes the int 3 instruction with the original byte. Also the program counter eip register should be adjusted back by one byte so that the processor can continue with the original instruction at that address (remember the eip register always has the address of the next instruction to be executed). This register adjustment is again achieved using ptrace.

How exactly does gdb get control when the int 3 instruction is executed in the target process? Let us look at the Linux 2.6.29 kernel to understand this better. Looking at the entry_32.S file in the linux/arch/x86/kernel directory, we can find the handler for the int 3 software interrupt:

ENTRY(int3)
        RING0_INT_FRAME
        pushl $-1                       # mark this as an int
        CFI_ADJUST_CFA_OFFSET 4
        SAVE_ALL
        TRACE_IRQS_OFF
        xorl %edx,%edx          # zero error code
        movl %esp,%eax          # pt_regs pointer
        call do_int3
        jmp ret_from_exception
        CFI_ENDPROC
END(int3) 

We can see that the function do_int3 will get invoked whenever the int 3 software interrupt is raised. This function calls do_trap as follows:

do_trap(3, SIGTRAP, "int3", regs, error_code, NULL);

The do_trap function generates a SIGTRAP signal and delivers it to the target process. As mentioned earlier, whenever a signal is delivered to the target process being traced, the process is stopped and the monitoring program, i.e. gdb gets control via a return from wait. gdb can then use ptrace and figure out that the SIGTRAP signal was delivered to the process and then notify the user that a breakpoint was hit.