home *** CD-ROM | disk | FTP | other *** search
-
-
-
-
-
-
- IBM'S INTERRUPT-SHARING PROTOCOL
- by Chris Dunford
- 8/6/91
-
- 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
- 301/992-9371
-
- CompuServe: 76703,2002
- Internet: 76703.2002@compuserve.com
-
-
- THE PROBLEM
- -----------
- 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):
-
- (data)
- 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)
- NewInt21:
- (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
- unloaded:
-
- vector
- 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:
-
- vector
- 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
- required.
-
- 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
- downlink.
-
- Here is a hypothetical memory map:
-
- ---------------------
- VECTOR TABLE
- 0000:0084 INT21h vector = 1200:0240 --+
- ---------------------- |
- |
- ---------------------- |
- TSR A |
- 1200:0240 NewInt21 (int handler) <----+
- ...
- 1200:0642 OldInt21 = 1000:0296 ---+
- ---------------------- |
- |
- ---------------------- |
- TSR B |
- 1000:0296 NewInt21 (int handler) <--+
- ...
- 1000:0415 OldInt21 = 0070:1234 ---+
- ---------------------- |
- |
- ---------------------- |
- DOS |
- 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):
-
- ---------------------
- VECTOR TABLE
- 0000:0084 INT21h vector = 1200:0240 --+
- ---------------------- |
- |
- ---------------------- |
- TSR A |
- 1200:0240 NewInt21 (int handler) <----+
- ...
- 1200:0642 OldInt21 = 0070:1234 ---+ <=== change made here
- ---------------------- |
- |
- ---------------------- |
- TSR B |
- 1000:0296 NewInt21 (int handler) |
- ... |
- 1000:0415 OldInt21 = 0070:1234 |
- ---------------------- |
- |
- ---------------------- |
- DOS |
- 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
- ------------
- 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:
-
- ---------------------
- VECTOR TABLE
- 0000:0084 INT21h vector = 1200:0240 --+
- ---------------------- |
- |
- ---------------------- |
- TSR A |
- 1200:0240 NewInt21 (int handler) <----+
- 1200:0242 OldInt21 = 1000:0296 ---+
- ... |
- ---------------------- |
- |
- ---------------------- |
- TSR B |
- 1000:0296 NewInt21 (int handler) <--+
- 1000:0298 OldInt21 = 0070:1234 ---+
- ... |
- ---------------------- |
- |
- ---------------------- |
- DOS |
- 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 ENTRY STRUCTURE
- -------------------
- 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
- handler.
-
- 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
- int_handler:
- ; 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.
-
-
- INSTALLING INTO THE CHAIN
- -------------------------
- 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
- handler.)
-
- ; 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
-
-
- INTERRUPT HANDLER
- -----------------
- 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:
-
- int_handler:
- (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
- all_done:
- ; if you are a hardware handler with flags=80h, do EOI here
- iret
-
- 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.
-
-
- WALKING THE CHAIN
- -----------------
- 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
- for:
-
- 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.
-
-
- DISCONNECTING FROM THE CHAIN
- ----------------------------
- 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
- end
-
- ; No, walk the chain until case B or C occurs
- L1:
- 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
- end
-
- ; ES:BX handler does not point to us, work backward
- les bx,es:[bx+2]
- jmp L1
-
- chain_busted:
- ; If we get here, we cannot unload safely
- ; Notify user and exit
-
- unload:
- ; 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.
-
-
- FINAL COMMENTS
- --------------
- 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
-