Art of Assembly/Win32 Edition is now available. Let me read that version.
PLEASE: Before emailing me asking how to get a hard copy of this text, read this.
Important Notice: As you have probably discovered by now, I am no longer updating this document. The reason is quite simple: I'm working on a Windows version of "The Art of Assembly Language Programming". In the past I have encouraged individuals to send me corrections to this text. However, as I am no longer updating this material, don't expect those correctioins to appear in a future release. I am collecting errata that I will post to Webster someday, so feel free to continue sending corrections to AoA/DOS (16-bit) to rhyde@cs.ucr.edu. If you're more interested in leading edge material, please see the information about the Win/32 edition, above.
int (software interrupt) instruction. Indeed, different manufacturers have used terms like exceptions, faults, aborts, traps, and interrupts to describe the phenomena this chapter discusses. Unfortunately, there is no clear consensus as to the exact meaning of these terms. Different authors adopt different terms to their own use. While it is tempting to avoid the use of such misused terms altogether, for the purpose of discussion it would be nice to have a set of well defined terms we can use in this chapter. Therefore, we will pick three of the terms above, interrupts, traps, and exceptions, and define them. This chapter attempts to use the most common meanings for these terms, but don't be surprised to find other texts using them in different contexts.int instruction is the main vehicle for executing a trap. Note that traps are usually unconditional; that is, when you execute an int instruction, control always transfers to the procedure associated with the trap. Since traps execute via an explicit instruction, it is easy to determine exactly which instructions in a program will invoke a trap handling routine.
iret (interrupt return) instruction. The interrupt return pops the far return address and the flags off the stack. Note that executing a far return is insufficient since that would leave the flags on the stack.cli instruction. Conversely, if you want to allow interrupts within a hardware interrupt service routine, you must explicitly turn them back on with an sti instruction. Note that the 80x86's interrupt disable flag only affects hardware interrupts. Clearing the interrupt flag will not prevent the execution of a trap or exception.iret instruction rather than ret. Although the distance of the ISR procedure (near vs. far) is usually of no significance, you should make all ISRs far procedures. This will make programming easier if you decide to call an ISR directly rather than using the normal interrupt handling mechanism.
SimpleISR proc far
mov ax, 0
iret
SimpleISR endp
This ISR obviously does not preserve the machine state; it explicitly disturbs the value in ax and then returns from the interrupt. Suppose you were executing the following code segment when a hardware interrupt transferred control to the above ISR:
mov ax, 5
add ax, 2
; Suppose the interrupt occurs here.
puti
.
.
.
The interrupt service routine would set the ax register to zero and your program would print zero rather than the value five. Worse yet, hardware interrupts are generally asynchronous, meaning they can occur at any time and rarely do they occur at the same spot in a program. Therefore, the code sequence above would print seven most of the time; once in a great while it might print zero or two (it will print two if the interrupt occurs between the mov ax, 5 and add ax, 2 instructions). Bugs in hardware interrupt service routines are very difficult to find, because such bugs often affect the execution of unrelated code.
The solution to this problem, of course, is to make sure you preserve all registers you use in the interrupt service routine for hardware interrupts and exceptions. Since trap calls are explicit, the rules for preserving the state of the machine in such programs is identical to that for procedures.
Writing an ISR is only the first step to implementing an interrupt handler. You must also initialize the interrupt vector table entry with the address of your ISR. There are two common ways to accomplish this - store the address directly in the interrupt vector table or call DOS and let DOS do the job for you.
Storing the address yourself is an easy task. All you need to do is load a segment register with zero (since the interrupt vector table is in segment zero) and store the four byte address at the appropriate offset within that segment. The following code sequence initializes the entry for interrupt 255 with the address of the SimpleISR routine presented earlier:
mov ax, 0
mov es, ax
pushf
cli
mov word ptr es:[0ffh*4], offset SimpleISR
mov word ptr es:[0ffh*4 + 2], seg SimpleISR
popf
Note how this code turns off the interrupts while changing the interrupt vector table. This is important if you are patching a hardware interrupt vector because it wouldn't do for the interrupt to occur between the last two mov instructions above; at that point the interrupt vector is in an inconsistent state and invoking the interrupt at that point would transfer control to the offset of SimpleISR and the segment of the previous interrupt 0FFh handler. This, of course, would be a disaster. The instructions that turn off the interrupts while patching the vector are unnecessary if you are patching in the address of a trap or exception handler[2].
Perhaps a better way to initialize an interrupt vector is to use DOS' Set Interrupt Vector call. Calling DOS with ah equal to 25h provides this function. This call expects an interrupt number in the al register and the address of the interrupt service routine in ds:dx. The call to MS-DOS that would accomplish the same thing as the code above is
mov ax, 25ffh ;AH=25h, AL=0FFh.
mov dx, seg SimpleISR ;Load DS:DX with
mov ds, dx ; address of ISR
lea dx, SimpleISR
int 21h ;Call DOS
mov ax, dseg ;Restore DS so it
mov ds, ax ; points back at DSEG.
Although this code sequence is a little more complex than poking the data directly into the interrupt vector table, it is safer. Many programs monitor changes made to the interrupt vector table through DOS. If you call DOS to change an interrupt vector table entry, those programs will become aware of your changes. If you circumvent DOS, those programs may not find out that you've patched in your own interrupt and could malfunction.
Generally, it is a very bad idea to patch the interrupt vector table and not restore the original entry after your program terminates. Well behaved programs always save the previous value of an interrupt vector table entry and restore this value before termination. The following code sequences demonstrate how to do this. First, by patching the table directly:
mov ax, 0
mov es, ax
; Save the current entry in the dword variable IntVectSave:
mov ax, es:[IntNumber*4]
mov word ptr IntVectSave, ax
mov ax, es:[IntNumber*4 + 2]
mov word ptr IntVectSave+2, ax
; Patch the interrupt vector table with the address of our ISR
pushf ;Required if this is a hw interrupt.
cli ; " " " " " " "
mov word ptr es:[IntNumber*4], offset OurISR
mov word ptr es:[IntNumber*4+2], seg OurISR
popf ;Required if this is a hw interrupt.
; Okay, do whatever it is that this program is supposed to do:
.
.
.
; Restore the interrupt vector entries before quitting:
mov ax, 0
mov es, ax
pushf ;Required if this is a hw interrupt.
cli ; " " " " " "
mov ax, word ptr IntVectSave
mov es:[IntNumber*4], ax
mov ax, word ptr IntVectSave+2
mov es:[IntNumber*4 + 2], ax
popf ;Required if this is a hw interrupt.
.
.
.
If you would prefer to call DOS to save and restore the interrupt vector table entries, you can obtain the address of an existing interrupt table entry using the DOS Get Interrupt Vector call. This call, with ah=35h, expects the interrupt number in al; it returns the existing vector for that interrupt in the es:bx registers. Sample code that preserves the interrupt vector using DOS is
; Save the current entry in the dword variable IntVectSave:
mov ax, 3500h + IntNumber ;AH=35h, AL=Int #.
int 21h
mov word ptr IntVectSave, bx
mov word ptr IntVectSave+2, es
; Patch the interrupt vector table with the address of our ISR
mov dx, seg OurISR
mov ds, dx
lea dx, OurISR
mov ax, 2500h + IntNumber ;AH=25, AL=Int #.
int 21h
; Okay, do whatever it is that this program is supposed to do:
.
.
.
; Restore the interrupt vector entries before quitting:
lds bx, IntVectSave
mov ax, 2500h+IntNumber ;AH=25, AL=Int #.
int 21h
.
.
.
int (software interrupt) instruction[3]. There are only two primary differences between a trap and an arbitrary far procedure call: the instruction you use to call the routine (int vs. call) and the fact that a trap pushes the flags on the stack so you must use the iret instruction to return from it. Otherwise, there really is no difference between a trap handler's code and the body of a typical far procedure.int 21h instruction is an example of a trap invocation. Your programs do not have to know the actual memory address of DOS' entry point to call DOS. Instead, DOS patches the interrupt 21h vector when it loads into memory. When you execute int 21h, the 80x86 automatically transfers control to DOS' entry point, whereever in memory that happens to be.int instruction.ah register. A typical trap handler would execute a case statement on the value in the ah register and transfer control to the appropriate handler function.into instruction in this category. This is an exception to this rule.