This article contains the following executables: DB.ARC
Rick is a software engineer specializing in systems programming and is the coautbor of Screen Machine, a screen design/prototyping/code generation utility. He can be reached at P.O. Box 1109, Half Moon Bay, CA 94019.
The 80386's debug registers and capabilities for virtualizing an 8086 real-mode environment have fostered some powerful software debuggers. Designing and implementing such debuggers is a good vehicle for exploring these 80386 features. This article describes a debugger I recently developed, DB.EXE, that utilizes the 80386 debug registers and protected and virtual 8086 modes. Note that because of their length, the full source code listings for DB are only available electronically.
DB enables breakpoints to be generated on code execution (including ROM code), interrupts, data accesses, and I/O accesses. Since DB does not use DOS or the BIOS, it facilitates system-level debugging. For example, you can debug a program which takes over INT 16h. In addition, DB can coexist and work with a real-mode debugger such as Debug or Codeview.
DB Architecture
DB consists of two layers which work together to utilize protected-mode features, provide interaction with the user, and process commands. DBISR.ASM contains all protected-mode code that handles exceptions and manipulates areas restricted to privilege-level 0 code. All other resident DB code operates in V86 mode at privilege level 3. DB was designed in this fashion to simplify debugging. It is very difficult to debug protected-mode code because most stand-alone software debuggers cannot be used. Thus, by placing most of the debugger's logic in the privilege-level 3 layer, that code could be debugged using a software debugger. It may also facilitate using a high-level language for that portion of the code, although in this implementation, DB has been written entirely in assembler.
In cases where privilege-level 3 code needs to manipulate an area (such as the debug registers) that can only be accessed by more privileged code, the privilege-level 3 layer of DB issues a user software interrupt (such as INT 60h). This causes a general-protection exception. The protected-mode exception handler in the privilege-level 0 layer of DB recognizes the user software interrupt as a request for privileged service and dispatches it to the appropriate routine.
Initialization
Initialization begins with the reprogramming of the master PIC, so that interrupts start at 20h rather than at 08h. This eliminates the possibility of collision between protected-mode exceptions and standard PC-hardware interrupts. For example, the protected-mode general-protection exception 0dh (one of the areas at the heart of DB) will no longer conflict with IRQ5, which normally generates interrupt 0dh.
The global descriptor table (GDT) is the next area to be initialized. Entries are created for all segments to be used by DB, including the interrupt descriptor table (IDT) and task state segment (TSS). After all these data areas are established, virtual 8086 mode is entered by creating a protected-mode exception stack frame, setting the VM bit in the EFLAGS, and executing an IRETD. DB then terminates and stays resident in your system.
Breakpoints on Interrupts
Since, in protected mode, interrupts pass through gates in the IDT rather than through the interrupt vectors, DB can get control as each interrupt occurs. In fact, DB must route all interrupts to their respective real-mode interrupt service routines per the real-mode interrupt vectors. (See pass_thru in Listing One, page 108.)
DB conveniently forces all software interrupts to take a slightly more indirect route. All IDT entries for software interrupts are defined with descriptor privilege level (DPL) equal to 0. When this is compared against the current privilege level (CPL) of the V86 code (CPL=3) attempting to execute an INT n instruction, a general-protection exception (interrupt 0dh) occurs. Using the CS:IP of the faulting instruction, DB detects the attempted software interrupt (see gen_prot_isr in Listing Two, page 108) and can generate a breakpoint before it routes the interrupt to the appropriate real-mode ISR.
Breakpoints on I/O
The key to generating breakpoints on I/O is the I/O permission bitmap located at the end of the TSS. This bitmap specifies which I/O addresses a task may access. (Refer to the 80386 Programmer's Reference Manual for more details.) When code in the V86 task tries to execute an I/O instruction, the processor consults the I/O permission bitmap to see if the task has been allowed access to the particular I/O address. If the corresponding bit in the bitmap is set, a general-protection exception is generated.
By setting such bits, DB generates a breakpoint via gen_prot_isr when an I/O access occurs at a particular address. In such cases, DB temporarily clears the bitmap entry and allows the instruction to continue, with one slight difference--it sets the trap flag causing an INT 1h just after the I/O instruction completes. Upon receiving INT 1h, DB recognizes that it is single-stepping through an I/O instruction. At that point, it checks for further user-specified conditions and either activates the debugger (if the conditions are met) or simply clears the trap flag and exits. When the INT 1h service routine is exited, the bits corresponding to the I/O address are again set in the I/O permission bitmap.
Breakpoints on Data Accesses
DB uses the 80386 debug registers to generate breakpoints on data accesses and code execution. DB supports all of the debug-register data-access options, including break-on-write or read/write accesses to bytes, words, or dwords. Since DB utilizes debug registers for execution breakpoints (as opposed to the INT 3h method), breakpoints can be set in ROM as well as RAM. The do_debug_reg routine (see Listing Three, page 109) performs all of the debug-register manipulation.
Using DB
Once DB is resident in your system, it can be activated either by simultaneously pressing the left and right Shift keys or by an INT 1h Software interrupt.
All of the currently supported DB command are shown in Table 1. Note that the XUD (exit to user debugger) command can be used to pass control to another debugger. For example, you can load your application under Debug, hotkey into DB to define a breakpoint, quick back to Debug, and execute a Go command. When the specified breakpoint occurs, DB gets control. You can then return to Debug by executing XUD. This is accomplished by generating an INT 1h, which will activate most debuggers. Optionally, an alternative interrupt can be specified with XUD. For example, issuing the command XUD 3 causes an INT 3 to be generated. Once specified, the alternative interrupt will be generated by subsequent XUD commands until it is specifically overridden. Int 1h, the default, seems to work well with Debug. For Codeview, XUD 2 (causing the INT 2h associated with NMI) also works well.
Table 1: DB commands.
Command Description ---------------------------------------------------------------- R [register name] Display or change register. E [address] Edit memory. D [address] Dump memory. T [number] Trace. G [address] Go. BPX address Break on code execution. BP[B] address [RW | W] Break on byte data access. BPW address [RW | W] Break on word data access. BPD address [RW | W] Break on dword data access. BPIO port address [R | W | RW] Break on I/O port access. BPINT int number [AX | AH | AL Break on interrupt. EQ |=hex value] BL List breakpoints. BC [* | breakpoint number] Clear breakpoints. XUD [int number] Exit to user debugger. Q Quit.
The XUD command is useful when you want to supplement your debugger's capabilities by utilizing DB's more specialized features such as breaking on I/O accesses.
Limitations and Enhancements
Since the purpose of the debugger code provided with this article is to highlight the 80386 capabilities, I've not dealt with all the video aspects of the debugger. To keep the code simple, DB always assumes text mode (25 line) and makes no attempt to handle other video modes. (There are comments in the code which indicate where this could be added.)
Although DB supports setting breakpoints on interrupts, there is one limitation. Currently, DB does not operate correctly if you specify a condition which causes a breakpoint within an interrupt service routine for an interrupt with equal or higher priority than the keyboard. Therefore, don't specify conditions which cause breakpoints within INT 8h or 9h code. The reason is that DB requires IRQ1 interrupts to get its own keyboard input. Unfortunately, interrupts can not occur if breakpoints are reached within INT 8h or 9h code before the ISRs issue EOI to the PIC.
One solution may be to have DB provide the EOI, and then trap and ignore the EOI issued by the interrupt service routine. This would necessitate trapping I/O on PIC accesses and keeping track of which interrupt is currently being serviced.
Currently, DB doesn't employ the 80386 paging feature. If this were implemented, DB could provide more robust breakpoint capabilities on memory accesses: You could set breakpoints on ranges of memory accesses, as opposed to the simple debug register-type memory-access breakpoints utilized here. Also, via paging, DB could further verify memory accesses to prevent "runaway" programs from overwriting the debugger.
Note that an errant program could potentially clear interrupts and then "go out to lunch," rendering DB virtually (no pun intended) useless. This is because DB initializes the V86 task to run with I/O privilege level (IOPL) equal to 3, thereby allowing V86 mode "sensitive" instructions (CLI, STI, PUSHF, POPF, INT n, and IRET) to affect the interrupt flag.
An option could be added to DB in which the IOPL of the V86 task would be set to a more privileged level than 3, thus causing general-protection exceptions when the V86 mode sensiti e instructions are attempted. These instructions would need to be detected and emulated.
Conclusion
As you can see, these 80386 features provide enormous benefits when utilized for debugger applications. The techniques presented here show how to apply these processor capabilities. There's room for enhancements, but this is the foundation.
References
80386 Programmer's Reference Manual. Santa Clara, CA: Intel Corp., 1986.
Green, Thomas. "80386 Protected Mode and Multitasking." Dr. Dobb's Journal (September, 1989).
Margulis, Neil, "Advanced 80386 Memory Management." Dr. Dobb's Journal (April, 1989).
Turley, James L. Advanced 80386 Programming Techniques. Berkeley, CA: Osborne/McGraw-Hill, 1988.
Williams, Al. "Homegrown Debugging--386 Style!" Dr. Dobb's Journal (March, 1990).
Williams, Al. "Roll Your Own DOS Extender: Part II." Dr. Dobb's Journal (November, 1990).
_YOUR OWN PROTECTED-MODE DEBUGGER_ by Rick Knoblaugh[LISTING ONE]
<a name="01e6_000f"> ;--------------------------------------------------------------------------- ;pass_thru - This procedure is JMPed to by any interrupt handler which wishes ; to pass control to the original ISR per the interrupt vector table. Also, ; it checks to see if there are any breakpoints set on the int. If there are, ; the int being passed through is checked to see if it matches the condition ; for the break point. If the condition for the break point is met, DR0 is ; used to cause a break at the ISR. Enter: See stack_area struc for stack ; layout. Any error code has been removed from stack. EIP on stack has been ; adjusted if necessary. ;--------------------------------------------------------------------------- pass_thru proc near mov bp, sp pushad call adjust_ustack ;adjust user stack ;returns with [esi][edx] pointing to user stack area mov cx, [bp].s_cs ;put on user cs mov [esi][edx].user_cs, cx mov ecx, [bp].s_eip ;put on ip mov [esi][edx].user_ip, cx movzx ebx, [bp].s_pushed_int ;get int number movzx ecx, [ebx * 4].d_offset ;offset portion mov [bp].s_eip, ecx mov cx, [ebx * 4].d_seg ;segment portion mov [bp].s_cs, cx mov cx, offset gdt_seg:sel_data mov fs, cx assume fs:data push fs cmp fs:trap_clear, TRUE ;tracing through an int? jne short pass_thru500 mov fs:trap_clear, FALSE ;reset it mov fs:int1_active, TRUE ;debugger active ;If mov [esi][edx].user_cs, cx mov ecx, [bp].s_eip mov [esi][edx].user_ip, cx mov cx, ZCODE mov [bp].s_cs, cx mov cx, offset int_1_isr movzx ecx, cx mov [bp].s_eip, ecx pass_thru500: pop ds ;get data seg (was fs) assume ds:data cmp int1_active, TRUE ;is debugger active? je short pass_thru999 ;if so, don't even think ;of breaking mov cx, num_int_bp ;number of defined int breaks jcxz pass_thru999 ;if no int type break points mov si, offset int_bpdat pass_thru700: cmp [si].int_stat, ACTIVE ;is break on int enabled? jne short pass_thru800 dec cx cmp [si].int_num, bl ;is this the int specified? jne short pass_thru800 cmp [si].int_reg, NO_CONDITION ;no conditions? je short pass_thru750 ;if none go ahead and set break mov dx, [si].int_val ;get data for comparison cmp [si].int_reg, INT_AL_COMP ;condition compare on al? jne short pass_thru730 cmp al, dl ;condition met? je pass_thru750 ;if so, go ahead and set break jmp short pass_thru800 ;if != look for more conditions pass_thru730: cmp [si].int_reg, INT_AH_COMP ;condition compare on ah? jne short pass_thru740 cmp ah, dl ;condition met je short pass_thru750 ;if so, go ahead and set break jmp short pass_thru800 ;if != look for more conditions pass_thru740: ;condition compare on ax cmp ax, dx jne short pass_thru800 ;if != look for more conditions pass_thru750: mov ebx, [bp].s_eip ;get offset and movzx edx, [bp].s_cs ;segment of ISR shl edx, 4 ;convert to linear add edx, ebx mov ch, 1 ;set debug register mov al, DEB_DAT_LEN1 ;exec breaks use 1 byte length mov ah, DEB_TYPE_EXEC sub cl, cl ;debug reg zero call do_debug_reg jmp short pass_thru999 pass_thru800: add si, size info_int ;advance to next int break or cl, cl ;all int breaks checked? jnz short pass_thru700 ;if not, check the next one pass_thru999: popad add sp, 2 ;get rid of int number pop bp iretd pass_thru endp <a name="01e6_0010"> <a name="01e6_0011">[LISTING TWO]
<a name="01e6_0011"> ;--------------------------------------------------------------------------- ;gen_prot_isr - JMP here if int 0dh. Look for software int. If a software int ; caused the exception then: If debugger is active, look for user software ; interrupts issued by PL3 layer of debugger. If int 15h function 89h deny. ; If int 15h function 87h, emulate it. If none of these, simply route the ; interrupt per the real mode interrupt vector table. If exception was not ; caused by a software int and there are breakpoints defined on I/O accesses, ; look for I/O instruction. If it is an I/O instruction, temporarily clear ; the corresponding TSS I/O permission bit map bit and set trap flag to ; single step through the instruction. If other than software int or I/O, ; display cs:ip, 0dh and then halt. ;--------------------------------------------------------------------------- gen_prot_isr proc near pushad ;Note: Don't use DX or AX below as DX may contain an I/O port address; in the ; case of a software interrupt, AH will have a function code. Also, don't use ; SI or CX as they are inputs for extended memory block move function mov bx, offset gdt_seg:sel_databs mov ds, bx movzx ebx, [bp].e_cs ;get cs of user instruction shl ebx, 4 ;make linear add ebx, [bp].e_eip ;add ip mov bx, [ebx] ;get bytes at cs:ip mov di, offset gdt_seg:sel_data mov ds, di ;debugger's data cmp bl, INT_OPCODE je short gen_prot020 cmp bl, INT3_OPCODE jne gen_prot150 ;go look for I/O instruction mov bh, 3 ;interrupt 3 gen_prot020: cmp trace_count, 0 ;is debugger tracing? je short gen_prot040 ;if not, skip test below ;See if this software interrupt is the instruction through which the user ;is tracing. If it is, set flag. mov di, [bp].e_cs cmp di, tuser_cs jne short gen_prot040 mov edi, [bp].e_eip cmp di, tuser_ip jne short gen_prot040 ; Clear trap bit so that it will not be set on user stack. Note: If user is ; doing a "trace n" where n is a number of instructions exceeding the number of ; instructions in the ISR, instructions executing upon return from ISR will ; still be trapped through as the int 1 code will again set the trap flag. btr [bp].e_eflags, trapf mov trap_clear, TRUE gen_prot040: inc [bp].e_eip ;get past the 0cdh (or 0cch) cmp bh, 3 ;int 3? je short gen_prot060 ;if so, only 1 byte gen_prot050: inc [bp].e_eip gen_prot060: ;See if the debugger is active and if this software interrupt is one of the ;ones used by the PL3 portion of the debugger to get PL0 services. cmp int1_active, TRUE ;is debugger active? jne short gen_prot085 ;Note: In the event that an interrupt occuring while debugger is active ; (e.g. timer) actually uses these user software interrupts, ; code to verify caller would need to be added here. cmp bh, 60h ;do debug registers? jne short gen_prot080 popad call do_debug_reg jmp gen_prot299 gen_prot080: cmp bh, 61h ;do I/O bit map? jne short gen_prot085 popad ; Unlike accessing of debug registers, PL3 code could actually manipulate TSS ; I/O bit map directly. However, this interface keeps this in one location. call do_bit_map jmp gen_prot299 gen_prot085: cmp bh, 15h ;int 15? jne short gen_prot100 cmp ah, 89h ;request for protected mode? jne short gen_prot090 ;if so, can't allow bts [bp].e_eflags, carry ;set carry popad jmp gen_prot299 ;and return gen_prot090: cmp ah, 87h ;request for extended move? jne short gen_prot100 call emulate_blk_mov ;if so, we must do it popad mov ah, 0 ;default to success jnz gen_prot299 ;exit if success mov ah, 3 ;indicate a20 gate failed jmp gen_prot299 ;and return gen_prot100: ;Adjust stack so that error code goes away and int number retrieved from ;instruction goes in spot on stack where pushed int number is (for stacks ;with no error code). Stack will be the way pass_thru routine likes it. mov ax, bx mov bx, [bp].e_pushed_bp shl ebx, 16 ;get into high word mov bl, ah ;interrupt number mov [bp].e_errcode, ebx cmp bl, 1 ;software int 1? jne short gen_prot140 ; Check to see if we are already in debugger. This is to handle the unlikely ; case where there is an actual INT 1 instruction inside of an interrupt ; handler. If there is and debugger is active, instruction will be ignored. popad cmp int1_active, TRUE ;already in debugger? jne short gen_prot130 ;if not, go enter int 1 ;else ignore it by returning add sp, 2 ;get rid of int number pop bp iretd gen_prot130: add sp, 4 ;error code gone mov bp, sp pushad jmp int_1_210 ;go enter int 1 gen_prot140: popad add sp, 4 ;error code gone jmp pass_thru ;route the int via vectors gen_prot150: cmp num_io_bp, 0 ;any I/O break points defined? je short gen_prot400 ;if not, don't look for I/O xor ah, ah ;use as string flag cmp bl, REP_PREFIX ;rep ? jne short gen_prot190 mov ah, STRING ;only string type use rep mov bl, bh ;get 2nd byte gen_prot190: ; If repeat prefix was found, ah now has a flag indicating only string type ; I/O instructions should be expected and bl now contains the byte of object ; code past the repeat prefix. Note: To be complete, this code should also ; look for the operand-size prefix and segment overrides. mov si, offset io_table mov cx, IO_TAB_ENTRIES gen_prot200: or ah, ah ;strings only? jz short gen_prot225 ;if not, go test test [si].io_info, ah ;if table entry is not a string jz short gen_prot300 ;type I/O, go try next one gen_prot225: cmp bl, [si].io_opcode jne short gen_prot300 mov io_instrucf, TRUE ;instruction found mov cl, [si].io_info ;get info about instruction mov io_inst_info, cl test cl, CONSTANT ;port number in instruction? jz short gen_prot250 ;if not, we have it movzx dx, bh ;get port gen_prot250: mov io_inst_port, dx ;save port mov cx, 1 ;number of bits sub ah, ah ;indicate clear call do_bit_map gen_prot260: bts [bp].e_eflags, trapf ;single step i/o popad gen_prot299: add sp, 2 ;int number pushed pop bp add sp, 4 ;error code iretd gen_prot300: add si, size io_struc ;advance to next table entry loop gen_prot200 gen_prot400: ;Also need to add code here to check ; ch = 3 get bn portion of debug status register into ax ; If clearing and eax !=0, eax holds other bits to be cleared (used for ; also clearing ge or le bits). cl = debug register number (0-3) if setting ; also have: al = length (0=1 byte, 1=2 bytes, 3=4 bytes); ah = type ; (0=execution, 1=write, 3=read/write); edx = linear address for break ; Also, if al='*' simply reactivate the breakpoint keeping the existing ; type and address. Exit: if disabling, specified debug register breakpoint ; is disabled. If enabling, specified debug register is loaded and ; breakpoint is enabled. If getting debug status register, bn portion of ; DR6 is returned in AX. ; Save ebx. <a name="01e6_0012"> <a name="01e6_0013">[LISTING THREE]
<a name="01e6_0013"> ;--------------------------------------------------------------------------- do_debug_reg proc near cmp ch, 3 ;requesting status? jne short do_deb050 ;if not mov eax, dr6 ;debug status register and ax, 0fh ;isolate bn status ret ;and return do_deb050: push ebx mov ebx, dr7 ;get debug control reg cmp ch, 1 ;determine function jb short do_deb850 ;if clear function go do it ja short do_deb100 ;setup, but not enable cmp al, '*' ;simply reset? je short do_deb850 do_deb100: push cx ;save function/reg # push edx ;save linear address mov edx, 0fh ;4 on bits shl cl, 2 ;reg # * bits associated add cl, 16 ;upper portion of 32 bit reg shl edx, cl not edx ;associated bits off and ebx, edx ;in the dr7 value shl al, 2 ;length bits to len position or al, ah ;put in the type mov dl, ah ;save type sub ah, ah shl eax, cl ;move len/rw to position or ebx, eax or dl, dl ;execution type? jz short do_deb500 ;if so, don't need ge bts bx, ge_bit do_deb500: pop edx ;restore linear address pop cx ;and debug register # cmp cl, 1 je short do_deb600 ja short do_deb700 mov dr0, edx jmp short do_deb800 do_deb600: mov dr1, edx jmp short do_deb800 do_deb700: cmp cl, 3 je short do_deb750 mov dr2, edx jmp short do_deb800 do_deb750: mov dr3, edx do_deb800: cmp ch, 2 ;setup, but not enable? je short do_deb900 ;if so, skip enable do_deb850: shl cl, 1 ;get to global enable for # inc cl movzx dx, cl ;bit number to turn on bts bx, dx ;set on in dr7 value or ch, ch ;set function? jnz short do_deb900 btr bx, dx ;if not, disable break or ax, ax ;clear ge or le? jz short do_deb900 ;if not continue btr bx, ax ;if so, clear ge or le bit do_deb900: mov dr7, ebx ;put adjusted value back pop ebx do_deb999: ret do_debug_reg endp isrcode ends end End Listings
Copyright © 1992, Dr. Dobb's Journal