2021 by C. Masloch. Usage of the works is permitted provided that this instrument is retained with the works, so that any entity that uses the works is notified of this instrument. DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY.
This document has been compiled on 2023-11-18.
TSR to keep hooks available. AMIS v3.6 compliant. Optimal installation, advanced deinstallation method.
KEEPHOOK installs do-nothing interrupt handlers that always pass control to the next handler in the interrupt chain. The purpose of this is to allow access to parts of an interrupt chain otherwise obscured by incompatible TSRs.
Compatible TSRs employ the IBM Interrupt Sharing Protocol (IISP) interrupt entrypoint headers to advertise their next handler pointer. This allows unhooking an interrupt handler even when it is not the current topmost handler.
The KEEPHOOK entries, crucially, can be accessed from the Alternate Multiplex Interrupt Specification (AMIS) interrupt list. KEEPHOOK allocates a multiplex number upon its installation, and identifies itself with the AMIS product name ‘KEEPHOOK
’ and vendor name ‘ecm
’.
If you know some of these concepts already you may wish to skip some or all of the following sections.
Interrupts originate as a control-flow feature, originally for interrupting normal execution of a program upon special conditions. These are called hardware interrupts. For example, a keyboard device may initiate a hardware interrupt request (IRQ) to the CPU to indicate it has new data available. That is, in the case of a keyboard, one or several new keypress or key release events.
The keyboard is actually numbered as the second IRQ (IRQ #1) on a typical 86-DOS system. IRQ #0 is used for a timer tick. It is raised by default with a frequency of 18.2 Hz (nearly 64 kilo-binary = 65_536 times per hour). There are several other IRQ sources, up to 15 of which can be differentiated by the dual-PIC setup of a typical IBM-PC-compatible 86-DOS machine.
When an IRQ is being serviced by the CPU, the CPU waits until such a time that it comes upon an instruction boundary. (A repeated string operation counts as multiple instructions for these purposes.) Then it saves some machine state onto the current stack. On an 8086 processor those are the 16-bit flags register, the code segment, and the instruction pointer, for a total of 6 bytes. This interrupt stack frame can be used to return the control flow to the point that is being interrupted. There is a dedicated "interrupt return" (iret
) instruction that performs this action. Upon execution of an interrupt, after the stack frame is written a few control flags are modified. On the 8086, the Interrupt Flag (IF) and Trace Flag (TF) are cleared.
Finally, to service the interrupt, a certain "far" address (16-bit code segment value and 16-bit instruction pointer value) is read from a specific entry of a table, the Interrupt Vector Table. The IVT is located in the random-access memory at linear address zero for the 8086. Loading the CS:IP
from the IVT is effectively an inter-segment branch to some code somewhere in the 1088 KiB segmented address space. This code is called the interrupt handler, or Interrupt Service Routine (ISR).
Typically, an interrupt handler servicing an IRQ will respond to the interrupt condition in some way, such as to modify some memory. Aside from the interrupt stack frame (FL, CS, IP) the handler may push further registers onto the stack to preserve their values. Later it can pop the registers in the reverse order and then return to the interrupted code using the iret
instruction.
During their design of the 8086, Intel provided for up to 256 different interrupt numbers. Each interrupt number has a corresponding entry in the IVT. There is also an instruction, called int
, which can cause the processor to branch to a specified interrupt handler as if it was servicing an IRQ. As opposed to the hardware interrupts, an interrupt caused by an int
instruction is called a software interrupt.
When 86-DOS was developed, it was decided that several software interrupt numbers would be used for the purpose of a "system call" operation. The most well-known and common of these software interrupts is interrupt 21h. It is used for most DOS service calls. Using a software interrupt is useful to DOS due to several reasons.
The IVT can be used by any process without having to set up a segment register to point to any OS data (freeing them up to be all used by the application), and also without having to copy any sort of dispatcher or jump table into each process's code segment. Besides, the interrupt stack frame allows for some state to be restored upon an interrupt return without more OS-side setup.
Software interrupts used as service dispatchers often will be utilised to return results in one or more registers or status flags. They can also clobber registers. Unlike the typical use of hardware interrupts not all registers need to be preserved by software interrupt handlers. This is because the application knows when and that it uses an int
instruction, and so can treat it like a function call following a certain calling convention. Hardware interrupts can occur at most instruction boundaries and are thus less alike ordinary function calls.
Hooking an interrupt generally means to bend the address stored in an IVT entry so as to point to a new interrupt handler. Once the address is updated, subsequent interrupt calls to the affected interrupt will call the new handler.
DOS itself hooks several interrupts on start-up, including interrupt 21h. In this case, generally DOS will not remember the address previously found in the IVT entry for that interrupt number.
However, there is a class of system extensions that falls into the category "Terminate and Stay Resident" (TSR) programs. A TSR installs a resident part before terminating another part, called the transient part. Thus it leaves resident a part of itself. Such programs may hook interrupts, such as the DOS service interrupt 21h. More often than not, such resident software will retain a segmented far pointer to the "next handler". This pointer is obtained from the corresponding IVT entry before the entry is modified to hook the interrupt.
The next handler pointer can be used for three purposes. First, if the resident part is ever to be uninstalled during a session then the program needs to have remembered the prior handler's address to restore that address into the IVT entry. Second, upon whatever handling the resident does when its interrupt handler is called, it may decide to "chain" the call to the prior handler. It can do so using a direct or indirect jump branch. Third, the resident may call the prior handler by emulating an interrupt call complete with a stack frame. This is obtained by running a pushf
instruction followed by a direct or indirect call branch. If the control flow returns from the prior interrupt handler then the resident may do additional handling afterwards. This allows the resident to do both pre- and post-processing of an interrupt service call.
As alluded to, a TSR program may be instructed to uninstall its resident portion. Non-resident software can also hook interrupts and may want to unhook them when it terminates.
In the simplest case, to unhook an interrupt means to take the "next handler" address the program has stashed away, and write it to the IVT entry of the interrupt in question.
However, consider this: Let A be the program that hooked interrupt 21h. If a different program, let's call it B, hooked the same interrupt at a point in time where A's handler already had been hooked into the interrupt, then we end up with a chain of interrupt handlers that goes like IVT -> B -> A -> DOS
. If A naively restores its next handler pointer (which contains the DOS's handler address) then the chain would change to be as follows, IVT -> DOS
. As desired, A's handler no longer is in the chain. But B's handler also is no longer in the chain, without B's awareness or consent.
If B also is instructed to uninstall and naively restores its next handler pointer into the IVT then the IVT will point to A's handler next. But by this time, the memory used by the resident A may already have been freed and possibly re-used. Clearly, this is concerning.
In some cases it can be appropriate or desired to uninstall one's interrupt hook in this way. However, more commonly it is better for the program A when trying to uninstall to check whether its resident still provides the top-most handler for the used interrupt number. That is, whether the IVT entry still points to A's resident handler, rather than elsewhere.
This introduces a failure condition: If, like before, B hooks the same interrupt after A has hooked it, and then A is told to uninstall its resident, its IVT entry check will indicate that its resident does not provide the top-most handler. Pending further options this must cause A to fail in part or in full. It should report this and leave resident at least a minimal interrupt handler, in the same spot that held the entrypoint for its full interrupt handler, so as not to disturb the interrupt handler chain. That is, the address stored by B as its next handler pointer must remain valid.
The problem with two TSRs both hooking the same interrupt is that one of them remembers a pointer to the other. It is not enough to modify the IVT.
However, what if we had a program (call it C) that indicates in some way where its "next handler" pointer lives? If A is directed to uninstall its resident portion, and C's is the top-most handler, then A can follow C's next handler pointer and determine that it points to A's handler. If C promises to use that particular pointer, and no other, to refer to A, then A gains an option. It can modify C's pointer and update it with the next handler pointer of A itself. So we go from IVT -> C -> A -> DOS
to the new chain IVT -> C -> DOS
.
To automate the indication, the IBM Interrupt Sharing Protocol (IISP) was defined. It consists of an 18-byte structure, of which 6 bytes are crucial. Those 6 are comprised first of a word (2-byte) signature, the string "KB", at the address of the interrupt handler entrypoint plus six bytes. And second, at the entrypoint address plus two bytes, a dword (4-byte) segmented far pointer. This pointer is the sole reference to the next handler; if the resident C wants to unhook itself (either from the IVT or another reference) or wants to call or chain to the next handler, it must use the IISP header's field.
If you install the resident A first, and then the B mentioned earlier, A can no longer access the reference to its interrupt handler. The same is true if you install A first, then C, and then B. However, there is a way to install B after A and still access the reference to A.
For how to do that, first some words about the Alternate Multiplex Interrupt Specification (AMIS). AMIS was defined by Ralf Brown to allow a common interface to TSRs with several standard functions. Instead of overloading common software interrupts like interrupt 21h (DOS services) or interrupt 2Fh (the traditional "multiplex" interrupt) with ever more special magic functions, AMIS defines a new interface on the previously unused interrupt 2Dh.
Each AMIS program allocates a multiplex number out of 256 possible values upon installation, choosing a number that is not yet used. To facilitate this, there is a common detection function, the AMIS function 0, called "Installation Check". The multiplex number to check is written to the AH
register, and the AMIS function number to call is put into AL
. Then the software interrupt 2Dh is called. Function 0 returns with AL
set to 0FFh if the multiplex number is in use, but leaves AL
at 0 instead if it isn't. (Function 0 also returns a signature and version number in three more registers but these don't matter to us.)
Another AMIS function is function 4, called "Determine Chained Interrupts". It can be called once it has been detected that a multiplex number is in use. Again the multiplex number is passed in AH
and the function number in AL
. Additionally, the querent must provide an interrupt number to check in the BL
register. This function can return a pointer to an interrupt handler entrypoint, or the address of an AMIS interrupt list, or other return values. It is assumed that AMIS TSRs' interrupt handlers begin with an IISP header.
The AMIS interrupt list is made up of a number of 3-byte entries: The first byte specifies an interrupt number, the second and third byte form a word that specifies an offset. The offset, combined with the same segment as used to address the interrupt list itself, points to the entrypoint of an interrupt handler that corresponds to the specified interrupt number. The last entry of the list is the one with an interrupt number of 2Dh. (Note that it is not prohibited to have multiple interrupt handlers for the same interrupt number, save interrupt 2Dh.)
Knowing that much, consider a case with a TSR program called D. It is an AMIS-compliant TSR which allocates a multiplex number to be detected by AMIS function 0, and returns an interrupt list upon being queried on AMIS function 4. Aside from interrupt 2Dh, it also hooks interrupt 21h.
Now if you install the TSRs in the order A then D then B, you end up with an interrupt chain for interrupt 21h that goes like IVT -> B -> D -> A -> DOS
. If you tell the A program to uninstall itself, it will determine that the IVT points to B, and B's handler does not start with an IISP header. The advanced deinstallation method kicks in now: The deinstaller for A can detect all AMIS multiplex numbers that are in use, then ask each resident AMIS TSR about its handler for interrupt 21h using function 4. If it finds such a handler it can search the interrupt chain starting at this handler.
In our case, A will detect the resident D, ask for its interrupt 21h handler with function 4, and receive the interrupt list. The interrupt list contains an entry for D's interrupt 21h handler entrypoint. Searching the chain starting from D's handler, which has an IISP header, allows A to find the reference to A's handler. Then A can uninstall its own handler from the chain by updating the reference in D's IISP header. The interrupt chain is updated from IVT -> B -> D -> A -> DOS
to now be IVT -> B -> D -> DOS
. The reference that B holds to D remains valid.
Installing KEEPHOOK is supposed to aid programs which employ the advanced deinstallation method of searching for references to their interrupt handlers starting from the handlers advertised by resident AMIS multiplexers. To this end, KEEPHOOK can set up handlers that do nothing on their own and always immediately chain to the next handler. KEEPHOOK's only handler that does something else is its interrupt 2Dh handler, which implements an AMIS multiplexer.
KEEPHOOK can be instructed to "cover" interrupt chains (which is the default operation), to "expose" chains, or to "uninstall" its handlers if reachable. It is also valid to specify both uninstalling and covering at the same time; first any reachable handlers will be uninstalled, then the same interrupts will be covered.
Interrupt numbers can be specified on the command line as one- or two-digit hexadecimal values, separated by blanks. It is also valid to specify the keyword ALL
which is evaluated to mean all interrupt numbers which KEEPHOOK currently has handlers installed for. For the /C and /E switches the default operation if no interrupt numbers are specified is to act as if ALL
was specified.
Covering means insuring that the top-most interrupt handler for an interrupt points to a handler provided by KEEPHOOK. That is, to make the IVT point to KEEPHOOK. If this is not yet true then KEEPHOOK tries to install a new handler as the top-most one. (This requires an unused entry in KEEPHOOK's interrupt list and a block from its heap to install a new handler.) This is the same operation as installing a regular TSR.
Covering is indicated by specifying the /C
switch. Covering may be specified when KEEPHOOK is already resident or when it is to be installed. If no operation is specified by switches (that is, none of the /C
, /E
, /U
, or /R
switches is present) but any interrupt number or the ALL
keyword is present then the default operation is covering.
Exposing means to make sure that the top-most interrupt handler is not one provided by KEEPHOOK. If it currently is, it will be uninstalled. (If several handlers in a row come to be installed consecutively by the same KEEPHOOK instance, and the first one is the topmost handler, then exposing the interrupt will uninstall the entire row.) This is the same operation as naively uninstalling a TSR, if possible. It allows access to a subsequent handler by a TSR that doesn't follow IISP header chains.
Exposing is indicated by specifying the /E
switch. Exposing may not be combined with covering. Exposing is only valid if KEEPHOOK is already resident.
Uninstalling means to remove all handlers provided by KEEPHOOK, where possible. To this end, KEEPHOOK itself uses the advanced deinstallation method.
Uninstalling is indicated by specifying the /U
switch (or the alias /R
switch). Uninstalling may be combined with covering, but not with exposing. Uninstalling is only valid if KEEPHOOK is already resident.
If the /U
switch is given without any interrupt number specifications and without /C
, then KEEPHOOK will attempt to fully uninstall itself. This only succeeds if all installed handlers succeed in unhooking. (If uninstalling fails, it will still have unhooked all reachable handlers.)
/X=NN
switch (Set preferred multiplex number) #Attempt resident detection and allocation of multiplex number NN first. NN is a one-digit or two-digit hexadecimal number.
During allocation of a multiplex number while installing a new instance of KEEPHOOK, the number specified by the /X=
switch is checked first. After that, every possible multiplex number is checked, counting up from 00h to FFh. (Absent the /X=
switch this means multiplex numbers are allocated in ascending order.)
During detection of already resident KEEPHOOK instances, a multiplex number specified by the /X=
switch is checked first. After that, every possible multiplex number is checked in order, counting down from FFh to 00h. (Absent the /X=
switch this means the instance that was last installed is found first.)
/S=number
switch (Set heap size) #Currently only supported when installing KEEPHOOK. Set heap size to number (in decimal).
The heap is used for the AMIS interrupt list and the interrupt handlers. Each heap block is 24 bytes large. The interrupt list takes 3 bytes per handler. (One interrupt list entry is taken up by the interrupt 2Dh handler.) Each interrupt handler takes up one list entry and one heap block. (It just so happens that a maximally compatible IISP header plus an indirect far jump instruction take up exactly 24 bytes.)
The interrupt list is allocated at the beginning of the heap and is always large enough to hold entries for all remaining heap blocks. The heap must be composed of at least 2 blocks (one for the list and the other for a handler). The default heap size is 8 blocks, just enough for 7 interrupt handlers.
/O
switch
/J
switch
/N
switch
../lmacros/
as seen from the keephook directory)
./mak.sh
(needs bash and nasm)
nasm -I ../lmacros/ transien.asm -o keephook.com
./mak.sh
(needs bash and hg and halibut)
hg 20088197f624, from commit on at 2023-11-18 11:04:26 +0100
If this is in ecm's repository, you can find it at https://hg.pushbx.org/ecm/keephook/rev/20088197f624