Use the 80386's hardware to debug your programs by including Al's assembly language code to establish breakpoints.
March 01, 1990
URL:http://www.drdobbs.com/embedded-systems/homegrown-debugging-386-style/184408317
Al Williams is a staff systems engineer for Quad-S Consultants Inc. His current work includes a hypertext system, several expert systems, and a 386 DOS extender package. He can be reached at 2525 South Shore Boulevard, Suite 309, League City, TX 77573.
Although the installed base of 80386-based machines is ever increasing, most use this souped-up machine as a faster 8086. One of the problems in running the 80386 under DOS is that you lose many of the advantages of the 386. In addition, many of the 80386's powerful features are only usable in protected mode. Of course, developers loath to use special 80386 features because this can shut them out of the large 8086/80286 market.
Still, some features are usable while the 80386 is operating as an 8086 (the so-called "real mode"). For instance, the 80386 has powerful on-board hardware that allows sophisticated debugging techniques that require hardware debugging boards on other processors. This on-board hardware is available in real mode (as well as the other modes). With a little ingenuity, you can put this hardware to work while debugging programs.
This article puts a little of that kind of ingenuity in your hands by showing how you can use the 80386 hardware to debug your programs. I'll provide a program that can be included in your assembly code to establish breakpoints for the purpose of debugging either C or assembly language programs. In addition, I'll provide an example program and a quick utility that I'll explain shortly. All examples presented in this article compile under either MASM 5.0 or Microsoft C 5.1.
BREAK386 (Listing One, BREAK386 .ASM, page 96) is not a traditional debugger in the sense of, say, DEBUG or CodeView. By adding BREAK386 to your assembly language code, you can study it with code, data, and single-step breakpoints. You can also examine DOS or BIOS interrupts that your program calls. In addition, BREAK386 can add the same 386 hardware debugging to your Microsoft C programs.
BREAK386 provides functions to set up 386 debugging (setup386( )), set breakpoints (break386( )), and reset 80386 debugging (clear386()). In addition, BREAK386 provides an optional interrupt handler ( int1_386( )) that supports register, stack, and code dumps along with single stepping. You can use any of these functions from either C or assembly language.
There are cases where you may wish to modify int1_386( ) or write your own interrupt handler. For example, you may want to send the register dumps to a printer and automatically restart your program. With C, you will often want the interrupt handler to print out variables instead of registers. I'll provide some example interrupt handlers in C in a later section.
You must assemble BREAK386 before you can use it. Be sure to change the .MODEL statement to reflect the model you are using. If you are using explicit segment definitions in assembly, you must decide how to integrate BREAK386's code and data segments with your own. Assemble BREAK386 with the /Ml option to prevent MASM from converting all labels to uppercase. The resulting .OBJ file can be linked with your programs just as with any other object module.
If you are using programs (such as memory managers or multitaskers) that also use 386-specific functions, you may have to remove these programs before BREAK386 will function. The other program will usually report a "privilege exception" or something similar. Simply remove the other 386 programs and try again.
Adding 386 breakpoints to your program requires three steps:
Figure 1 shows the output from a breakpoint dump when using int1_ 386( ). The hexadecimal number on the first line is the contents of the low half of the DR6 register at the time of the breakpoint. The display shows all 16-bit and segment registers (except FS and GS). Following that is a dump of 32 words of memory starting at the bottom of the stack (1CB1:09FA in the example). The first three words of the stack are from the debug interrupt. The first word is the IP register, followed by the CS register and the flags. A simple change in the interrupt handler can remove this extra data from the display (see "Detailed Program Operation" in the next section).
Program breakpoint:OFF1 AX=0000 FL=7216 BX=0080 CX=0007 DX=06AA SI=0000 DI=0A00 SP=09FA BP=0882 CS=1B66 IP=0051 DS=1BAD ES=1B56 SS=1CB1 Stack dump:(1CB1 : 09FA) 0051 1B66 7216 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 CODE=1B66 : 0049 = 6A 04 E8 3F 00 83 C4 08 * B9 14 00 8A D1 80 C2 41 <V>iew output, <T>race toggle, <C>ontinue or <A>bort? _
Below the stack dump is a dump of program code. This dump usually consists of 16 bytes; 8 bytes before the current instruction and 8 bytes at the instruction pointer. This is convenient for data breakpoints because they occur after the offending instruction. The dump shows the starting memory address (1 B66:0049) followed by the bytes at that address. An asterisk marks the current CS:IP location, followed by the remaining 8 bytes. If IP is less than 8, the code dump will start at CS:0 resulting in fewer than 8 bytes before the asterisk.
The last line of the dump prompts you for further action. You can:
Listings Four and Five, page 102, show examples of using BREAK386 in assembly and C. The identifiers beginning with BP_ are defined in BREAK386.H and BREAK 386.INC.
A few notes on these functions are in order. Your program must call setup386( ) before any other BREAK386 calls. You should pass it a segment and an offset pointing to the interrupt handler. After calling setup386( ), you may use break386( ) to set and clear breakpoints. Figure 2 shows the parameters break386( ) requires.
retcode=break386(n,type,address); where: n is the breakpoint number (from 1 to 4). type is the type of breakpoint. This should be one of the manifest constants defined in BREAK386.H (or BREAK386.INC). If you are clearing the breakpoint, the type is not meaningful. address is the address to set the breakpoint. This must be a far address (that is, one with both segment and offset). If you are using small model C, you should cast the pointer to be a far type (see the example). To clear a breakpoint, set address to 0000:0000 (or a far NULL in C). retcode is returned by the function. A zero indicates success. A non-zero value means that you tried to set a breakpoint less than 1 or greater than 4. Note that the type parameter is not checked for validity. The types available are: BP_CODE - Code breakpoint BP_DATAW1 - One byte data write breakpoint BP_DATARW1 - One byte data read/write breakpoint BP_DATAW2 - Two byte data write breakpoint BP_DATARW2 - Two byte data read/write breakpoint BP_DATAW4 - Four byte data write breakpoint BP_DATARW4 - Four byte data read/write breakpoint
You must keep in mind a few facts about the 80386 when setting breakpoints or tracing. First, 2- and 4-byte data breakpoints must be aligned according to their size. For example, it is incorrect to set a 2-byte breakpoint at location 1000:0015 because that location is on an odd byte. Similarly, a 4-byte breakpoint can monitor address 1000:0010 or 1000:0014 but not address 1000:0013. If you must watch an unaligned data item, you will have to set multiple breakpoints. For example, to monitor 2 bytes at 1000:0015, set a 1-byte breakpoint at 1000:0015 and another at 1000:0016.
Also, keep in mind that a data breakpoint will occur even if you only access a portion of its range. For instance, if you are monitoring a word at 2200:00F0 and a program writes a byte to 2200: 00F1, a breakpoint will occur.
Setting a data breakpoint with break 386( ) will also set the global exact bit. When all the data breakpoints are either reassigned or deactivated, break386( ) will clear the exact bit.
Because int1_386( ) always sets the resume flag, you will find that a code breakpoint that immediately follows a data breakpoint won't work. I'll show how this can be rectified shortly.
Because INT and INTO instructions temporarily clear the trace flag, BREAK-386 will not single step through interrupt handlers. If you wish to single step through an interrupt routine, you will have to set a breakpoint on its first instruction. A replacement for int1_ 386( ) might emulate INT and INTO instructions to solve this problem.
Because BREAK386 uses BIOS keyboard and video routines, take care when placing breakpoints in these routines. In addition, single-stepping BIOS keyboard and video routines should be avoided. If you must debug in these areas, reassemble BREAK386 so that it doesn't use BIOS (see the DIRECT equate in BREAK386.ASM). Note, however, that many of its features will no longer function. Finally, you should avoid setting breakpoints in BREAK386's code or data.
BREAK386.INC contains two macros, traceon and traceoff, that can be used to control tracing. You may insert them anywhere in your code to enable or disable tracing. Remember, however, that you will see the traceoff macro as well as your own code when single stepping.
The function clear386( ) must be called prior to exiting the program. This turns off the breakpoint handlers. If you fail to call clear386( ) for any reason (a control-break, or a critical error), the next program that uses a location you have breakpointed will cause the break to occur. This can have unfortunate consequences because your interrupt 1 handler is probably no longer in memory. If you find that you have exited a program without turning off debugging and you have not encountered a breakpoint, run DBGOFF (Listing Six, page 104) to turn off hardware debugging.
With some care, BREAK386 can be used with other debuggers. In CodeView, for example, BREAK386 seems to work fine, as long as you are not single stepping (via CodeView). When you single step data breakpoints will be ignored and BREAK386 code breakpoints will "freeze" CodeView at that step. If you are using BREAK386 with CodeView, it is probably a good idea to leave the code breakpoints and single stepping to CodeView.
BREAK386 (Listing One) begins with the .386P directive, which ensures that MASM 5.0 will generate references to the debug registers. Be careful to place the .MODEL directive before the .386P, otherwise 32-bit segments will be generated (which doesn't work well with unmodified DOS!).
The parameters you may want to change are near the top of the source file. The equate to DIRECT controls the video mode. If DIRECT is 0, BREAK386 uses BIOS for input and output. If, however, you want to poke around in the keyboard or video routines, you must set DIRECT to 1. This causes BREAK386 to use direct video output for the debug dump. It will share the screen with your program (no video swapping) and breakpoints will simply terminate the program in a similar manner to the "A" command mentioned earlier.
You can change the STKWRD equate to control how many words are dumped from the stack when using int1_386( ). Setting STKWRD to zero will completely disable stack dumping. Similarly, if you set INTSTACK to zero, the display will not show the IP/CS/FLAGS at the top of the stack. If you are writing your own interrupt handler and don't need int1_386( ), you can assemble with ENABLE_INT1 set to zero to reduce BREAK386's size.
The operations of start386( ), clear-386( ), and break386( )are fairly straight-forward. The implementation of int1_386( ) deserves some comment. It is important to realize that int1_386( ) only debugs non-386-specific programs because it only saves the 16-bit registers and the 8086 segment registers (int1_386( ) does not destroy FS and GS). Because int1_386( ) only runs on a 386, it does use the 32-bit registers. You can easily modify int1_386( ) to save all the 386 registers, but it requires more space on the interrupted program's stack.
The most difficult aspect of the interrupt handler is managing the resume flag. The code below label c1 converts the three words at the top of the stack into six words so that setting the resume flag is possible. There are three things to remember about the way the resume flag is managed:
It is possible to write an interrupt handler completely in C to monitor data breakpoints. The hahdler must be declared as a far interrupt function. For example, the following function could be linked with the example in Listing Five:
void interrupt far new1(Res,Rds,Rdi,Rsi,Rbp,Rsp,Rbx,Rdx, Rcx,Rax)
{ printf("\nBreakpoint reached.\n"); }
By calling setup386new1( ) instead of setup386(int1_386), new1( )will be invoked for every breakpoint. Your function can read and write the interrupted program's registers using the supplied parameters (Rax, Rbx, and so on). Keep in mind that you cannot use this technique for code breakpoints. C's inability to manipulate the resume flag will cause an endless loop on a code breakpoint.
Listing Seven, provides the functions to write interrupt handlers in C. The procedure is much the same as described earlier, except that you must call csetup386( ) instead of setup386( ). The argument to csetup386( ) is always a pointer to an ordinary far function (even in small model).
The actual interrupt handler is _cint1_ 386( ). This function will call your C code when an interrupt occurs. _cint1 _386( ) passes your routine two arguments. The first argument, a far void pointer, is set to the beginning of the interrupted stack frame (see Figure 3 for the format of the stack frame). The second argument is an unsigned long int that contains the contents of DR6.
All registers, and local variables on the stack can be read using the pointer to the stack frame (if you know where to look). In addition, all values (except SS) can be modified. It is usually wise not to modify SP, CS, or IP.
_cint1_386( ) switches to a local stack. The size of the stack can be controlled using STACKSIZE (near the top of Listing Seven). Be sure to adjust the stack if you need more space.
Listing Eight (page 105) shows an example of an interrupt handler in C. The example interrupt handler displays a breakpoint message and allows you to continue with or without breakpoints, abort the program, or change the value of a local variable in the loop( ) function.
Many enhancements and modifications are possible with BREAK386. By altering the words on int1_386( )'s stack, for example, you can modify registers. You can redirect output to the printer (although you can screen print the display now) by replacing the OUCH routine. Perhaps the most ambitious enhancement would be to use BREAK386 as the core of your own debugger. You could write a stand-alone debugger or a TSR debugger that would pop up over another debugger (DEBUG or CodeView).
Keep in mind that 386 hardware breakpoints aren't just for debugging. The data breakpoint capability has many uses. For example, you might want to monitor the BIOS keyboard typeahead buffer's head and tail pointers to see when a keystroke is entered or removed. In this manner you could capture the keyboard interrupt in such a way that other programs couldn't reprogram your interrupt vector.
You can also use data breakpoints to detect interrupt vector changes or interrupt processing. Some assembly language programs could use data breakpoints for automatic stack overflow detection. Programs that decrement the stack pointer without using a push instruction (Microsoft C programs, for example) are not candidates for this type of stack protection.
Debugging with 386 assistance is quite practical and useful. The programs presented here should get you started and help you develop your own programs with this powerful hardware feature.
Turley, James L., Advanced 80386 Programming Techniques, Osborne McGraw-Hill, Berkeley, Calif., 1988.
Intel Corporation, 80386 Programmer's Reference Manual, Intel Corp., Santa Clara, Calif., 1986.
Most PC developers are familiar with some aspect of chip debug assistance. Even the 8088 has a breakpoint interrupt and a "single-step flag," which allows debuggers to trace code one instruction at a time. The 386 shares these same features with the earlier processors, but adds eight debug registers (two of which Intel reserves). These debug registers control the hardware breakpoint features.
Hardware breakpoints are much more powerful than ordinary breakpoints (such as those in DEBUG) for two reasons. First, hardware breakpoints don't actually modify your program. This means that you can set breakpoints anywhere, even in ROM. Also, a program can't overwrite a breakpoint when it modifies itself or loads an overlay. Second, it is possible to set breakpoints on data. A data breakpoint triggers when your program accesses a certain memory location.
Microsoft's CodeView implements a similar data breakpoint capability, called "tracepoints." To maintain compatibility with non-386 PCs, however, CodeView doesn't use 386 features. As a result, CodeView checks tracepoints after the execution of each instruction. This, of course, is terribly slow. By moving the tracepoints to 386 hardware, execution isn't slowed down at all. Actually, you will usually want to slow down execution just a bit (see the discussion of the exact bit). Even then, the slowdown in execution is imperceptible.
Because there are four debug address registers in the 80386, it is possible to have four active breakpoints at once. Each address register (DRO-DR3) represents a linear address at which a different breakpoint will occur. In protected mode, the concept of a linear address is not straightforward. In real mode, however, a linear address can easily be calculated from a segment/offset pair. Simply multiply the segment value by 10 hex (shift left 4 bits) and add the offset. For example, to set a data breakpoint at B800:0020 (somewhere in the CGA video buffer), you would need a linear address of:
B800 x 10 + 20 = B8020
Once you have loaded the address registers, you must enable the breakpoints you wish to use and tell the processor what type of breakpoints they are. This is done via the debug control register (DR7). DR7 contains bits to enable each breakpoint and to set their type individually (see Figure 4). You will notice that DR7 has global and local enable bits as well as global and local exact bits (explained shortly). The difference between the various global bits and local bits is only important when the 80386 is multitasking in protected mode. For the purpose of this article, they are the same.
The exact bits are flags to tell the 80386 to slow down. At first glance, this doesn't seem to be helpful, but a detailed look at the 80386 architecture reveals the purpose of this bit.
The 80386 gains some of its speed by overlapping instruction fetches and data fetches. This is an excellent idea when executing code, but causes problems in debugging data. Without the exact bit set, a data breakpoint will not occur at the instruction that caused the data access! Being somewhat of an inconvenience, Intel included the GE/LE bits. With either (or both) of them set, data breakpoints will occur immediately after the instruction that caused them, although the processor will lose a slight amount of speed.
All debug breakpoints generate an interrupt 1. To distinguish the various breakpoints, you must read the debug status register (DR6). DR6 has bits corresponding to the various breakpoint conditions (see Figure 5). Note the BT flag at bit 15. As with the local bits in DR7, only multitasking systems use the BT flag. Therefore, the flag is not considered in this article. The 386 never clears the bits in DR6, so after you determine what caused the interrupt, you should clear DR6.
With the general detect (GD) bit set in DR7, the 80386 prohibits access to the debug registers. Any attempt to access the debug registers will cause an interrupt 1 with the BD flag set in DR6. Intel's in-circuit emulator uses this feature, although you can use it if you have any reason to disable or control access to the debug registers. When a GD interrupt occurs, the interrupt handler is invoked and the GD bit is cleared. Otherwise, the routine would fault (with an endless loop) when the interrupt routine attempted to read DR6.
You can decide from the interrupt routine whether to terminate the user program, or to allow access to the registers. BREAK386 does not use the GD bit.
The last consideration with breakpoint interrupts is how to resume the interrupted program. If we simply return (as in a normal interrupt), there is nothing to stop a code breakpoint from occurring again immediately. The resume flag (found in the flag's register) prevents this from occurring. This flag inhibits further debug exceptions while set, and resets automatically as soon as one instruction successfully executes. Control of the resume flag is automatic in protected mode. Handling it from real mode, however, is somewhat of a trick, as seen in BREAK386.
-- A.W.
HOMEGROWN DEBUGGING -- 386 STYLE! by Al Williams
[LISTING ONE]
Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.
;***************************************************************************
;* File: BREAK386.ASM *
;* BREAK386 "main programs". Contains setup386, clear386, break386 and *
;* int1_386. *
;* Williams - June, 1989 *
;* Compile with: MASM /Ml BREAK386; *
;***************************************************************************
.MODEL small
.386P
public _break386,_clear386,_setup386,_int1_386
; Set up stack offsets for word size arguments based on the code size
; Be careful, regardless of what Microsoft's documentation says,
; you must use @CodeSize (not @codesize, etc.) when compiling with /Ml
IF @CodeSize ; True for models with far code
arg1 EQU <[BP+6]>
arg2 EQU <[BP+8]>
arg3 EQU <[BP+10]>
arg4 EQU <[BP+12]>
ELSE
arg1 EQU <[BP+4]>
arg2 EQU <[BP+6]>
arg3 EQU <[BP+8]>
arg4 EQU <[BP+10]>
ENDIF
.DATA
; Things you may want to change:
DIRECT EQU 0 ; IF 0 use BIOS; IF 1 use direct video access
STKWRD EQU 32 ; # of words to dump off the stack
INTSTACK EQU 1 ; When 0 don't display interrupt stack words
USE_INT1 EQU 1 ; Set to 0 to disable int1_386()
oldoffset dw 0 ; old interrupt 1 vector offset
oldsegment dw 0 ; old interrupt 1 vector segment
IF USE_INT1
video dw 0b000H ; segment of video adapter (changed by vinit)
csip db 'CODE=',0
done db 'Program terminated normally.',0
notdone db 'Program breakpoint:',0
stkmess db 'Stack dump:',0
vpage db 0
vcols db 80
IFE DIRECT
prompt db '<V>iew output, <T>race toggle, <C>ontinue or <A>bort? ',0
savcursor dw 0 ; inactive video cursor
ALIGN 4
vbuff dd 1000 dup (07200720H)
ELSE
cursor dw 0
color db 7
ENDIF
ENDIF
.CODE
; This is the start up code. The old interrupt one vector is saved in
; oldsegment, oldoffset. int1_386 does not chain to the old vector, it
; simply replaces it.
_setup386 proc
push bp
mov bp,sp
push es
mov ax,3501H ; get old int1 vector
int 21h
mov ax,es
mov oldsegment,ax
mov oldoffset,bx
pop es
mov ax,arg2 ; get new interrupt handler address
push ds
mov dx,arg1
; If int1_386 is being assembled, setup386 will check to see if you are
; installing int1386. If so, it will call vinit to set up the video parameters
; that int1_386 requires.
IF USE_INT1
cmp ax,seg _int1_386
jnz notus
cmp dx,offset _int1_386
jnz notus
push dx
push ax
call vinit ; Int'l video if it is our handler
pop ds
pop dx
ENDIF
notus: mov ax,2501H ; Store interrupt address in vector table
int 21H
pop ds
xor eax,eax ; Clear DR7/DR6 (just in case)
mov dr7,eax
mov dr6,eax
pop bp
ret
_setup386 endp
; This routine sets/clears breakpoints
; Inputs:
; breakpoint # (1-4)
; breakpoint type (see BREAK386.INC)
; segment/offset of break address (or null to clear breakpoint)
; Outputs:
; AX=0 If successful
; AX=-1 If not successful
_break386 proc
push bp
mov bp,sp
mov bx,arg1 ; breakpoint # (1-4)
cmp bx,1
jb outrange
cmp bx,4
jna nothigh
outrange:
mov ax,0ffffH ; error: breakpoint # out of range
pop bp
ret
nothigh:
movzx eax,word ptr arg4 ; get breakpoint address
shl eax,4
movzx edx,word ptr arg3 ; calculate linear address
add eax,edx ; if address = 0 then
jz resetbp ; turn breakpoint off!
dec bx ; set correct address register
jz bp0
dec bx
jz bp1
dec bx
jz bp2
mov dr3,eax
jmp short brcont
bp0: mov dr0,eax
jmp short brcont
bp1: mov dr1,eax
jmp short brcont
bp2: mov dr2,eax
brcont:
movzx eax,word ptr arg2 ; get type
mov cx,arg1 ; calculate proper position
push cx
dec cx
shl cx,2
add cx,16
shl eax,cl ; rotate type
mov edx,0fh
shl edx,cl ; calculate type mask
not edx
pop cx
shl cx,1 ; calculate position of enable bit
dec cx
mov ebx,1
shl ebx,cl
or eax,ebx ; enable bp
mov ebx,dr7 ; get old DR7
and ebx,edx ; mask out old type
or ebx,eax ; set new type/enable bits
; Adjust enable bit (set on for data bp's, off if no data bp's)
adjge:
mov eax,200H
and ebx,0fffffdffH ; reset GE bit
test ebx,033330000H ; test for data bp's
jz nodatabp
or ebx,512
nodatabp:
mov dr7,ebx
pop bp
xor ax,ax
ret
; Here we reset a breakpoint by turning off it's enable bit & setting type to 0
; Clearing the type is required so that disabling all data breakpoints will
; clear the GE bit also.
resetbp:
mov cx,bx ; calculate type/len bit positions
mov edx,0fh
dec cx
shl cx,2
add cx,16
shl edx,cl
not edx
mov cx,bx ; calculate enable bit position
shl cx,1
dec cx
mov eax,1
shl eax,cl
not ax ; flip bits
mov ebx,dr7
and ebx,eax ; clear enable
and ebx,edx ; clear type
jmp adjge
_break386 endp
; Reset the debug register, disabling all breakpoint. Also restore the old
; interrupt 1 vector
_clear386 proc
pushf
pop ax
and ax,0FEFFH ; turn off trace flag
push ax
popf
xor eax,eax ; turn off all other breakpoints
mov dr7,eax
mov dr0,eax
mov dr1,eax
mov dr2,eax
mov dr3,eax
mov dr6,eax
mov ax,2501H ; restore old int 1 vector
push ds
mov bx,oldsegment
mov dx,oldoffset
mov ds,bx
int 21H
pop ds
ret
_clear386 endp
IF USE_INT1
; This is all code relating to the optional INT 1 handler
; This macro is used to get a register value off the stack and display it
; R is the register name and n is the position of the register on the stack
; i.e.: outreg 'AX',10
outreg macro r,n
mov ax,&r
mov dx,[ebp+&n SHL 1]
call regout
endm
; This is the interrupt 1 handler
_int1_386 proc far
sti ; Enable interrupts (see text)
pusha ; Save all Registers
push ds
push es
push ss
push @data
pop ds ; Reload DS
mov bp,sp ; point ebp to top of stack
IFE DIRECT
call savevideo
ENDIF
mov ax,video ; get video addressabilty
mov es,ax
assume cs:@code,ds:@data
mov bx,offset notdone ; Display breakpoint message
call outstr
mov edx,dr6
call hexout
xor edx,edx
mov dr6,edx
call crlf
;do register dump
outreg 'AX',10
outreg 'FL',13
outreg 'BX',7
outreg 'CX',9
outreg 'DX',8
call crlf
outreg 'SI',4
outreg 'DI',3
outreg 'SP',6
outreg 'BP',5
call crlf
outreg 'CS',12
outreg 'IP',11
outreg 'DS',2
outreg 'ES',1
outreg 'SS',0
call crlf
; do stack dump
IF STKWRD
mov bx,offset stkmess
call outstr ; Print stack dump title
push fs
mov dx,[ebp] ; get program's ss
mov fs,dx
mov al,'('
call ouch
mov al,' '
call ouch
call hexout
mov al,':'
call ouch
mov al,' '
call ouch
mov bx,[ebp+12] ; get stack pointer (before pusha)
IFE INTSTACK
add bx,6 ; skip interrupt info if desired
ENDIF
mov dx,bx
push bx
call hexout
mov al,')'
call ouch
call crlf
pop bx
mov cx,STKWRD
sloop:
mov dx,fs:[bx] ; get word at stack
push bx
push cx
call hexout ; display it
pop cx
pop bx
inc bx
inc bx
loop sloop
pop fs
ENDIF
nostack:
; Here we will dump 16 bytes starting 8 bytes prior to the instruction
; that caused the break
push fs
call crlf
mov bx, offset csip
call outstr
mov cx,8
mov ax,[ebp+24] ; get cs
mov fs,ax
mov bx,[ebp+22] ; get ip
cmp bx,8 ; make sure we have 8 bytes before
jnb ipbegin ; the begining of the segment
mov cx,bx ; If not, only dump from the start
ipbegin: sub bx,cx ; of the segment
push bx
push cx
mov dx,ax ; display address
call hexout
mov al,':'
call ouch
mov al,' '
call ouch
mov dx,bx
call hexout
mov al,'='
call ouch
pop cx
pop bx
or bx,bx ; if starting at 0, don't display any
jz ipskip ; before IP
iploop:
mov dl,fs:[bx] ; get byte
push bx
push cx
call hex1out ; output it
pop cx
pop bx
inc bx
loop iploop
ipskip:
push bx
mov al,'*' ; put '*' before IP location
call ouch
mov al,' '
call ouch
pop bx
; This is basically a repeat of the above loop except it dumps the 8 bytes
; starting at IP
mov cx,8
xiploop:
mov dl,fs:[bx]
push bx
push cx
call hex1out
pop cx
pop bx
inc bx
loop xiploop
call crlf
call crlf
pop fs
IFE DIRECT
; Here we will ask if we should continue or abort
mov bx,offset prompt
call outstr
keyloop:
xor ah,ah ; Get keyboard input
int 16H
and al,0dfh ; make upper case
cmp al,'T'
jz ttoggle
cmp al,'A'
jz q1
cmp al,'C'
jz c1
cmp al,'V'
jnz keyloop
; Display program's screen until any key is pressed
call savevideo
xor ah,ah
int 16H
call savevideo
jmp keyloop
; Execution comes here to toggle trace flag and continue
ttoggle:
xor word ptr [bp+26],256 ; toggle trace flag on stack
; Execution comes here to continue running the target program
c1:
call crlf
IFE DIRECT
call savevideo
ELSE
xor ax,ax
mov cursor,ax
ENDIF
pop ss
pop es
pop ds
popa
; This seems complicated at first.
; You MUST insure that RF is set before continuing. If RF is not set
; you will just cause a breakpoint immediately!
; In protected mode, this is handled automatically. In real mode it
; isn't since RF is in the high 16 bits of the flags register.
; Essentially we have to convert the stack from:
;
; 16 bit Flags 32 bit flags (top word = 1 to set RF)
; 16 bit CS to -----> 32 bit CS (garbage in top 16 bits)
; 16 bit IP 32 bit IP (top word = 0)
;
; All this so we can execute an IRETD which will change RF.
sub esp,6 ; make a double stack frame
xchg ax,[esp+6] ; get ip in ax
mov [esp],ax ; store it
xor ax,ax
mov [esp+2],ax ; eip = 0000:ip
mov ax,[esp+6]
xchg ax,[esp+8] ; get cs
mov [esp+4],ax
xor ax,ax
mov [esp+6],ax
mov ax,[esp+8] ; zero that stack word & restore ax
xchg ax,[esp+10] ; get flags
mov [esp+8],ax
mov ax,1 ; set RF
xchg ax,[esp+10]
iretd ; DOUBLE IRET (32 bits!)
ENDIF
; Execution resumes here to abort the target program
q1:
IFE DIRECT
call savevideo
ENDIF
call quit
_int1_386 endp
IFE DIRECT
; save video screen & restore ours (only with BIOS please!)
; (assumes 25 lines/page)
savevideo proc near
pusha
push es
mov ah,0fh
int 10h ; reread video page/size in case
mov vpage,bh ; program changed it
mov vcols,ah
push savcursor
mov ah,3 ; get old cursor
mov bh,vpage
int 10H
mov savcursor,dx
pop dx
mov ah,2 ; set new cursor
int 10H
movzx ax,vpage
mov cl,vcols ; compute # bytes/page
xor ch,ch
mov dx,cx ; vcols * 25 * 2
shl cx,3
shl dx,1
add cx,dx
mov dx,cx
shl cx,2
add cx,dx
push cx
mul cx
mov di,ax ; start at beginning of page
pop cx
shr cx,2 ; # of double words to transfer
mov ax,video
mov es,ax
mov si,offset vbuff ; store inactive screen in vbuff
xloop: mov eax,es:[di] ; swap screens
xchg eax,[si]
mov es:[di],eax
add si,4
add di,4
loop xloop
pop es
popa
ret
savevideo endp
ENDIF
; This routine prints a register value complete with label
; The register name is in AX and the value is in dx (see the outreg macro)
regout proc near
push dx
push ax
mov al,ah
call ouch
pop ax
call ouch
mov al,'='
call ouch
pop dx
call hexout
ret
regout endp
; Plain vanilla hexadecimal digit output routine
hexdout proc near
and dl,0fh
add dl,'0'
cmp dl,3ah
jb ddigit
add dl,'A'-3ah
ddigit:
mov al,dl
call ouch
ret
hexdout endp
; Plain vanilla hexadecimal word output routine
hexout proc near
push dx
shr dx,12
call hexdout
pop dx
push dx
shr dx,8
call hexdout
pop dx
; Call with this entry point to output just a byte
hex1out:
push dx
shr dx,4
call hexdout
pop dx
call hexdout
mov al,' '
call ouch
ret
hexout endp
; These routines are for direct video output. Using them allows you to
; debug video bios calls, but prevents you from single stepping
IF DIRECT
;output a character in al assumes ds=dat es=video destroys bx,ah
ouch proc near
mov bx,cursor
mov ah,color
mov es:[bx],ax
inc bx
inc bx
mov cursor,bx
ret
ouch endp
; <CR> <LF> output. assumes ds=dat es=video destroys ax,cx,dx,di clears df
crlf proc near
mov ax,cursor
mov cx,160
xor dx,dx
div cx
inc ax
mul cx
mov cursor,ax
mov cx,80
mov ah,color
mov al,' '
mov di,cursor
cld
rep stosw
ret
crlf endp
ELSE
; These are the BIOS output routines
; Output a character
ouch proc near
mov ah,0eh
mov bh,vpage
int 10h
ret
ouch endp
; <CR> <LF> output.
crlf proc near
mov al,0dh
call ouch
mov al,0ah
call ouch
ret
crlf endp
ENDIF
; Intialize the video routines
vinit proc near
mov ah,0fh
int 10h
mov vcols,ah
mov vpage,bh
cmp al,7 ; monochrome
mov ax,0b000H
jz vexit
mov ax,0b800H
vexit: mov video,ax
ret
vinit endp
; outputs string pointed to by ds:bx (ds must be dat) es= video when DIRECT=1
outstr proc near
outagn:
mov al,[bx]
or al,al
jz outout
push bx
call ouch
pop bx
inc bx
jmp outagn
outout: ret
outstr endp
; This routine is called to return to DOS
quit proc near
call _clear386
mov ax,4c00h ; Return to DOS
int 21h
quit endp
ENDIF
end
[LISTING TWO]
;***************************************************************************
;* File: BREAK386.INC *
;* Header file to include with assembly language programs using BREAK386 *
;* Williams - June, 1989 *
;***************************************************************************
IF @CodeSize ; If large style models
extrn _break386:far,_clear386:far,_setup386:far,_int1_386:far
ELSE
extrn _break386:near,_clear386:near,_setup386:near,_int1_386:far
ENDIF
; Breakpoint equates
BP_CODE EQU 0 ; CODE BREAKPOINT
BP_DATAW1 EQU 1 ; ONE BYTE DATA WRITE BREAKPOINT
BP_DATARW1 EQU 3 ; ONE BYTE DATA R/W BREAKPOINT
BP_DATAW2 EQU 5 ; TWO BYTE DATA WRITE BREAKPOINT
BP_DATARW2 EQU 7 ; TWO BYTE DATA R/W BREAKPOINT
BP_DATAW4 EQU 13 ; FOUR BYTE DATA WRITE BREAKPOINT
BP_DATARW4 EQU 15 ; FOUR BYTE DATA R/W BREAKPOINT
; Macros to turn tracing on and off
; Note: When tracing, you will actually "see" traceoff before it turns
; tracing off
traceon macro
push bp
pushf
mov bp,sp
xchg ax,[bp]
or ax,100H
xchg ax,[bp]
popf
pop bp
endm
traceoff macro
push bp
pushf
mov bp,sp
xchg ax,[bp]
and ax,0FEFFH
xchg ax,[bp]
popf
pop bp
endm
[LISTING THREE]
/***************************************************************************
* File: BREAK386.H *
* C Header for C programs using BREAK386 or CBRK386 *
* Williams - June, 1989 *
**************************************************************************/
#ifndef NO_EXT_KEYS
#define _CDECL cdecl
#else
#define _CDECL
#endif
#ifndef BR386_HEADER
#define BR386_HEADER
/* declare functions */
void _CDECL setup386(void (_CDECL interrupt far *)());
void _CDECL csetup386(void (_CDECL far *)());
void _CDECL clear386(void);
int _CDECL break386(int,int, void far *);
void _CDECL far interrupt int1_386();
/* breakpoint types */
#define BP_CODE 0 /* CODE BREAKPOINT*/
#define BP_DATAW1 1 /* ONE BYTE DATA WRITE BREAKPOINT*/
#define BP_DATARW1 3 /* ONE BYTE DATA R/W BREAKPOINT*/
#define BP_DATAW2 5 /* TWO BYTE DATA WRITE BREAKPOINT*/
#define BP_DATARW2 7 /* TWO BYTE DATA R/W BREAKPOINT*/
#define BP_DATAW4 13 /* FOUR BYTE DATA WRITE BREAKPOINT*/
#define BP_DATARW4 15 /* FOUR BYTE DATA R/W BREAKPOINT*/
#endif
[LISTING FOUR]
;***************************************************************************
;* File: DEBUG386.ASM *
;* Example assembly language program for use with BREAK386 *
;* Williams - June, 1989 *
;* Compile with: MASM /Ml DEBUG386.ASM; *
;***************************************************************************
.model large
.386
INCLUDE break386.inc
.stack 0a00H
.data
align 2 ; make sure this is word aligned
memcell dw 0 ; cell to write to
.code
main proc
;setup data segment
mov ax,@data
mov ds,ax
assume cs:@code,ds:@data
; start debugging
push seg _int1_386 ; segment of interrupt handler
push offset _int1_386 ; offset of interrupt handler
call _setup386
add sp,4 ; balance stack (like a call to C)
; set up a starting breakpoint
push seg bp1 ; segment of breakpoint
push offset bp1 ; offset of breakpoint
push BP_CODE ; breakpoint type
push 1 ; breakpoint # (1-4)
call _break386
add sp,8 ; balance the stack
push seg bp2 ; set up breakpoint #2
push offset bp2
push BP_CODE
push 2
call _break386
add sp,8
push seg bp3 ; set up breakpoint #3
push offset bp3
push BP_CODE
push 3
call _break386
add sp,8
push @data ; set up breakpoint #4 (data)
push offset memcell
push BP_DATAW2
push 4
call _break386
add sp,8
bp1:
mov cx,20 ; loop 20 times
loop1:
mov dl,cl ; print some letters
add dl,'@'
mov ah,2
bp2:
int 21h
bp3:
loop loop1 ; repeat
mov bx,offset memcell ; point bx at memory cell
mov ax,[bx] ; read cell (no breakpoint)
mov [bx],ah ; this should cause breakpoint 4
call _clear386 ; shut off debugging
mov ah,4ch
int 21h ; back to DOS
main endp
end main
[LISTING FIVE]
/***************************************************************************
* File: DBG386.C *
* Example C program using BREAK386 with the built in interrupt handler *
* Al Williams -- 15 July 1989 *
* Compile with: CL DBG386.C BREAK386 *
***************************************************************************/
#include <stdio.h>
#include <dos.h>
#include "break386.h"
int here[10];
void far *bp;
int i;
main()
{
int j;
setup386(int1_386); /* set up debugging */
bp=(void far *)&here[2]; /* make long pointer to data word */
break386(1,BP_DATAW2,bp); /* set breakpoint */
for (j=0;j<2;j++) { /* loop twice */
for (i=0;i<10;i++) /* for each element in here[] */
{
char x;
putchar(i+'0'); /* print index digit */
here[i]=i; /* assign # to array element */
}
break386(1,0,NULL); /* turn off breakpoint on 2nd pass */
}
clear386(); /* turn off debugging */
}
[LISTING SIX]
;***************************************************************************
;* File: DBGOFF.ASM *
;* Try this program if you leave a program abnormally (say, with a stack *
;* overflow). It will reset the debug register. *
;* Williams - June, 1989 *
;* Compile with: MASM DBGOFF; *
;***************************************************************************
.model small
.386P
.stack 32
.code
main proc
xor eax,eax ; clear dr7
mov dr7,eax
mov ah,4ch ; exit to DOS
int 21H
main endp
end main
[LISTING SEVEN]
;***************************************************************************
;* File: CBRK386.ASM *
;* Functions to allow breakpoint handlers to be written in C. *
;* Williams - June, 1989 *
;* Compile with: MASM /Ml CBRK386.ASM; *
;***************************************************************************
.MODEL small
.386P
public _csetup386
; Set up stack offsets for word size arguments based on the code size
; Be careful, regardless of what Microsoft's documentation says,
; you must use @CodeSize (not @codesize, etc.)
IF @CodeSize ; True for models with far code
arg1 EQU <[BP+6]>
arg2 EQU <[BP+8]>
arg3 EQU <[BP+10]>
arg4 EQU <[BP+12]>
ELSE
arg1 EQU <[BP+4]>
arg2 EQU <[BP+6]>
arg3 EQU <[BP+8]>
arg4 EQU <[BP+10]>
ENDIF
.DATA
; You may need to change the next line to expand the stack your breakpoint
; handler runs with
STACKSIZE EQU 2048
oldoffset dw 0 ; old interrupt 1 vector offset
oldsegment dw 0 ; old interrupt 1 vector segment
oldstack equ this dword
sp_save dw 0
ss_save dw 0
ds_save dw 0
es_save dw 0
ccall equ this dword ; C routine's adress is saved here
c_off dw 0
c_seg dw 0
oldstkhqq dw 0 ; Old start of stack
newsp equ this dword ; New stack address for C routine
dw offset stacktop
dw seg newstack
; Here is the new stack. DO NOT MOVE IT OUT OF DGROUP
; That is, leave it in the DATA or DATA? segment.
newstack db STACKSIZE DUP (0)
stacktop EQU $
extrn STKHQQ:word ; Microsoft heap/stack bound
.CODE
; This routine is called in place of setup386(). You pass it the address of
; a void far function that you want invoked on a breakpoint.
; It's operation is identical to setup386() except for:
;
; 1) The interrupt 1 vector is set to cint1_386() (see below)
; 2) The address passed is stored in location CCALL
; 3) DS and ES are stored in ds_save and es_save
_csetup386 proc
push bp
mov bp,sp
push es
mov ax,es
mov es_save,ax
mov ax,ds
mov ds_save,ax
mov ax,3501H
int 21h
mov ax,es
mov oldsegment,ax
mov oldoffset,bx
pop es
mov ax,arg2
push ds
mov dx,arg1
mov c_seg,ax
mov c_off,dx
mov ax,seg _cint1_386
mov ds,ax
mov dx,offset _cint1_386
mov ax,2501H
int 21H
pop ds
xor eax,eax
mov dr6,eax
pop bp
ret
_csetup386 endp
;**************************************************************************
;* *
;* Here is the interrupt handler!!! *
;* Two arguments are passed to C, a far pointer to the base of the stack *
;* frame and the complete contents of dr6 as a long unsigned int. *
;* *
;* The stack frame is as follows: *
;* *
;* . *
;* . *
;* (Interrupted code's stack) *
;* FLAGS *
;* CS *
;* IP <DDDD? *
;* AX 3 *
;* CX 3 *
;* DX 3 *
;* BX 3 *
;* SP DDDDDY (Stack pointer points to IP above) *
;* BP *
;* SI *
;* DI *
;* ES *
;* DS *
;* SS <DDDDD pointer passed to your routine points here *
;* *
;* The pointer is two way. That is, you can read the values or set any of *
;* them except SS. You should, however, refrain from changing CS,IP,or SP. *
;* *
;**************************************************************************/
_cint1_386 proc
pusha ; save registers
push es
push ds
push ss
mov ax,@data ; point at our data segment
mov ds,ax
mov ax,ss
mov ss_save,ax ; remember old stack location
mov sp_save,sp
cld
lss sp,newsp ; switch stacks
mov ax,STKHQQ ; save old end of stack
mov oldstkhqq,ax
mov ax,offset newstack ; load new end of stack
mov STKHQQ,ax
sti
mov eax,dr6 ; put DR6 on stack for C
push eax
push ss_save ; put far pointer to stack frame
push sp_save ; on new stack for C
mov ax,es_save ; restore es/ds from csetup386()
mov es,ax
mov ax,ds_save
mov ds,ax
call ccall ; call the C program
xor eax,eax ; clear DR6
mov dr6,eax
mov ax,@data
mov ds,ax ; regain access to data
lss sp,oldstack ; restore old stack
add sp,2 ; don't pop off SS
; (in case user changed it)
mov ax,oldstkhqq ; restore end of stack
mov STKHQQ,ax
pop ds
pop es
popa
; This seems complicated at first.
; You MUST insure that RF is set before continuing. If RF is not set
; you will just cause a breakpoint immediately!
; In protected mode, this is handled automatically. In real mode it
; isn't since RF is in the high 16 bits of the flags register.
; Essentially we have to convert the stack from:
;
; 16 bit Flags 32 bit flags (top word = 1 to set RF)
; 16 bit CS to -----> 32 bit CS (garbage in top 16 bits)
; 16 bit IP 32 bit IP (top word = 0)
;
; All this so we can execute an IRETD which will change RF.
sub esp,6 ; make a double stack frame
xchg ax,[esp+6] ; get ip in ax
mov [esp],ax ; store it
xor ax,ax
mov [esp+2],ax ; eip = 0000:ip
mov ax,[esp+6]
xchg ax,[esp+8] ; get cs
mov [esp+4],ax
xor ax,ax
mov [esp+6],ax
mov ax,[esp+8] ; zero that stack word & restore ax
xchg ax,[esp+10] ; get flags
mov [esp+8],ax
mov ax,1 ; set RF
xchg ax,[esp+10]
iretd ; DOUBLE IRET (32 bits!)
_cint1_386 endp
end
[LISTING EIGHT]
/***************************************************************************
* File: CBRKDEMO.C *
* Example C interrupt handler for use with CBRK386 *
* Williams - June, 1989 *
* Compile with: CL CBRKDEMO.C BREAK386 CBRK386 *
***************************************************************************/
#include <stdio.h>
#include <conio.h>
#include <ctype.h>
#include <dos.h>
#include "break386.h"
/* functions we will reference */
int loop();
void far broke();
main()
{
int i;
/* declare function broke as our interrupt handler */
csetup386(broke);
break386(1,BP_CODE,(void far *)loop); /* set break at function loop */
for (i=0;i<10;i++) loop(i);
printf("Returned to main.\n");
clear386(); /* turn off debugging */
}
/* This function has a breakpoint on its entry */
loop(int j)
{
printf("Now in loop (%d)\n",j);
}
/**************************************************************************
* *
* Here is the interrupt handler!!! *
* Note it must be a far function (normal int the LARGE, HUGE & MEDIUM *
* models). Two arguments are passed: a far pointer to the base of the stack *
* frame and the complete contents of dr6 as a long unsigned int. *
* *
* The stack frame is as follows: *
* *
* . *
* . *
* (Interrupted code's stack) *
* FLAGS *
* CS *
* IP <DDDD? *
* AX 3 *
* CX 3 *
* DX 3 *
* BX 3 *
* SP DDDDDY (Stack pointer points to IP above) *
* BP *
* SI *
* DI *
* ES *
* DS *
* SS <DDDDD pointer passed to your routine points here *
* *
* The pointer is two way. That is, you can read the values or set any of *
* them except SS. You should, however, refrain from changing CS,IP,or SP. *
* *
**************************************************************************/
void far broke(void far *p,long dr6)
{
static int breaking=1; /* don't do anything if breaking=0 */
int c;
if (breaking)
{
int n;
int far *ip;
/**************************************************************************
* *
* Here we will read the local variable off the interrupted program's stack! *
* Assuming small model, the stack above our stack frame looks like this: *
* i - variable sent to loop *
* add - address to return to main with *
* <our stack frame starts here> *
* *
* This makes i the 15th word on the stack (16th on models with far code) *
* *
**************************************************************************/
#define IOFFSET 15 /* use 16 for large, medium or huge models */
n=*((unsigned int far *)p+IOFFSET);
printf("\nBreakpoint reached! (DR6=%lX i=%d)\n",dr6,n);
/* Ask user what to do. */
do {
printf("<C>ontinue, <M>odify i, <A>bort, or <N>o breakpoint? ");
c=getche();
putch('\r');
putch('\n'); /* start a new line */
if (!c) /* function key pressed */
{
getch();
continue;
}
c=toupper(c);
/* Modify loop's copy of i (doesn't change main's i) */
if (c=='M')
{
int newi;
printf("Enter new value for i: ");
scanf("%d",&newi);
*((unsigned int far *)p+IOFFSET)=newi;
continue;
}
if (c=='A') /* Exiting */
{
clear386(); /* ALWAYS turn off debugging!!! */
exit(0);
}
if (c=='N')
breaking=0; /* We could have turned off breakpoints instead */
} while (c!='A'&&c!='N'&&c!='C');
}
}