[Chaper Seventeen][Previous]
[Next] [Art of
Assembly][Randall Hyde]
Art of Assembly: Chaper Seventeen
- 17.3 - Exceptions
- 17.3.1 - Divide Error Exception (INT 0)
- 17.3.2 - Single Step (Trace) Exception
(INT 1)
- 17.3.3 - Breakpoint Exception (INT 3)
- 17.3.4 - Overflow Exception (INT 4/INTO)
- 17.3.5 - Bounds Exception (INT 5/BOUND)
- 17.3.6 - Invalid Opcode Exception (INT
6)
- 17.3.7 - Coprocessor Not Available (INT
7)
17.3 Exceptions
Exceptions occur (are raised) when an abnormal condition occurs during
execution. There are fewer than eight possible exceptions on machines running
in real mode. Protected mode execution provides many others, but we will
not consider those here, we will only consider those exceptions interesting
to those working in real mode[4].
Although exception handlers are user defined, the 80x86 hardware defines
the exceptions that can occur. The 80x86 also assigns a fixed interrupt
number to each of the exceptions. The following sections describe each of
these exceptions in detail.
In general, an exception handler should preserve all registers. However,
there are several special cases where you may want to tweak a register value
before returning. For example, if you get a bounds violation, you may want
to modify the value in the register specified by the bound
instruction before returning. Nevertheless, you should not arbitrarily modify
registers in an exception handling routine unless you intend to immediately
abort the execution of your program.
17.3.1 Divide Error Exception (INT 0)
This exception occurs whenever you attempt to divide a value by zero
or the quotient does not fit in the destination register when using the
div
or idiv
instructions. Note that the FPU's
fdiv and fdivr instructions do not raise this exception.
MS-DOS provides a generic divide exception handler that prints a message
like "divide error" and returns control to MS-DOS. If you want
to handle division errors yourself, you must write your own exception handler
and patch the address of this routine into location 0:0.
On 8086, 8088, 80186, and 80188 processors, the return address on the stack
points at the next instruction after the divide instruction. On the 80286
and later processors, the return address points at the beginning of the
divide instruction (include any prefix bytes that appear). When a divide
exception occurs, the 80x86 registers are unmodified; that is, they contain
the values they held when the 80x86 first executed the div
or idiv
instruction.
When a divide exception occurs, there are three reasonable things you can
attempt: abort the program (the easy way out), jump to a section of code
that attempts to continue program execution in view of the error (e.g.,
as the user to reenter a value), or attempt to figure out why the error
occurred, correct it, and reexecute the division instruction. Few people
choose this last alternative because it is so difficult.
17.3.2 Single Step (Trace) Exception (INT 1)
The single step exception occurs after every instruction if the trace
bit in the flags register is equal to one. Debuggers and other programs
will often set this flag so they can trace the execution of a program.
When this exception occurs, the return address on the stack is the address
of the next instruction to execute. The trap handler can decode this opcode
and decide how to proceed. Most debuggers use the trace exception to check
for watchpoints and other events that change dynamically during program
execution. Debuggers that use the trace exception for single stepping often
disassemble the next instruction using the return address on the stack as
a pointer to that instruction's opcode bytes.
Generally, a single step exception handler should preserve all 80x86 registers
and other state information. However, you will see an interesting use of
the trace exception later in this text where we will purposely modify register
values to make one instruction behave like another.
Interrupt one is also shared by the debugging exceptions capabilities of
80386 and later processors. These processors provide on-chip support via
debugging registers. If some condition occurs that matches a value in one
of the debugging registers, the 80386 and later CPUs will generate a debugging
exception that uses interrupt vector one.
17.3.3 Breakpoint Exception (INT 3)
The breakpoint exception is actually a trap, not an exception. It occurs
when the CPU executes an int 3
instruction. However, we will
consider it an exception since programmers rarely put int 3
instructions directly into their programs. Instead, a debugger like Codeview
often manages the placement and removal of int 3
instructions.
When the 80x86 calls a breakpoint exception handling routine, the return
address on the stack is the address of the next instruction after the breakpoint
opcode. Note, however, that there are actually two int
instructions
that transfer control through this vector. Generally, though, it is the
one-byte int 3
instruction whose opcode is 0cch; otherwise
it is the two byte equivalent: 0cdh, 03h.
17.3.4 Overflow Exception (INT 4/INTO)
The overflow exception, like int 3
, is technically a trap.
The CPU only raises this exception when you execute an into
instruction and the overflow flag is set. If the overflow flag is clear,
the into
instruction is effectively a nop
, if
the overflow flag is set, into
behaves like an int 4
instruction.
Programmers can insert an into
instruction after an
integer computation to check for an arithmetic overflow. Using into
is equivalent to the following code sequence:
<< Some integer arithmetic code ยป
jno GoodCode
int 4
GoodCode:
One big advantage to the into
instruction is that it does not
flush the pipeline or prefetch queue if the overflow flag is not set. Therefore,
using the into
instruction is a good technique if you provide
a single overflow handler (that is, you don't have some special code for
each sequence where an overflow could occur).
The return address on the stack is the address of the next instruction after
into
. Generally, an overflow handler does not return to that
address. Instead, it will usually abort the program or pop the return address
and flags off the stack and attempt the computation in a different way.
17.3.5 Bounds Exception (INT 5/BOUND)
Like into
, the bound
instruction will cause
a conditional exception. If the specified register is outside the specified
bounds, the bound
instruction is equivalent to an int
5
instruction; if the register is within the specified bounds, the
bound
instruction is effectively a nop.
The return address that bound
pushes is the address of the
bound
instruction itself, not the instruction following bound
.
If you return from the exception without modifying the value in the register
(or adjusting the bounds), you will generate an infinite loop because the
code will reexecute the bound
instruction and repeat this process
over and over again.
One sneaky trick with the bound
instruction is to generate
a global minimum and maximum for an array of signed integers. The following
code demonstrates how you can do this:
; This program demonstrates how to compute the minimum and maximum values
; for an array of signed integers using the bound instruction
.xlist
.286
include stdlib.a
includelib stdlib.lib
.list
dseg segment para public 'data'
; The following two values contain the bounds for the BOUND instruction.
LowerBound word ?
UpperBound word ?
; Save the INT 5 address here:
OldInt5 dword ?
; Here is the array we want to compute the minimum and maximum for:
Array word 1, 2, -5, 345, -26, 23, 200, 35, -100, 20, 45
word 62, -30, -1, 21, 85, 400, -265, 3, 74, 24, -2
word 1024, -7, 1000, 100, -1000, 29, 78, -87, 60
ArraySize = ($-Array)/2
dseg ends
cseg segment para public 'code'
assume cs:cseg, ds:dseg
; Our interrupt 5 ISR. It compares the value in AX with the upper and
; lower bounds and stores AX in one of them (we know AX is out of range
; by virtue of the fact that we are in this ISR).
;
; Note: in this particular case, we know that DS points at dseg, so this
; ISR will get cheap and not bother reloading it.
;
; Warning: This code does not handle the conflict between bound/int5 and
; the print screen key. Pressing prtsc while executing this code may
; produce incorrect results (see the text).
BoundISR proc near
cmp ax, LowerBound
jl NewLower
; Must be an upper bound violation.
mov UpperBound, ax
iret
NewLower: mov LowerBound, ax
iret
BoundISR endp
Main proc
mov ax, dseg
mov ds, ax
meminit
; Begin by patching in the address of our ISR into int 5's vector.
mov ax, 0
mov es, ax
mov ax, es:[5*4]
mov word ptr OldInt5, ax
mov ax, es:[5*4 + 2]
mov word ptr OldInt5+2, ax
mov word ptr es:[5*4], offset BoundISR
mov es:[5*4 + 2], cs
; Okay, process the array elements. Begin by initializing the upper
; and lower bounds values with the first element of the array.
mov ax, Array
mov LowerBound, ax
mov UpperBound, ax
; Now process each element of the array:
mov bx, 2 ;Start with second element.
mov cx, ArraySize
GetMinMax: mov ax, Array[bx]
bound ax, LowerBound
add bx, 2 ;Move on to next element.
loop GetMinMax ;Repeat for each element.
printf
byte "The minimum value is %d\n"
byte "The maximum value is %d\n",0
dword LowerBound, UpperBound
; Okay, restore the interrupt vector:
mov ax, 0
mov es, ax
mov ax, word ptr OldInt5
mov es:[5*4], ax
mov ax, word ptr OldInt5+2
mov es:[5*4+2], ax
Quit: ExitPgm ;DOS macro to quit program.
Main endp
cseg ends
sseg segment para stack 'stack'
stk db 1024 dup ("stack ")
sseg ends
zzzzzzseg segment para public 'zzzzzz'
LastBytes db 16 dup (?)
zzzzzzseg
ends
end Main
If the array is large and the values appearing in the array are relatively
random, this code demonstrates a fast way to determine the minimum and maximum
values in the array. The alternative, comparing each element against the
upper and lower bounds and storing the value if outside the range, is generally
a slower approach. True, if the bound
instruction causes a
trap, this is much slower than the compare and store method. However, it
a large array with random values, the bounds violation will rarely occur.
Most of the time the bound
instruction will execute in 7-13
clock cycles and it will not flush the pipeline or the prefetch queue[5].
Warning: IBM, in their infinite wisdom, decided to use int 5
as the print screen operation. The default int 5
handler will
dump the current contents of the screen to the printer. This has two implications
for those who would like to use the bound
instruction in their
programs. First, if you do not install your own int 5
handler
and you execute a bound
instruction that generates a bound
exception, you will cause the machine to print the contents of the screen.
Second, if you press the PrtSc key with your int 5
handler
installed, BIOS will invoke your handler. The former case is a programming
error, but this latter case means you have to make your bounds exception
handler a little smarter. It should look at the byte pointed at by the return
address. If this is an int 5
instruction opcode (0cdh), then
you need to call the original int 5
handler, or simply return
from interrupt (do you want them pressing the PrtSc key at that point?).
If it is not an int 5
opcode, then this exception was probably
raised by the bound
instruction. Note that when executing a
bound
instruction the return address may not be pointing directly
at a bound
opcode (0c2h). It may be pointing at a prefix byte
to the bound
instruction (e.g., segment, addressing mode, or
size override). Therefore, it is best to check for the int 5
opcode.
17.3.6 Invalid Opcode Exception (INT 6)
The 80286 and later processors raise this exception if you attempt to
execute an opcode that does not correspond to a legal 80x86 instruction.
These processors also raise this exception if you attempt to execute a bound,
lds, les, lidt, or other instruction that requires a memory operand but
you specify a register operand in the mod/rm field of the mod/reg/rm byte.
The return address on the stack points at the illegal opcode. By examining
this opcode, you can extend the instruction set of the 80x86. For example,
you could run 80486 code on an 80386 processor by providing subroutines
that mimic the extra 80486 instructions (like bswap
, cmpxchg
,
etc.).
17.3.7 Coprocessor Not Available (INT 7)
The 80286 and later processors raise this exception if you attempt to
execute an FPU (or other coprocessor) instruction without having the coprocessor
installed. You can use this exception to simulate the coprocessor in software.
On entry to the exception handler, the return address points at the coprocessor
opcode that generated the exception.
[4] For more details on exceptions in protected
mode, see the bibliography.
[5] Note that on
the 80486 and later processors, the bound instruction may actually be slower
than the corresponding straight line code.
- 17.3 - Exceptions
- 17.3.1 - Divide Error Exception (INT 0)
- 17.3.2 - Single Step (Trace) Exception
(INT 1)
- 17.3.3 - Breakpoint Exception (INT 3)
- 17.3.4 - Overflow Exception (INT 4/INTO)
- 17.3.5 - Bounds Exception (INT 5/BOUND)
- 17.3.6 - Invalid Opcode Exception (INT
6)
- 17.3.7 - Coprocessor Not Available (INT
7)
Art of Assembly: Chaper Seventeen - 29 SEP 1996
[Chaper Seventeen][Previous] [Next]
[Art of Assembly][Randall
Hyde]