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 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 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.
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.
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.
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.
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.
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.
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.
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.
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]
;---------------------------------------------------------------------------
;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
[LISTING TWO]
;---------------------------------------------------------------------------
;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.
[LISTING THREE]
;---------------------------------------------------------------------------
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