At work I am working on a semi-basic (breakpoints, stepping, watch, query for symbols, handling optimized code etc.) debugger for a new chip, since this is the first time that I actually worked on a debugger, I’m reading through some resources.
The series of posts will be documenting some aspects of writing a debugger and personal understandings of them. Some of the information here will be pretty basic, but none the less included for completeness. This series will be posted *after* I have actually written the debugger, to ensure correctness of the posts.
I guess this post does assume that you know at least how to debug programs….
So the first in the series if going to be the information from the book How Debuggers Work by Johnathan Rosenberg. The book is dated (1996, talks about Visual Basic 3.0….) and the reviews arent exactly high, but none the less its a start.
Symbolic (source-level) vs Machine level debugging
- Machine level debugging inspects and steps through the machine instructions actually being run on a system
- Symbolic (source-level) debugging maps the instructions back to the source code and provides more intuitive use cases
Application vs In-circuit
- Application debuggers are high level debuggers
- In circuit debuggers are sits between OS and bare hardware and monitor all interactions between OS and hardware
Various views offered by a typical debugger
Current debuggers more or less shows the information listed in the bullet points below, sometimes under different names.
- Source view: pretty basic, shows the source code
- Stack view (Stack trace): a stack trace made up of a number of stack frames (each one a function call).
- Breakpoint view: shows the breakpoints currently set
- CPU view: machine level view showing the actual instruction (disassembly) being executed as well as the current state of hardware registers
- Disassembly are machine instructions (assembly) translated back into textual assembly language representation (so they are human readable)
- Variable view: At the symbolic level, variables can be listed
- Inspector and Evaluator: variables can be inspected for value, additional code can be evaluated at the breakpoint
Debugger, Operating Systems
The debugger is a collection of these views and abilities above into a package.
For debugging applications on an operating system, there needs to be API support from the OS that allows the debugger to attach to an application, have R/W access to the process’s memory and have control over the execution of instructions in the process. The OS APIs also provide ways to notify the debugger when anything important happens (for example segfault).
The debugger is implemented with a main loop that basically waits for user commands to launch one of the above views.
Hardware requirements for debugging
At minimum, hardware must provide the following:
- A way to specify breakpoints (bp). These are specific locations in the executing code that when the processor reaches them, the execution will stop
- Notification (interrupt or trap) that would notify the OS (thus debugger) that an event has occurred (such as hitting a bp)
- The ability to R/W hardware registers
These requirements allow for the following:
- Break point
- Single stepping
- Fault detection
- Watch point
Break point
Usually implemented by  the processor as a special instruction that causes trap to the OS, which in turn notifies the debugger.
The debugger has access to application’s text space (source code), so when a bp is placed, it knows what location that instruction is. It saves the original instruction, and places the bp instruction in. Once the bp is hit, the hardware produces a trap which then tells the debugger where the trap was (which instruction) and why. From there, it is up to the debugger to proceed.
To continue after the breakpoint, the debugger must first restore the original instruction into the bp location, single step that, and put the bp back into the location before letting application continue. The last step is to ensure that the next time execution gets to the bp it still traps.
Single Step
This is where the processor is instructed to execute a single machine instruction and trap to OS again. Some hardware provides flags for this operation. In case this is not provided in hardware, it can be emulated by the debugger by break pointing the next instruction (read the instruction at the current program counter, save next instruction down and set bp at the next). Careful that the next instruction (or the current) may be a jump which requires solving what the correct “next” instruction is.
Fault Detection
Some faults are detected by hardware (such as division by 0, memory access violation etc.) while others are detected by the OS. In all cases, the debugger is notified before the application being debugged is allowed to run again.
Watch Point
Watch points (data breakpoint) are triggers for the debugger when some parts of the debugged application’s address is modified. There are a few ways for the hardware to implement this:
- Have a set of registers that specifies the starting address and the runlength. Any write to any of the addresses will cause a trap
- Mark specific memories as read only, which causes a trap to be generated on write
As a summary point, different processor architectures implement the above differently, and some have implications on debugger design. For example, MIPS have delay slots after branch instructions that can be filled by instructions that were supposed to run before a branch (since a branch takes a while to be fetch from memory, so if we do not want to stall the pipeline, then we can do a few more instruction while waiting for the branch). This means that a debugger seeing a branch must also see if the delay slots after it has been filled, and execute those before branching.