CSE240
Interrupts
What are interrupts?
There are two definitions of an interrupt as used with the 80x86
processors:
- it is an event which takes place which needs to be taken care of
immediately, such as a key being pressed (the key is then placed into
a buffer) or division by zero (an error message is issued and the
program is aborted), and
- it is a subroutine which can be called as part of the normal operation
of a program (this is known as an interrupt service routine, or ISR).
It is similar to a procedure accessed by the CALL statement, but its
address is not known until runtime.
Interrupts are accessed through a table located in low memory. The
table is a list of addresses of where the ISR is located in memory,
of the form SEGMENT:OFFSET (the addresses are FAR pointers).
This way, if the address of an ISR changes, only the table needs to
be updated. Any programs which use the ISR will then use this changed
address. This table is often called a vector table, and the
addresses in the table are called vectors. Changing the address
of an ISR is then logically called revectoring.
Obtaining an interrupt vector
The following code will place the ISR's segment value in ES
and the offset value in BX:
; place ISR number to get in AL
mov ah, 35h
int 21h
; segment is in ES, offset is in BX
Changing an interrupt vector
The following code changes an interrupt vector to the FAR address that
it finds in DS:DX:
; place new segment value in DS
; place new offset value in DX
; place ISR number to change in AL
mov ah, 25h
int 21h
When a program revectors an interrupt, it is said to have "hooked the
interrupt", or sometimes "hooked the vector".
Although it is possible to modify the vector table manually, it is
recommended that you always use the interrupt 21h functions 25h and
35h instead.
Calling an ISR
The INT instruction is used to call an interrupt. The parameter given
to INT is the interrupt number. When the INT instruction executes,
it does the following:
- push the FAR return address on the stack
- push the flags register on the stack
- load CS and IP with the address found in the vector table
(at this point execution resumes at the new address)
When the ISR finishes, it executes the instruction IRET, which is
similar to RET but it also restores the flags register from the stack.
Creating an ISR
The only component an ISR must have is an IRET instruction. There are,
though, a few rules of thumb regarding the design of an ISR. First, it
should not take very long to execute, since it will most likely be
preempting a longer-running task. The preemptted task would run the
risk of not being able to finish in a timely manner. Second, the number
of ISRs available is essentially reduced from within an ISR. Generally,
DOS interrupt calls (such as INT 21h) should not be called from within
an ISR. There are exceptions to this rule, as will be discussed later.
A code example of creating and revectoring an ISR is given below.
The clock interrupt
(Interrupt 1Ch)
The clock interrupt is called automatically by DOS 18.2 times per second.
This interrupt does nothing by default and is designed to be revectored.
Whatever ISR this interrupt points to is then called 18.2 times per second.
TSR (terminate and stay resident) interrupt
(Interrupt 31h, function 21h)
This interrupt is intended to be called, not revectored. In order for
a TSR program to be useful, it must hook one or more interrupts. When
the TSR interrupt is called from within a program, the program exits
(almost) normally, leaving part of itself in memory. The part that
is left in memory consists of those procedures to which the hooked vectors
point. If a vector were hooked, but the procedure to which it points
were unloaded from memory, the vector would point to garbage.
An error code can be returned to the calling procedure (which is most
often DOS itself) in AL.
cseg segment word public 'CODE'
assume cs:cseg
TSR_PROC proc far
pushf
push cx
mov cx, 0FFFFh ; number of iterations
count:
nop ; body of loop: do nothing
loop count
pop cx
popf
iret
TSR_PROC endp
start:
lea dx, TSR_PROC ; point to new ISR procedure
mov ax, segment TSR_PROC
mov ds, ax
mov ah, 25h ; function to revector an interrupt
mov al, 1Ch ; interrupt to revector
int 21h ; revector it
sti ; make sure interrupts are enabled!
mov dx, cseg
;[sub dx, dseg] ; we don't have a data segment here
add dx, 10h ; want to save 10h paragraphs from PSP
; compute number of paragraphs to save:
add dx, ((start - TSR_PROC) + 15) / 16
mov ax, 3100h ; function to T&SR
int 21h
cseg ends
stack segment stack 'STACK'
dw 100h dup (?)
stack ends
end start ; where to start execution
When this program terminates, it leaves a certain number of paragraphs
in memory. Since the TSR_PROC procedure is probably not an integral
multiple of 16 bytes, the entire procedure plus some extra bytes from
the beginning of the main program are left in memory.
Chaining interrupt vectors
When revectoring any interrupt, it is important to note that a program
may have already revectored it (there can be multiple TSR programs
already in memory, each having its own vector hooks). When revectoring
an interrupt, it is good practice (and often mandatory) to save the current
vector, and within the new ISR to call the old ISR. There are two ways to
accomplish this, each used for a different reason. With either method, it
is required to have the old vector address saved in memory in the following
format:
old_vector label dword
old_offset dw ?
old_segment dw ?
- Jumping to the old address
The JMP instruction can be used to jump directly to the old ISR
address. If it is known that the called (old) ISR does not need to
return to the current ISR, execution can be transferred directly to the
old ISR. When the old ISR executes its IRET instruction, it would be as
though an IRET instruction in the new ISR were executed, and execution
returns to the interrupted program.
E.g.: jmp old_vector
- Calling the old ISR
The old ISR can be called by a simulated INT instruction. It
must be simulated, because executing the INT instruction for this
interrupt number would send execution back to the beginning of the new
ISR (and so on infinitely, crashing the machine), because it's been
revectored! The way to accomplish this is to push the flags register on
the stack and to call the old address. The flags register needs to be
pushed first because the old ISR executes an IRET instruction, not a RET
instruction. The IRET instruction then causes execution to return to the
current procedure, which can then continue.
E.g.:
pushf
cli
call old_vector
sti
The CLI instruction switches off the maskable interrupts. This reduces
the likelihood of the new ISR from inadvertently being called recursively.
IMPORTANT!
When an interrupt service routine that is part of a TSR program is
called, it is important to remember that the DS, SS, and ES registers
point to segments in the interrupted program's memory areas.
That means that in order to access variables in the TSR program's data
segment, the DS register will need to be changed (don't forget to save
the old value first, and to restore it later).
Back to CSE240 main page
Copyright © 1997 Jeffrey A. Meunier