Dec 282017
Description of IBM's interrrupt sharing protocol.
File INTPRO.ZIP from The Programmer’s Corner in
Category Tutorials + Patches
Description of IBM’s interrrupt sharing protocol.
File Name File Size Zip Size Zip Type
INTPROTO.TXT 20731 6641 deflated

Download File INTPRO.ZIP Here

Contents of the INTPROTO.TXT file

by Chris Dunford

In the PS/2 BIOS Interface Technical Reference, IBM has suggested a
protocol for the sharing of system interrupts. Although the protocol
was intended to allow sharing of hardware interrupts, it is equally
usable for software interrupts.

One of the features of the interrupt sharing protocol is that it permits
a resident program to "unhook" itself from an interrupt even if it is
not the first interrupt handler in the chain of handlers. The benefit
of this should be immediately apparent to developers of TSRs. It is a
commonplace in TSR manuals to see verbiage along these lines:

Program X can be unloaded from memory by typing ... at the DOS
prompt. For this to work, however, X must be the last TSR loaded.
If other resident software is loaded after X, you cannot unload X.

The interrupt sharing protocol eliminates this restriction.

However, for the protocol to work, it must be followed by the majority
of TSR writers. To date, this has not occurred. Because the protocol
is easy to implement and inexpensive in terms of memory, I feel that at
least part of the reason for this must be that the protocol has not been
widely publicized; most DOS programmers are simply unaware of it. I am
offering this document as a modest attempt to let DOS programmers know
that a solution exists for a longstanding problem.

Let me add as a caveat that I do not have and have not examined the
primary source for this information--the PS/2 BIOS reference. Most of
the information in this paper was gleaned from other sources, augmented
by my own experiences in writing TSRs. The most recent writeup as of
this date (August 6, 1991) was in the 7/91 issue of the Microsoft
Systems Journal.

This document is not copyrighted. Its distribution in any form is
encouraged (please try to avoid making a profit on it). If you make
changes, please make sure they are clearly marked so that I get blame
only for my own errors and credit only where it is due. Please try to
get any changes, amplifications, corrections, etc., back to me so that a
clean copy can be redistributed if necessary.

The perpetrator of this document is:

Chris Dunford
The Cove Software Group
PO Box 1072
Columbia, MD 21044

CompuServe: 76703,2002
Internet: [email protected]

The vast majority of terminate-and-stay-resident (TSR) programs need to
intercept one or more hardware or software interrupts. Shown below is
typical code for accomplishing this, assuming that the interrupt to be
"hooked" is the DOS service interrupt (INT 21h):

OldInt21 dd ?

(installation code)
; Save current INT 21h vector
mov ax,3521h
int 21h
mov word ptr OldInt21,bx
mov word ptr OldInt21+2,es

; Set new INT 21h vector
mov dx,offset NewInt21
mov ax,2521h
int 21h

(interrupt handler)
(perform processing as required)
jmp OldInt21

The installation code saves the current contents of the INT 21h vector
in a 32-bit variable called OldInt21, which is known as a "downward
link" or "downlink" because it provides a link "downward" in the chain
of interrupt handlers. The installation then sets the INT 21h vector to
point to its own interrupt handler at NewInt21. When an INT 21h call is
subsequently issued, execution is routed to NewInt21, which performs
whatever processing it needs to do. It then executes a far jump to the
address in OldInt21, allowing previously installed TSRs (and DOS, of
course) to do their work. The flow of control looks like this if only
one TSR is loaded:

vector OldInt21
application (int 21h) -----------> TSR ----------> DOS

To unload itself, the TSR simply resets the INT 21h vector to its
initial contents (i.e., the address in OldInt21). The TSR's interrupt
handler is now no longer in the chain, and the TSR can be safely

application (int 21h) -----------> DOS

This scheme works fairly well until the situation arises where more than
one program tries to hook the same vector:

vector OldInt21A OldInt21B
app -----------> TSR A ---------> TSR B ---------> DOS

This also works fine--until TSR B wants to unload itself. B cannot
follow its normal procedure and replace the INT 21h vector with the
contents of its OldInt21. If it did, the interrupt chain would look
like this:

app -----------> DOS

The problem, obviously, is that TSR A has been unceremoniously removed
from the interrupt chain; it has been disabled without notice, even
though it remains in memory. This is obviously an unsatisfactory--and
quite possibly dangerous--situation.

Nor can TSR B simply unload itself without fixing INT 21h. TSR A would
still have TSR B's address stored in its OldInt21; when it has completed
its processing of an INT 21h call, it will jump to the address where TSR
B was at one time--but is no longer--loaded. The only unpredictable
aspect of the result is which kind of reboot (hard or soft) will be

TSR B's only option is to wave its hands and notify the user that it
cannot be unloaded. This is satisfying to the programmer ("We told you
that you can't do this") but not to the user.

The root of the problem is that TSR A has TSR B's address, but not vice
versa. If TSR B knew where TSR A was keeping its (B's) address, the
resolution would be simple: B could simply copy its downlink into A's

Here is a hypothetical memory map:

0000:0084 INT21h vector = 1200:0240 --+
---------------------- |
---------------------- |
1200:0240 NewInt21 (int handler) <----+
1200:0642 OldInt21 = 1000:0296 ---+
---------------------- |
---------------------- |
1000:0296 NewInt21 (int handler) <--+
1000:0415 OldInt21 = 0070:1234 ---+
---------------------- |
---------------------- |
0070:1234 INT 21h entry point <--+

The vector table entry for INT 21h points to TSR A's interrupt handler
at 1200:0240. TSR A's OldInt21 contains TSR B's interrupt handler
address (1000:0296); when A has completed its work, it jumps to B at
that address. B has DOS's address (0070:1234) in its OldInt21; when it
has finished, it jumps to DOS's address.

To take itself out of the chain, all B would have to do would be to put
its downward link (DOS's address, contained in B's OldInt21) into A's
downward link (which currently contains B's address):

0000:0084 INT21h vector = 1200:0240 --+
---------------------- |
---------------------- |
1200:0240 NewInt21 (int handler) <----+
1200:0642 OldInt21 = 0070:1234 ---+ <=== change made here
---------------------- |
---------------------- |
1000:0296 NewInt21 (int handler) |
... |
1000:0415 OldInt21 = 0070:1234 |
---------------------- |
---------------------- |
0070:1234 INT 21h entry point <--+

B is now removed from the chain; it could be safely unloaded, and A will
remain active. The problem, as mentioned, is that B doesn't know the
address of A's OldInt21, so it can't make the correction.

The solution offered by the interrupt sharing protocol is simplicity
itself: it requires that the downward link pointer be kept at a
specific offset from the interrupt handler entry point. The entry
point for the first handler in a chain can be found in the
interrupt vector table; in this manner, a chain can be traced from
first handler to last.

The offset of the downward link from the entry point turns out to be 2.
Thus, since TSR A's entry point (found in the vector table) is
1200:0240, his downlink must be located at 1200:0242. The map would
look like if both programs followed the protocol:

0000:0084 INT21h vector = 1200:0240 --+
---------------------- |
---------------------- |
1200:0240 NewInt21 (int handler) <----+
1200:0242 OldInt21 = 1000:0296 ---+
... |
---------------------- |
---------------------- |
1000:0296 NewInt21 (int handler) <--+
1000:0298 OldInt21 = 0070:1234 ---+
... |
---------------------- |
---------------------- |
0070:1234 INT 21h entry point <--+

The difference between this and the first map shown is that the
addresses of the downlinks can be determined by any external program:
they are no longer private to each TSR.

Program B can find A's downlink by simply following the chain (starting
at the address contained in the INT 21h vector), examining the downward
links until the link that points to B's interrupt handler is found.
These links will always be located at the handler entry point + 2. When
B finds a link that points to his handler (at 1000:0296), he has found
what he needs.

The remainder of this document fills in the necessary details for
implentation of the protocol.

The protocol requires that you use a small (18-byte) block of mixed
code and data at your interrupt handler's entry point. When you take
over an interrupt, you save the current vector in a specific location
within this block and then set the vector to point to the start of the
block. The first item in the block is a short jump to your interrupt

The block looks like this:

intercept: jmp short int_handler
prevhndlr dd 0
signature dw 424Bh
flag db 0
jmp short hwreset
db 7 dup (0) ; Reserved
; your interrupt handler starts here...

'intercept' is the address you'll use when you do a SETVEC to intercept
the interrupt vector, i.e., the entry point for your interrupt handler.

'prevhndlr' is set to contain the initial contents of the interrupt
vector at the time you take over. (This is what we were calling
OldInt21 in the previous sections.)

'signature' must contain 424Bh ("KB") and is used to help identify one
of these blocks.

'flag' is important only if you're taking over a hardware interrupt
that requires an EOI (say, INT 8 or INT 9). For software interrupts
(INT 16 or INT 21, e.g.), leave it 0. For hardware interrupts, the
first installed handler should set the flag to 80h. Only the handler
whose flag is 80h is allowed to issue EOI.

The 'jmp short hwreset' is pretty much irrelevant to anything software
people will use; it's primarily for hardware manufacturers (allows
them to specify code to be executed to reinitialize the hardware on a
system reset). However, you must be prepared for the eventuality that
this entry point will be called by someone; do this by simply defining
an HWRESET label with a RETF:

hwreset: retf

Finally, leave the seven reserved bytes as zeroes.

To install into the interrupt chain, simply (a) save the address of
the current interrupt handler in PREVHNDLR, and (b) set the interrupt
vector to point to INTERCEPT. Your handler is now first in the chain.
This is no different from what you're probably doing now. (NOTE: the
code shown here assumes that CS contains the segment of the interrupt

; Save current vector
mov al,interrupt number
mov ah,35h
int 21h ; Current vector in ES:BX
mov word ptr cs:prevhndlr,bx
mov word ptr cs:prevhndlr+2,es

; Set new vector
mov ax,cs
mov ds,ax
mov dx,offset intercept ; DS:DX -> new intercept
mov ah,25H
mov al,interrupt number
int 21h

Your interrupt handler, which begins at the label INT_HANDLER, performs
its duties as required. When you are done processing, check the
contents of PREVHNDLR; if it is nonzero (the usual case), chain to the
previous handler by executing a long jump to the address contained in
PREVHNDLR. Otherwise, just IRET. Sample code:

(do your thing, preserving regs as necessary)
push ax ; Is PREVHNDLR 0:0?
mov ax,word ptr cs:prevhndlr
or ax,word ptr cs:prevhndlr+2
pop ax
jz all_done ; Yes, PREVHNDLR is 0:0, do IRET
jmp cs:prevhndlr ; No, chain to next handler
; if you are a hardware handler with flags=80h, do EOI here

hwreset: retf ; Don't forget this!

It is strongly recommended that the INT_HANDLER label immediately
follow the end of the protocol block, even though this is not required
by the protocol. Some programs may assume this to be the case and
look for a specific jump distance at offset 1 of the block when
attempting to identify whether or not this is a valid block. In other
words, they will look for the first item in the block to be a JMP
SHORT $+18.

It is critical that you chain to the previous handler using the address
stored in PREVHNDLR. Do not store the address elsewhere and use that
for chaining. The reason for this is simple: as discussed in the
introductory sections, one of the main features of the protocol is that
other programs are allowed to find and mess with the contents of your
PREVHNDLR. In particular, the handler whose address is in your
PREVHNDLR may take himself out of the chain by replacing what's in your
PREVHNDLR with what's in his PREVHNDLR.

To recap the introductory sections, suppose you are program C and the
chain currently looks like this:

vector -> C -> B -> A

You have program B's address in your PREVHNDLR. B has program A's
address in his PREVHNDLR. B can remove himself from the chain by
putting A's address in *YOUR* PREVHNDLR:

vector -> C -> A

This will not work if you store B's address somewhere else--B must
know where his address is stored in YOUR code. There's an example of
this in DISCONNECTING, below.

To "walk" an interrupt handler chain, get the current vector:

mov al,interrupt number
mov ah,35h
int 21h ; ES:BX has current

Check to see whether ES:BX points to a valid entry structure. Look

byte ptr ES:[BX] = 0EBh (jmp short)
word ptr ES:[BX+6] = 424Bh (signature)
byte ptr ES:[BX+9] = 0EBh (another jmp short)

If all of these match, odds are real good that this is a valid
structure implementing the protocol: there is a protocol-aware
interrupt handler at ES:BX. The address of the previous handler is at
ES:[BX+2], so you can find the previous one via

les bx,es:[bx+2]

You can continue in this fashion until either (a) ES:BX is zero, or
(b) ES:BX doesn't point to a valid structure (meaning someone isn't
cooperating or you've reached a pointer into DOS or BIOS). See the
next section for a more complete code example.

To remove yourself from the chain, simply walk the chain as above
until either (a) ES:BX points to your own structure, (b) ES:BX points
to a structure whose PREVHNDLR field points to your structure, or (c)
ES:BX does not point to a valid structure.

In case (a), you are the last handler registered, and you can simply
reset the interrupt vector to point to the previous handler (the one
whose address is in your PREVHNDLR field).

In case (b), someone has registered after you, but you can take
yourself out of the chain by replacing his PREVHNDLR with what you
have stored in your own.

In case (c), you cannot safely unload. A non-protocol handler has
broken the chain.

Coding this is not difficult at all, nor does it use much memory. An
example follows (with some pseudocode to save space). Assume the
existence of a check_valid_structure subroutine that returns carry set
if ES:BX does not point to a valid protocol structure:

; Get address of first handler (last loaded)
mov al,interrupt number
mov ah,35h
int 21h ; First handler at ES:BX

; Are we the first handler (case A)?
if (es = seg INTERCEPT) and (bx = offset INTERCEPT) then
; Yes, we are the first handler, just reset the
; vector to point to the previous handler
lds dx,cs:prevhndlr ; DS:DX -> previous handler
mov al,(interrupt number)
mov ah,25h
int 21h
jmp unload ; Now safe to unload

; No, walk the chain until case B or C occurs
call check_valid_structure ; ES:BX -> protocol structure?
jc chain_busted ; No, chain is broken (case C)
lds dx,dword ptr es:[bx+2] ; DS:DX = his PREVHNDLR
if (ds = seg INTERCEPT) and (dx = offset INTERCEPT) then
; He points to us (case B). Set his PREVHNDLR
; to contents of our PREVHNDLR. This takes us
; out of the interrupt service chain.
lds dx,cs:prevhndlr ; DS:DX -> handler before us
mov es:[bx+2],dx ; Beam us up...
mov es:[bx+4],ds
jmp unload ; Now safe to unload

; ES:BX handler does not point to us, work backward
les bx,es:[bx+2]
jmp L1

; If we get here, we cannot unload safely
; Notify user and exit

; Here it is safe to unload

As you can see, the code is reasonably short and sweet. It would be
sensible to implement much of this as subroutines, especially for
those TSRs that intercept more than one interrupt. In general, you
should check all of the vectors you intercept for "disconnectability"
before disconnecting any of them. This implies the existence of a
"chain walking" subroutine and a disconnecting subroutine, both of
these being generalized versions of the code shown above.

Note that the protocol allows you to install yourself as other than the
first interrupt handler, and even to re-order a chain. If for any
reason you don't want to be first, walk the chain and insert yourself
wherever you want by copying someone else's PREVHNDLR into your own,
then putting your address into his. You are now inserted into the chain
just after him.

The code samples given above are generic and are not copied from
working code from my own software. There may be errors.

Chris Dunford 8/6/91

 December 28, 2017  Add comments

Leave a Reply