CSE240

Interrupts


What are interrupts?

There are two definitions of an interrupt as used with the 80x86 processors:
  1. 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
  2. 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:
  1. push the FAR return address on the stack
  2. push the flags register on the stack
  3. 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 ?
  1. 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

  2. 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