Jon Chappell is product manager at Computer Innovations, Inc., and has been developing language compiler and related software for microcomputers for the past eight years. You can reach him at Computer Innovations, Inc., (908) 542-5920.
Abstract
Although debugging tools have grown increasingly sophisticated over the years, some debugging tasks have become so complex that they require hardware support. Intel provides one solution in its 80386/486 processors hardware breakpointing. Hardware breakpointing allows the programmer to plant debugging traps in a program in such a way that the program being debugged can be run at nearly full-speed. Hardware breakpoints can benefit programmers if they have the tools needed to use the chip's built-in debugging registers.
Breakpointing
In this article, breakpointing refers to the process of stopping the program being debugged. Debuggers must be able to perform this fundamental operation, so that the user is given control over program exectution. (Breakpointing is also referred to watchpointing or tracepointing.)
Traditional Debugging
Traditional debugging refers to software debugging without the use of additional hardware. Several problems that traditional debugging facilities do not address well include breakpoints in ROM locations, multitask debugging and breakpointing on memory reference.Setting code breakpoints in ROM is impossible using a software-only debugger. The usual mechanism replaces the first part of the instruction at a given address with an interrupt instruction, which passes control to the debugger. Since the CPU cannot write into a ROM, a trap cannot be placed and the code cannot be breakpointed. However, using the 80386/486 debug registers to set a breakpoint in ROM solves this problem.
In recent years, the implementation of multiple task systems has increased. Frequently, multiple tasks share common resources, including RAM memory, disk space and hardware ports. When more than one task share the same memory, debugging the resultant system can become very difficult. For instance, suppose you used a memory buffer to buffer a hardware device. The memory buffer has two different programs running that are allowed to write into it, according to a protocol that you have devised. However, one of those programs is not following the protocol correctly and is overwriting memory in the buffer. The system is not running correctly. Which program should you debug? The Intel 80386 debugging mechanism lets you stop on task switch, so you'll have a chance to find the problem.
Breakpoint On Condition
When debugging a program, a programmer may have difficulty knowing just where a given piece of memory is getting corrupted. He may have single-stepped his heart out, trying to locate the offending lines of code. One solution to invalid memory access is "breakpoint on condition". In particular, it would be nice to have the debugger stop the program whenever that particular piece of memory is read or written.
Software Breakpointing Mechanism
Stopping on reference to a given variable is available in many debuggers: sdb under UNIX allows it, and several DOS-based debuggers allow it. Some debuggers go a little further in letting you say: "Stop when variable!=0". This feature isn't worth much unless the programmer already knows where the problem exists. Without hardware assistance, the debugger is forced to single-step the program, checking each instruction for reference to the particular memory location. The program must run one-instruction-at-a-time, with control being transferred to and from the debugger for each instruction. Depending on the amount of code you run, this process requires too much time.
Hardware Breakpointing Mechanism
Intel solved the speed problem by placing debug registers right on the chip and providing instructions to access them. Since there are four address registers, you can monitor up to four memory locations simultaneously. In addition, there is one control and one status register. You can use these registers without adversely affecting the resulting program's speed. (In fact, I ran the dhrystone benchmark under UNIX on a 386, with and without the debug register set, and found no significant difference in the timings.)
Debug Control Register
The debug control register contains groups of bits that correspond to the four debug registers. For each address register, the control register has bits that control local/global exception, read/write control and length.Local/Global Exception: The local context is used for single-task breakpoints. It causes breakpoints to be generated only inside the task being debugged. However, the global context bits can be set for multiple task debugging.
Read/Write Control: Three condition states are defined: instruction execution (for code breakpointing), read/write (for usual access to a given data address), and write only (for stopping only when a value is written to the address). The distinction between read/write and write only is convenient, as it allows the programmer to skip all the reads of a particular piece of memory and concentrate on portions of code that modify the value.
Length: Only one-, two-, and four-byte lengths may be used. For larger data structures, use of more than one debug register is needed.
Debug Status Register
With this list of controls, determining which condition triggered a debug exception can be a problem. You can consult the debug status register for this and other information.
The UNIX Debugging Interface ptrace (2)
ptrace, the UNIX debugging interface, can be used by one process to control the execution of another and to read and write the memory space of that process. ptrace () is necessary for doing anything to the debugged task (unlike under DOS, where you could just set up a far pointer and read/write another program's memory space at will). Under UNIX, you must have permission to access another program's memory and registers. Table 1 lists available ptrace() requests.The debugger issues all the requests in Table 1, except for request 0, which the process being debugged issues to initiate debugging. The debugger can only control the process being debugged directly via calls 7, 8, and 9, which correspond to debugger commands like Go, Single Step, and Kill process. Reading and writing of the process's instruction space can be used for machine disassembling and for setting instruction breakpoints. Reading and writing of the process's data space is useful for displaying the contents of variables and allowing the user to modify them. Reading and writing of the process's user space is required for accessing many other important pieces of information about the debugged process, including the CPU registers and hardware debugging registers.
More information is available in the ptrace(2) section of the UNIX System V Programmer's Reference Manual.
Implementation
The code in Listing 1 is a simplified debugging example. The code will help you understand how to use the hardware breakpointing mechanism inside a debugger program which consists here of a simple C++ debugger class, with several methods for accomplishing low-level debugging. This is indeed a very small part of the code necessary to implement your own commercial-grade debugger.
Where To Go From Here
If you are interested in using the hardware debugging features of the 80386/486, the Intel 80386 and 80486 Programmer's Reference Manual contains complete specification of the debugging facilities on board those chips. The UNIX Programmer's Reference Manual (for the 80386) contains more information on the UNIX interface for debugging.Alternatively, you may want to use a debugger that takes advantage of this hardware. Computer Innovations is shipping such a debugger, called Debug-2000TM, for the UNIX/386 environment.
References
Intel 80386 Programmer's Reference Manual, 1986, Intel Corp.Intel 80486 Programmer's Reference Manual, 1990, Intel Corp.
UNIX Programmer's Reference Manual, 1989, AT&T, Prentice Hall.