The Art of
ASSEMBLY LANGUAGE PROGRAMMING

Chapter Eighteen (Part 2)

Table of Content

Chapter Eighteen (Part 4)

CHAPTER EIGHTEEN:
RESIDENT PROGRAMS (Part 3)
18.3 - Reentrancy
18.3.1 - Reentrancy Problems with DOS
18.3.2 - Reentrancy Problems with BIOS
18.3.3 - Reentrancy Problems with Other Code
18.4 - The Multiplex Interrupt (INT 2Fh)
18.3 Reentrancy

One big problem with active TSRs is that their invocation is asynchronous. They can activate at the touch of a keystroke timer interrupt or via an incoming character on the serial port just to name a few. Since they activate on a hardware interrupt the PC could have been executing just about any code when the interrupt came along. This isn't a problem unless the TSR itself decides to call some foreign code such as DOS a BIOS routine or some other TSR. For example the main application may be making a DOS call when a timer interrupt activates a TSR interrupting the call to DOS while the CPU is still executing code inside DOS. If the TSR attempts to make a call to DOS at this point then this will reenter DOS. Of course DOS is not reentrant so this creates all kinds of problems (usually it hangs the system). When writing active TSRs that call other routines besides those provided directly in the TSR you must be aware of possible reentrancy problems.

Note that passive TSRs never suffer from this problem. Indeed any TSR routine you call passively will execute in the caller's environment. Unless some other hardware ISR or active TSR makes the call to your routine you do not need to worry about reentrancy with passive routines. However reentrancy is an issue for active TSR routines and passive routines that active TSRs call.

18.3.1 Reentrancy Problems with DOS

DOS is probably the biggest sore point to TSR developers. DOS is not reentrant yet DOS contains many services a TSR might use. Realizing this Microsoft has added some support to DOS to allow TSRs to see if DOS is currently active. After all reentrancy is only a problem if you call DOS while it is already active. If it isn't already active you can certainly call it from a TSR with no ill effects.

MS-DOS provides a special one-byte flag ( InDOS) that contains a zero if DOS is currently active and a non-zero value if DOS is already processing an application request. By testing the InDOS flag your TSR can determine if it can safely make a DOS call. If this flag is zero you can always make the DOS call. If this flag contains one you may not be able to make the DOS call. MS-DOS provides a function call Get InDOS Flag Address that returns the address of the InDOS flag. To use this function load ah with 34h and call DOS. DOS will return the address of the InDOS flag in es:bx. If you save this address your resident programs will be able to test the InDOS flag to see if DOS is active.

Actually there are two flags you should test the InDOS flag and the critical error flag (criterr). Both of these flags should contain zero before you call DOS from a TSR. In DOS version 3.1 and later the critical error flag appears in the byte just before the InDOS flag.

So what should you do if these flags aren't both zero? It's easy enough to say "hey come back and do this stuff later when MS-DOS returns back to the user program." But how do you do this? For example if a keyboard interrupt activates your TSR and you pass control on to the real keyboard handler because DOS is busy you can't expect your TSR to be magically restarted later on when DOS is no longer active.

The trick is to patch your TSR into the timer interrupt as well as the keyboard interrupt. When the keystroke interrupt wakes your TSR and you discover that DOS is busy the keyboard ISR can simply set a flag to tell itself to try again later; then it passes control to the original keyboard handler. In the meantime a timer ISR you've written is constantly checking this flag you've created. If the flag is clear it simply passes control on to the original timer interrupt handler if the flag is set then the code checks the InDOS and CritErr flags. If these guys say that DOS is busy the timer ISR passes control on to the original timer handler. Shortly after DOS finishes whatever it was doing a timer interrupt will come along and detect that DOS is no longer active. Now your ISR can take over and make any necessary calls to DOS that it wants. Of course once your timer code determines that DOS is not busy it should clear the "I want service" flag so that future timer interrupts don't inadvertently restart the TSR.

There is only one problem with this approach. There are certain DOS calls that can take an indefinite amount of time to execute. For example if you call DOS to read a key from the keyboard (or call the Standard Library's getc routine that calls DOS to read a key) it could be hours days or even longer before somebody actually bothers to press a key. Inside DOS there is a loop that waits until the user actually presses a key. And until the user presses some key the InDOS flag is going to remain non-zero. If you've written a timer-based TSR that is buffering data every few seconds and needs to write the results to disk every now and then you will overflow your buffer with new data if you wait for the user who just went to lunch to press a key in DOS' command.com program.

Luckily MS-DOS provides a solution to this problem as well - the idle interrupt. While MS-DOS is in an indefinite loop wait for an I/O device it continually executes an int 28h instruction. By patching into the int 28h vector your TSR can determine when DOS is sitting in such a loop. When DOS executes the int 28h instruction it is safe to make any DOS call whose function number (the value in ah) is greater than 0Ch.

So if DOS is busy when your TSR wants to make a DOS call you must use either a timer interrupt or the idle interrupt (int 28h) to activate the portion of your TSR that must make DOS calls. One final thing to keep in mind is that whenever you test or modify any of the above mentioned flags you are in a critical section. Make sure the interrupts are off. If not your TSR make activate two copies of itself or you may wind up entering DOS at the same time some other TSR enters DOS.

An example of a TSR using these techniques will appear a little later but there are some additional reentrancy problems we need to discuss first.

18.3.2 Reentrancy Problems with BIOS

DOS isn't the only non-reentrant code a TSR might want to call. The PC's BIOS routines also fall into this category. Unfortunately BIOS doesn't provide an "InBIOS" flag or a multiplex interrupt. You will have to supply such functionality yourself.

The key to preventing reentering a BIOS routine you want to call is to use a wrapper. A wrapper is a short ISR that patches into an existing BIOS interrupt specifically to manipulate an InUse flag. For example suppose you need to make an int 10h (video services) call from within your TSR. You could use the following code to provide an "Int10InUse" flag that your TSR could test:

MyInt10         proc    far
inc     cs:Int10InUse
pushf
call    cs:OldInt10
dec     cs:Int10InUse
iret
MyInt10         endp

Assuming you've initialized the Int10InUse variable to zero the in use flag will contain zero when it is safe to execute an int 10h instruction in your TSR it will contain a non-zero value when the interrupt 10h handler is busy. You can use this flag like the InDOS flag to defer the execution of your TSR code.

Like DOS there are certain BIOS routines that may take an indefinite amount of time to complete. Reading a key from the keyboard buffer reading or writing characters on the serial port or printing characters to the printer are some examples. While in some cases it is possible to create a wrapper that lets your TSR activate itself while a BIOS routine is executing one of these polling loops there is probably no benefit to doing so. For example if an application program is waiting for the printer to take a character before it sends another to printer having your TSR preempt this and attempt to send a character to the printer won't accomplish much (other than scramble the data sent to the print). Therefore BIOS wrappers generally don't worry about indefinite postponement in a BIOS routine.

5 8 9 D E 10 13 16 17 21 28

If you run into problems with your TSR code and certain application programs you may want to place wrappers around the following interrupts to see if this solves your problem: int 5 int 8 int 9 int B int C int D int E int 10 int 13 int 14 int 16 or int 17. These are common culprits when TSR problems develop.

18.3.3 Reentrancy Problems with Other Code

Reentrancy problems occur in other code you might call as well. For example consider the UCR Standard Library. The UCR Standard Library is not reentrant. This usually isn't much of a problem for a couple of reasons. First most TSRs do not call Standard Library subroutines. Instead they provide results that normal applications can use; those applications use the Standard Library routines to manipulate such results. A second reason is that were you to include some Standard Library routines in a TSR the application would have a separate copy of the library routines. The TSR might execute an strcmp instruction while the application is in the middle of an strcmp routine but these are not the same routines! The TSR is not reentering the application's code it is executing a separate routine.

However many of the Standard Library functions make DOS or BIOS calls. Such calls do not check to see if DOS or BIOS is already active. Therefore calling many Standard Library routines from within a TSR may cause you to reenter DOS or BIOS.

One situation does exist where a TSR could reenter a Standard Library routine. Suppose your TSR has both passive and active components. If the main application makes a call to a passive routine in your TSR and that routine call a Standard Library routine there is the possibility that a system interrupt could interrupt the Standard Library routine and the active portion of the TSR reenter that same code. Although such a situation would be extremely rare you should be aware of this possibility.

Of course the best solution is to avoid using the Standard Library within your TSRs. If for no other reason the Standard Library routines are quite large and TSRs should be as small as possible.

18.4 The Multiplex Interrupt (INT 2Fh)

When installing a passive TSR or an active TSR with passive components you will need to choose some interrupt vector to patch so other programs can communicate with your passive routines. You could pick an interrupt vector almost at random say int 84h but this could lead to some compatibility problems. What happens if someone else is already using that interrupt vector? Sometimes the choice of interrupt vector is clear. For example if your passive TSR is extended the int 16h keyboard services it makes sense to patch in to the int 16h vector and add additional functions above and beyond those already provided by the BIOS. On the other hand if you are creating a driver for some brand new device for the PC you probably would not want to piggyback the support functions for this device on some other interrupt. Yet arbitrarily picking an unused interrupt vector is risky; how many other programs out there decided to do the same thing? Fortunately MS-DOS provides a solution: the multiplex interrupt. Int 2Fh provides a general mechanism for installing testing the presence of and communicating with a TSR.

To use the multiplex interrupt an application places an identification value in ah and a function number in al and then executes an int 2Fh instruction. Each TSR in the int 2Fh chain compares the value in ah against its own unique identifier value. If the values match the TSR process the command specified by the value in the al register. If the identification values do not match the TSR passes control to the next int 2Fh handler in the chain.

Of course this only reduces the problem somewhat it doesn't eliminate it. Sure we don't have to guess an interrupt vector number at random but we still have to choose a random identification number. After all it seems reasonable that we must choose this number before designing the TSR and any applications that call it after all how will the applications know what value to load into ah if we dynamically assign this value when the TSR goes resident?

Well there is a little trick we can play to dynamically assign TSR identifiers and let any interested applications determine the TSR's ID. By convention function zero is the "Are you there?" call. An application should always execute this function to determine if the TSR is actually present in memory before making any service requests. Normally function zero returns a zero in al if the TSR is not present it returns 0FFh if it is present. However when this function returns 0FFh it only tells you that some TSR has responded to your query; it does not guarantee that the TSR you are interested in is actually present in memory. However by extending the convention somewhat it is very easy to verify the presence of the desired TSR. Suppose the function zero call also returns a pointer to a unique identification string in the es:di registers. Then the code testing for the presence of a specific TSR could test this string when the int 2Fh call detects the presence of a TSR. the following code segment demonstrates how a TSR could determine if a TSR identified as "Randy's INT 10h Extension" is present in memory; this code will also determine the unique identification code for that TSR for future reference:

; Scan through all the possible TSR IDs. If one is installed
see if
; it's the TSR we're interested in.

mov     cx
0FFh                ;This will be the ID number.
IDLoop:         mov     ah
cl                  ;ID -> AH.
push    cx                      ;Preserve CX across call
mov     al
0                   ;Test presence function code.
int     2Fh                     ;Call multiplex interrupt.
pop     cx                      ;Restore CX.
cmp     al
0                   ;Installed TSR?
je      TryNext                 ;Returns zero if none there.
strcmpl                         ;See if it's the one we want.
byte    "Randy's INT "
byte    "10h Extension"
0
je      Success                 ;Branch off if it is ours.
TryNext:        loop    IDLoop                  ;Otherwise
try the next one.
jmp     NotInstalled            ;Failure if we get to this point.

Success:        mov     FuncID
cl              ;Save function result.
.
.
.

If this code succeeds the variable FuncId contains the identification value for resident TSR. If it fails the application program probably needs to abort or otherwise ensure that it never calls the missing TSR.

The code above lets an application easily detect the presence of and determine the ID number for a specific TSR. The next question is "How do we pick the ID number for the TSR in the first place?" The next section will address that issue as well as how the TSR must respond to the multiplex interrupt.

Chapter Eighteen (Part 2)

Table of Content

Chapter Eighteen (Part 4)

Chapter Eighteen: Resident Programs (Part 3)
29 SEP 1996