Intel's 80386 processor has received a great deal of praise for its improvements over the earlier 80286. Much has been written on the excellent speed and (essentially) flat address space of the 386. In developing software, however, there are other concerns that are just as important as operating speed, even though fast development is often a critical factor. In this article I'll discuss some of the advantages of the 386 processor from the perspective of software debugging.
When Intel designed the 80386, it included a new feature: hardware support for debugging. Probably the most important addition of the new debugging support was the inclusion of breakpoint registers, which allow breakpoints on memory reads, writes, and instruction fetches. Although breakpoint registers are powerful, they do have limitations. By using a few other features of the 80386, our company was able to create a software debugger that contains most of the features found in hardware-assisted debuggers. In addition to breakpoint registers, our product, Soft-ICE, uses several protected mode features, such as virtual 8086 mode, paging, I/O privilege level, and breakpoint registers, to add real-time hardware-level breakpoints and other features found only in hardware-assisted debuggers to existing DOS software debuggers. This article describes how these 80386 features work and how our debugger uses them.
All 80x86 real-address-mode software debuggers cause side effects to the program environment. These side effects are caused because the debuggers use memory, interrupt vectors ([INT 1], [INT 2], and [INT 3]) and DOS or BIOS for I/O. Software debuggers are also at risk of being overwritten or affected in other ways by the target program. For this reason, many hardware-assisted debuggers load the debugging code into write-protected memory on an option card. Even this solution can cause side effects because that memory is mapped into the lower Mbyte that is visible to the 80x86 processor in real-address mode, and this address space (such as C0000H or D0000H) is also used by other adapter cards. By using the 80386 in virtual 8086 mode, though, it is possible to write a debugger that surrounds the DOS environment in a virtual machine without any of the side effects mentioned earlier.
The 80386 provides a virtual 8086 capability intended for use by protected mode operating systems. This feature was necessary because 80386 protected mode is not backward compatible with 8086 executable programs. The 8086 virtual-machine capability is implemented in such a way that a protected mode operating system can allow multiple virtual 8086 tasks that are controlled by the operating system kernel, and so the operating system and other native tasks are isolated from ill-behaved DOS programs. The operating system cannot be overwritten by the DOS program and has complete control over interrupts, I/O, and the memory map.
Several operating systems commercially available today use the virtual 8086 mode of the 80386. These include FlexOS (DRI), PC-MOS/386 (Software Link), VPix (Phoenix Technologies and Interactive Systems), and Windows/386 (Microsoft).
If you think of the model described earlier but replace the operating system with a debugger, you get some interesting benefits. The debugger can control the 8086 environment without affecting it or being affected by it, and the debugger code does not run in the virtual 8086 task and therefore is not visible to DOS or to DOS programs. To implement a debugger based on this model, the debugger must have many features of an operating system, including a complete I/O system--the debugger cannot rely on DOS or BIOS for I/O.
A protected mode debugger has more control over the virtual 8086 task than is possible with a conventional software debugger. Interrupts are controlled because all interrupts go through the protected mode interrupt table to the protected debugger. (It is the responsibility of the protected debugger to generate the interrupt in the 8086 virtual machine if necessary.) I/O is controlled because the protected debugger has control over which IN and OUT instructions are passed through to the hardware and which cause exceptions (80386 exceptions are very sirnilar to 8086-style interrupts). The memory map is completely controlled by the 80386 paging mechanism. Using paging, the amount of memory given to the virtual machine can be varied in 4K increments up to 1 Mbyte. In most instances 640K is the right number. Memory pages can also be marked as "not present," causing an exception if a program running in the 80386 virtual machine accesses that memory page and so giving the debugger control over access of memory regions.
This article describes how protected mode features can be used to provide sophisticated debugger breakpoints. The breakpoints are generally implemented by gaining control of the processor when an exception occurs. At this point a debugger window can be popped up or control can be given to a conventional DOS software debugger. This process is described in detail later.
A useful feature in debugging device drivers is having breakpoints on IN and OUT instructions. IN and OUT instructions execute under control of the protected debugger. The 80386 gives the operating system the ability to trap on accesses to any I/O address, a capability that was included to allow the operating system to "virtualize" the I/O of an ill-behaved DOS program. An example of this would be capturing bytes output to the parallel port in a printer driver application, where the protected mode operating system could send these bytes to a print spooler.
The 80386 allows the protected mode operating system to control the virtual machine's access to I/O ports through a bit mask. The bit mask contains a separate bit for each 8086 I/O port. If the bit is clear, the 80386 lets the IN or OUT instruction execute normally. If the bit is set, the 80386 generates an exception that is handled by the protected mode operating system. I/O addresses range from 0 to 65,535, so a complete bit mask takes 65,536 bits, or 8K of memory. If the protected mode operating system provides a bit mask of less than 8K, then any accesses to I/O ports that are not covered by the bit mask cause an exception.
Our protected debugger takes the place of the protected mode operating system, using the I/O bit mask to provide breakpoints on IN and OUT instructions. To set an I/O breakpoint, the debugger sets the bit that corresponds to the specified I/O address. When the target program running in the virtual 8086 task accesses that I/O address, an 80386 exception occurs. An 80386 exception is similar to a software interrupt. At this point the debugger has control, but it does not know if an IN, INS, OUT, or OUTS instruction caused the exception.
The protected debugger's exception handler can look at the actual instruction that caused the exception to determine the instruction type. More specific breakpoints are possible by comparing the actual value being output with a predefined value. In the case of an input, the instruction can be single-stepped and the value in the AL or AX register compared with a predefined value. If these specific criteria are not met, the debugger can give control back to the virtual 8086 task.
One advantage of using the 80386 virtual-machine features to cause I/O breakpoints is that 80386 exceptions occur instantaneously. This may not seem like a revolutionary statement, but the builders of hardware-assisted debuggers and in-circuit emuiators have been confronted with this problem for years. As microprocessors become more pipelined, it is difficult to cause an instantaneous breakpoint--for example, most hardware-assisted debuggers generate an NMI (nonmaskable interrupt) when the breakpoint conditions are met. Because the 80386 prefetches and predecodes several instructions before the actual target instruction is executed, the 80386 has actually executed several instructions before it recognizes the NMI.
When debugging, it is often useful to set a breakpoint on a hardware or software interrupt. You may, for example, want to run a program until it makes a DOS call to read the version number. You could set a breakpoint for INT 21 with AH = 30. Interrupt breakpoints are a natural for protected debuggers.
In our protected debugger, all interrupts go through the protected mode interrupt table that is under complete control of the debugger. It is the responsibility of the protected debugger to get the address from the 8086 virtual mode task's interrupt vector table at 0:0 and transfer control to that address. With a little additional qualification code that compares the interrupt number with a value previously input by the user, you have breakpoint-on-interrupt capability. This method works equally well with hardware or software interrupts.
Another debugging feature that can be implemented using the virtual machine is the ability to pop your debugger up at any time, even if interrupts are disabled or masked off. Often, when a program is hung, it is useful to pop into your debugger and poke around. Conventionally, this is done with an external button that is linked to an option card that causes an NMI. The conventional method often has problems because so many option cards, including most popular multivideo cards, use NMIs.
A protected mode debugger managing a virtual 8086 environment can actually provide this breakout capability through a key sequence. The specific 80386 feature that is used to provide the breakout capability is called privilege levels. To understand privilege levels you must understand a little bit about 80386 protection. The 80386 has four different protection levels, numbered level 0 (highest privilege) through level 3 (lowest privilege). The four different privilege levels can be used for four layers of differing trust levels.
In our debugger application, we need only two levels: level 0 and level 3. The debugger--as you'd expect--runs at level 0, while the virtual 8086 task runs at level 3. A program running at level 0 has complete access to all 80386 protected features. A level 3 program in contrast, cannot access 80386 control registers and other 80386 features. The ability to access I/O ports can be set (by the operating system or in this case by the protected debugger) at any level.
If the privilege level is set to 3, the target program running in the virtual 8086 task has some additional restrictions. Certain instructions that have to do with interrupts cause exceptions. These instructions are STI, CLI, LOCK, INT, PUSHF, POPF, and IRET. If you are an assembly-language programmer, you may have noticed that most of these instructions affect the processor interrupt flag. If the 80386 had two interrupt flags--one for the virtual machine and one for the native environment--it would not be necessary to monitor these instructions. Because the 80386 does not keep track of the state of the interrupt flag separately for the virtual machine, the protected debugger must monitor all instructions that cause a change in the state of the interrupt flag.
By getting control when any of these instructions are executed by the target program, it is possible to "virtualize" the interrupt system. In the case of the breakout feature, the only concern is to handle the keyboard interrupt. When the target program disables interrupts, the debugger must continue to get keystroke interrupts. The keyboard interrupt must be handled by the debugger but cannot be passed through to the virtual 8086 task. When keyboard interrupts occur, the protected debugger must monitor the keystrokes looking for a key sequence. if the sequence is found, the debugger is popped up. The tricky part is making sure all keyboard activity meant for the target environment is passed through accurately.
For hard-core systems types, it is worth mentioning that accesses to the interrupt controller mask register must be monitored as well. This is necessary in the cases in which interrupts are masked at the interrupt controller instead of at the processor interrupt flag.
The next debugging feature I'll discuss is memory range breakpoints. Memory range breakpoints are especially useful when an errant program is overwriting a portion of memory. By trapping on the write, you can find the actual code that has gone astray.
To implement memory range breakpoints, the protected debugger uses an 80386 protected mode feature called paging. The paging mechanism in the 80386 was intended for providing demand-page virtual memory in a protected mode operating system. Memory is divided into 4K pages, and the operating system can mark each page as present or not present in the 80386 page tables. If a program is executing and it enters a page that is not present, an exception occurs. It is the responsibility of the operating system paging exception handler to load the actual contents of that page from a mass storage device. Paging takes advantage of the fact that most programs tend to spend most of their execution time in a few concentrated areas.
Again, our debugger takes on the role of an operating system to manage the paging mechanism. When the user specifies a memory range, the debugger must first determine which pages the range covers. The debugger marks these pages not present in the 80386 page tables, and control is given back to the user program. If the user program accesses one of the pages marked not present, an exception occurs, and the debugger's exception handler gets control at this point. The 80386 passes the exception handler the address that was accessed in control register 2 (CR2). In most cases the specified memory range does not start exactly on a 4K page boundary. When an exception occurs, the debugger must compare the actual address accessed with the actual range specified. If the access was within the 4K page but not within the specified range, control is given back to the target program.
This boundary condition problem can cause performance of the target program to degrade visibly in some instances, although in most cases the performance hit is negligible. One instance in which the performance drop is noticeable is if the top of the program's stack is in the page but not within the range. Even with this performance hit, range breakpoints using paging are still thousands of times faster than the software simulation technique that some debuggers use.
A refinement of range breakpoints is possible. The 80386 paging mechanism allows pages to be readprotected and write-protected, which gives the debugger the ability to allow memory range breakpoints on read, write, or read/write accesses.
Range breakpoints occur immediately when using the 80386 paging mechanism. The instruction that caused the breakpoint to occur is also restartable, so once the memory is present, the program can continue without missing an instruction. This gives the protected debugger the same advantage over hardware debuggers that it has with I/O breakpoints; breakpoints are not affected by the 80386 instruction pipelining.
Another 80386 feature that fits in with our protected debugger is the previously mentioned breakpoint registers. Breakpoint registers were designed specifically for debugging purposes. They can be used in real-address mode and can be implemented very easily in a simple terminate-and-stay-resident (TSR) program or directly in a user program.
The 80386 includes four breakpoint registers that can be used to set breakpoints for a byte, word, or double-word. These breakpoints can be on write, read/write, or execute accesses. The four breakpoint registers are named DR0, DR1, DR2, and DR3. Each breakpoint register holds the 32-bit linear address of the byte, word, or double-word of interest. The address must be on a word or double-word boundary for those respective data types.
There are two additional registers: DR6 and DR7. DR6 is a status register that is read after the breakpoint has occurred to determine which of the four breakpoints was triggered. DR7 is a control register that is written to specify the parameters for a particular debug register for example, write-only on a double-word. DR7 also contains two enable bits that must be set to activate the breakpoint.
When a breakpoint goes off, an 80386 exception occurs. The exception handler must read the status register to determine which of the four breakpoints was triggered.
As with the I/O and range breakpoints, you can easily extend the capabilities of the debug registers by adding qualifying code to your exception handler. Other features that our Soft-ICE debugger provides by additional qualification code are breakpoint on read-only and comparison with a data value. A read-only breakpoint can be implemented by decoding the instruction that caused the exception to determine if it is a memory read. If not, control is returned to the target program.
Using breakpoint registers to perform breakpoints on execution has an advantage over the conventional INT 3 approach. Software debuggers for the 80x86 place an INT 3 at the address of the desired execute breakpoint. An INT 3 is used because it is a special single-byte instruction (most interrupts are 2-byte instructions) included in all 80x86 processors specifically for providing breakpoint capability. The INT 3 approach has the disadvantage that it cannot work in ROM; the breakpoint registers work fine in ROM code.
Optimally, the protected debugger should provide all the necessary debugging commands, such as dump, unassemble, modiIy, single-step, display registers, and so on. Many people, however, are addicted to their favorite language-specific debugger. For these people, it would be nice to extend the capabilities of their existing debugger by adding the features that are possible with the protected debugger. This is possible. The conventional debugger runs in the 8086 virtual machine and the protected debugger runs in protected mode. The DOS environment is still affected by your conventional software debugger, but you can add additional breakpoint capability, such as breakpoint on memory range or I/O ports.
Our Soft-lCE debugger provides both methods. For users who need a complete systems debugger, we provide all the necessary debugging commands. For those who wish to extend the capability of their existing software debuggers, we have a pop-up window that allows them to set sophisticated breakpoints that will trigger their software debugger.
Triggering the conventional software debugger is possible by understanding a little about the way most 80x86 software debuggers work. Most software debuggers use the INT 3 approach described earlier to provide breakpoints on execution. They also use the 80x86 INT 1 single-step mechanism. All members of the 8086 line have the capability to single-step the next instruction. The debugger must set a special processor flag called the trap flag, and when the trap flag is set, an INT 1 occurs after every instruction is executed.
By relying on the INT 1 vector or the INT 3 vector pointing to the conventional debugger's breakpoint-handling routines, we can generate INT 1s or INT 3s to wake up a conventional debugger. Most debuggers handle unsolicited INT is or INT 3s beautifully; however, a few will not. The third approach uses NMI. Most debuggers have a method of handling unsolicited breakpoints through the NMI mechanism--for example, Codeview provides this with a command-line switch. They provide this capability to break out of hung programs when the user presses an external button. This button is wired through the PC bus to the NMI pin. By taking advantage of these three conventionalbreakpoint mechanisms, we can wake up almost any conventional debugger.
A side effect of waking up a debugger unknowingly is a problem with reentrancy. Many debuggers enable interrupts or use DOS for I/O. If you wake the debugger up while the processor is in an interrupt routine, or within MS-DOS or the ROM BIOS, the debugger will fail. Waking up a nonreentrant debugger is still useful for application-level debugging. Many debuggers are mostly reentrant, and you can wake these up at any time; Periscope I and II are examples of these.
To build a complete protected debugger requires several additional components. These include "virtualizing" the video display system to save and restore the screen at any time and likewise the keyboard so users can debug keyboard device drivers. The purpose of this article was to describe debugging features, so I haven't gone into those details. By creatively applying 80386 protected mode operating system features in a real-address mode debugger, you can provide most of the features found in a hardware-assisted debugger with the convenience of a software debugger.