Strategies and Techniques for Writing State-of-the-Art TSRs that Exploit MS-DOS(R) 5

Douglas Boling

Douglas Boling is a programmer and writer. He is a contributing editor for PC Magazine.

{ewc navigate.dll, ewbutton, /Bcodeview /T"Click to open or copy the code samples from this article." /C"samples_1}

Ever since PRINT.COM was introduced in DOS 2.0, terminate-and-stay-resident (TSR) utilities have been a mainstay of the DOS operating system. But the functions necessary for TSR programming weren’t comprehensively documented until the advent of MS-DOSÒ 51. MS-DOS 5 also includes new features that were previously available only as third-party add-ons, such as HIMEM.SYS and EMM386.SYS.

Unfortunately, the added services of MS-DOS 5 make today’s TSR a more complex beast than the TSR of the early years of MS-DOS. For example, the MS-DOS Shell Program, like the MicrosoftÒ WindowsÔ graphical environment, allows multiple instances of MS-DOS to be running on the same machine. New device drivers such as HIMEM.SYS require TSRs to incorporate additional code to save the state of the drivers.

This article discusses how to write TSRs for MS-DOS 5. It details how to remain compatible with earlier versions of MS-DOS while taking advantage of the new features of MS-DOS 5. A sample TSR, TEMPLATE.ASM, demonstrates the techniques discussed here. A general understanding of the MS-DOS API and the writing of TSRs is assumed.

A number of new features in MS-DOS 5 are relevant to TSR programming. Of special interest are the extended memory manager HIMEM.SYS and the expanded memory manager EMM386.SYS. These drivers provide new methods of allocating memory beyond the bounds of the invariably scarce conventional memory.

HIMEM.SYS is not new, but MS-DOS 5 is the first version of MS-DOS to include it. HIMEM.SYS implements the extended memory specification (XMS) developed by Lotus, Intel, and Microsoft. This XMS memory manager regulates access to extended memory as well as the high memory area (HMA) located just above the 1MB conventional memory boundary. The HMA, 16 bytes short of a full 65536 bytes, is reached by setting a segment register to 0FFFFH and using the offset to access the memory above the standard 1MB limit of real mode.

EMM386.SYS uses the page registers and virtual-86 mode of the IntelÒ 386 to backfill RAM into the unused address space between the 640KB conventional memory line and the ROM BIOS located at segments E000H or F000H. MS-DOS 5 integrates the API to access Upper Memory Blocks (UMBs) into the standard INT 21H system interrupt. To remain compatible with earlier versions of MS-DOS, the inclusion of the UMBs in the standard MS-DOS memory API can be disabled. Fortunately, the impact of EMM386.SYS as well as HIMEM.SYS on TSR code is minimal. Both drivers have documented functions that allow their state to be saved and restored, a critical requirement for TSRs.

MS-DOS 5 includes the DOSSHELL program, which uses the MS-DOS task switcher. Although the features of the task switcher are useful for users, the problems of multiple MS-DOS sessions complicate TSR programming. Fortunately, the MS-DOS 5 task switcher includes an API that allows applications to manage the task-switching aspects of the system.

We Knew That

Microsoft seems to have finally realized that a number of "undocumented" functions were in fact documented everywhere but the MS-DOS technical reference. While some of this information was hidden away in the Microsoft MS-DOS Encyclopedia, the inclusion of a number of the most widely used "undocumented" functions in the Microsoft MS-DOS Programmer’s Reference was not only a pleasant surprise, but an indication that these functions will probably remain stable in future versions of MS-DOS.

One newly documented function, Set Extended Error Information (Function 5D0AH), available since DOS version 3.1, is necessary for TSRs that use the file operations of MS-DOS. Set Active PSP Address (Function 50H), an absolute requirement for TSRs, has also been documented. Other newly documented functions include Get Default Disk Parameter Block and Get Disk Parameter Block (Functions 1FH and 32H), which allow programs to get information about block devices. These two functions have been around since DOS 2.0, although the format of the Disk Parameter Block tables has changed slightly with each version.

Other new functions calls deal with new features of MS-DOS 5 such as handling UMBs. The functions Set Upper-Memory Link (Function 5803H) and Set Memory Allocation Strategy (Function 5801H) must be used to access the UMBs.

When an application is launched, the UMBs are not linked to the memory allocation chain. To use the standard Allocate Memory (Function 48H) call to claim upper memory, UMBs must be linked using Set Upper-Memory Link, and the memory allocation strategy must be set to instruct MS-DOS to look in the UMBs to find a suitable free memory block. (See the DOS Q&A, MSJ, Vol. 6, No. 6, for information on allocating UMBs--Ed.) Fortunately, the calls Get Upper-Memory Link (Function 5802H) and Get Memory Allocation Strategy (Function 5800H) can be used to save the current memory allocation configuration.

The addition of the SETVER command means the Get Version Number (Function 30H) call cannot be relied upon to return the true version of DOS. This can be important for TSRs that rely on version-specific undocumented features. To determine the true version of DOS, a new call, Get DOS Version (Function 3306H), has been added.

Get DOS Version returns the true version of DOS as well as the DOS revision number. This number, returned in the low 3 bits of DL, can be used to keep track of different bug fix releases of DOS that do not change the version number, a process that was particularly difficult with the bug-ridden early releases of DOS 4. DH returns the DOS version flags, which indicate whether MS-DOS is executing from ROM or from the HMA.

Multiplex Interrupt

Before discussing TSR programming, it is a good idea to review the MS-DOS multiplex interrupt function, INT 2FH. The multiplex interrupt was first used in DOS 2.0 so that PRINT.COM could find itself in memory. Unfortunately, the method used in DOS 2 could only be used by PRINT. With the release of DOS 3, PRINT.COM was redesigned so that other programs could use the multiplex interrupt.

The multiplex interrupt provides both a method of finding previously installed copies of a program and a standard API mechanism to access those installed programs. The standard installation check is to perform a multiplex interrupt with the device ID of your TSR in AH and AL set to 0. If the corresponding TSR is loaded, a nonzero value is placed in AL and the installed program returns.

mov ah,0DBh ;Select Device ID 0DBh

mov al,0 ;Install Check

int 2fh

or al,al ;If AL<>0, device ID found.

je label1

inc DevDB_Found ;Set program installed flag

label1:

Since two applications may use the same multiplex ID, additional steps should be taken to ensure that the TSR responding to the installation check is the desired TSR. A convenient way to do this is to place additional values in the other registers or to return a pointer to a predetermined block of data.

Other multiplex commands are unique to each program. To access a resident TSR, a program places the device ID of that TSR into AH and the command into AL, then performs an INT 2FH. The multiplex interrupt routine in the installed TSR checks for its device ID in AH. If found, the TSR acts on the command and returns. If the device ID doesn’t match, the TSR simply passes the command down the interrupt chain by jumping to the previous INT 2FH vector. The following code demonstrates how to monitor the multiplex interrupt.

int2f proc far

assume cs:code,ds:nothing,es:nothing

cmp ah,0DB ;Is DevID correct?

je my_code

muxint_jmp:

jmp cs:[int2fh] ;Call old int

mycode:

or al,al ;See if install check command

jne next_cmd

mov al,-1 ;Set installed flag

int2f_return:

iret

next_cmd:

o

o ; Check for other application-specific commands

o

jmp short int2f_return

int2f endp

Both MS-DOS and Windows2 use the multiplex interrupt to provide APIs to other programs. MS-DOS reserves device IDs 0 through BFH for its own use. For example, PRINT uses device ID 01H, DOSKEY uses ID 43H, and enhanced mode Windows uses device ID 16H. Application programs are left to fight over the remaining 64 IDs. While it seems that application programs get the short end of the stick with only 64 device IDs, the multiplex interrupt is quite handy as a standard communication method.

Task Switchers

As mentioned earlier, another complication for TSR programmers is the popularity of Windows and MS-DOS task-switching software. There is no problem if a TSR is loaded after the task switcher because the TSR is loaded into memory local to the particular MS-DOS session. However, if a TSR is loaded before the task switcher is started, it sits in global memory where it is likely to be called from more than one MS-DOS session. Reentrancy problems can result if the TSR is called from one session while being already active in another session.

A simple solution is to not allow two sessions to call the TSR at the same time. But that’s not an option for TSRs such as command-line editors. This solution also forces the user to switch to the session with the active TSR, terminate it, and return to the current session to reinvoke the TSR.

A better way to solve the multiple MS-DOS session problem is to use instance memory. Instance memory is made unique by the task switcher for each MS-DOS session. This allows the TSR to have a unique instance of data for each MS-DOS session. The TSR can therefore be easily made reentrant, since it uses a different data segment for each MS-DOS session.

Unfortunately, the Windows 3.0 and MS-DOS 5 task switchers use different APIs for creating and maintaining session data. The task switching API under the MS-DOS shell is complete, if a bit complex; the Windows task switch API is not complete.

Windows 3.0 can maintain instance memory only under 386 enhanced mode. Not only does Windows not provide instance memory under real and standard modes, it doesn’t even notify MS-DOS programs of session switches. Under standard and real modes, the TSR is notified only that Windows is being launched and terminated.

TSRs are left with three options for Windows 3.0 real and standard modes. The TSR can allow only one session to activate the program at a time, the TSR can be made reentrant including eliminating any internal stacks, or it can prevent Windows from launching by intercepting the Windows launch broadcast.

Fortunately, the Windows real and standard mode problem will be solved by Windows 3.1. Real mode won’t exist in Windows 3.1; standard mode in 3.1 will use the MS-DOS task switcher API to manage session data. Unfortunately, (based on information distributed with the beta II Windows 3.1 release) Windows 3.1 enhanced mode uses the Windows 3.0 enhanced mode task switcher API for (I assume) compatibility, forcing TSRs to manage two separate APIs to operate with Windows.

When Windows is started, it broadcasts a launch message to the system by calling the multiplex interrupt with AX = 1605H. The TSR should push the FLAGS register and perform a far call to the old INT 2FH vector to pass the notification down the multiplex chain. When the call returns, the TSR can take the actions necessary to create instance data.

When the Windows launch notification is sent, bit 0 of DX is clear if Windows is starting in enhanced mode. If the TSR needs to prevent Windows from starting, it can place a nonzero value in CX and return. Windows will then abort its startup. Windows does not display any message before terminating so it is up to the TSR to tell the user why Windows aborted its launch. If no session data is needed, the TSR should simply return from the init call with CX unmodified.

If a TSR wants to create instance data for each session, it should pass the notification down the multiplex chain (see Figure 1), then place ES:BX in the NextDev field of the SWSTARTUPINFO structure (see Figure 2). The sisInstanceData field should point to a list of SWINSTANCEITEM structures. These SWINSTANCEITEM structures consist of a far pointer to the memory block to be instanced and a word value containing the size of the memory block (see Figure 3). The list is terminated by a SWINSTANCEITEM structure containing zeros. ES:BX should be modified to point to the SWSTARTUPINFO structure. No other registers should be modified. Each time enhanced mode Windows creates an MS-DOS session, the memory blocks pointed to by the SWINSTANCEITEM structures are unique to that session.

Figure 1 Creating Instance Data for Each Session

SWSTARTUPINFO STRUC

sisVersion dw 3

sisNextDev dd ?

sisVirtDevFile dd 0

sisReferenceData dd 0

sisInstanceData dd ?

SWSTARTUPINFO ENDS

InstanceList:

iisPtr1 dd InstMemPtr ;Ptr to inst memory

iisSize1 dw InstMemSize ;Size of inst mem blk

iisPtr2 dd 0 ;A NULL structure

iisSize2 dw 0 ; terminates the list

int2f proc far

assume cs:code,ds:nothing,es:nothing

cmp ax,1605h ;See if Windows starts

je init_win

cmp ax,4b05h ;See if switcher gets instance

je init_instance ; data.

int2f_jmp:

jmp cs:[int2fh] ;Call old int

init_win:

test dx,01h ;See if enhanced mode Windows

jne init_instance

inc cs:win_enhanced ;Set Enhanced Windows flag

init_instance:

pushf

call cs:[int2fh] ;Call old int

mov word ptr cs:[sisNextDev],bx

mov word ptr cs:[sisNextDev+2],es

mov bx,offset InstanceList

mov word ptr cs:[sisInstanceData],bx

mov word ptr cs:[sisInstanceData+2],cs

push cs ;ES:BX point to switcher struc

pop es

mov bx,offset StartupInfo

int2f_exit:

iret

int2f endp

Figure 2 The SWSTARTUPINFO Structure

SWSTARTUPINFO STRUC

sisVersion dw 3 ;Ignored by DOS

sisNextDev dd ? ;Ptr to previous SWSTARTUPINFO struc

sisVirtDevFile dd 0 ;Ptr to Win 3 Virt device driver name

sisReferenceData dd ? ;Data to be used by Virt Dev Driver

sisInstanceData dd ? ;Ptr to list of INSTANCEITEM structures

SWSTARTUPINFO ENDS

sisVersion Ignored by DOS. Windows 3 uses this value to identify the structure.
sisNextDev Pointer to the previous SWSTARTUPINFO structure passed to the program by the multiplex interrupt return.
sisVirtDevFile Ignored by DOS. This is a pointer to the ASCIIZ name of the Windows virtual device driver that should be loaded when Windows starts. If this field is 0, no virtual device will be loaded.
sisReferenceData This value will be passed to the virtual device driver if any is loaded. This field can contain any value.
sisInstanceData This is a pointer to a list of SWINSTANCEITEM structures. The list is terminated with a null SWINSTANCEITEM structure.

Figure 3 The SWINSTANCEITEM Structure

SWINSTANCEITEM STRUC

iisPtr dd ? ;Long pointer to memory block

iisSize dw ? ;Size of memory block to be instanced

SWINSTANCEITEM ENDS

iisPtr Long pointer to the block of memory to be instanced.
iisSize Size of block of memory to be instanced.

To prevent enhanced mode Windows from switching MS-DOS sessions during critical sections of code, you should use the BeginCriticalSection and EndCriticalSection calls. When entering a critical section, the TSR should perform a multiplex interrupt with AX=1681H. When leaving the critical section a TSR should perform a multiplex interrupt with AX=1682H. While these two calls can be quite useful, they should be used sparingly since they can degrade the performance of Windows.

The MS-DOS task switcher API, like the Windows MS-DOS Shell API, uses the multiplex interrupt but also includes a series of callback notification functions that offer more control to the TSR. To determine if an MS-DOS task switcher is loaded, a TSR can perform a multiplex interrupt with AX=4B02H. If the MS-DOS task switcher is loaded, AX will be 0, and ES:DI will contain a pointer to the task switcher service routine. This installation check does not follow the normal multiplex installation check standard.

The MS-DOS task switcher service routine provides various functions to resident programs, including getting the task switcher version and testing memory regions to see if they lie in global or local memory. Two other functions, Hook Notification Chain (Function 0004H) and Unhook Notification Chain (Function 0005H), should be used to inform the switcher that an application wishes to be informed of session events. While the current switcher does not use these functions (relying instead on the Build Notification Call discussed below), applications should use the Hook and Unhook calls for compatibility with future switcher implementations.

The MS-DOS task switcher uses the notification chain to signal TSRs of events about to happen and to get approval for those events. For example, the task switcher notifies programs that the current session is to be suspended. Before the task switcher suspends a session, it calls the notification chain. The TSRs in the notification chain can respond to the task switcher, indicating whether or not it’s OK to suspend the session. If a program hooked into the notification chain is in a critical section of code, it can prevent the session suspend by returning a nonzero value in AX. Similar notifications are sent for create session, activate session, destroy session, and switcher exit. The create session notification is handy for programs that need to initialize their instance data regions.

For programs that are resident before the task switcher is loaded, the task switcher performs a multiplex interrupt with AX=4B01H to build a notification chain. Installed TSRs should first pass the notification down the multiplex interrupt chain. (In fact, this Build Notification process must be supported in your TSR code even if the task switcher is detected before your TSR is launched. This is because the current version of the MS-DOS task switcher ignores the Hook and Unhook Notification Chain calls.) On return, the TSR should place ES:BX into the scbiNext field of the SWCALLBACKINFO structure (see Figures 4 and 5) and point ES:BX to that structure. The scbiEntryPoint field of the structure should point to the TSR’s notification routine.

Figure 4 The SWCALLBACKINFO Structure

SWCALLBACKINFO STRUC

scbiNext dd ? ;Ptr to previous SWCALLBACKINFO struc

scbiEntryPoint dd ? ;Address of notification routine

scbiReserved dd ?

scbiAPI dd ? ;Ptr to list of SWAPINFO structures

SWCALLBACKINFO ENDS

scbiNext Pointer to the previous SWCALLBACKINFO structure passed to the program by the multiplex interrupt return.
scbiEntryPoint Pointer to entry point of program notification routine. The routine should return with a RETF instruction.
scbiReserved Reserved value.
scbiAPI This is a pointer to a list of SWSWAPINFO structures. The list is terminated with a null SWSWAPINFO structure.

Figure 5 The SWSWAPINFO Structure

SWSWAPINFO STRUC

aisLength dw 10 ;Length of structure

aisAPI dw ? ;API Identifier

aisMajor dw ? ;Major version number

aisMinor dw ? ;Minor version number

aisSupport dw ? ;Support level

SWSWAPINFO ENDS

aisLength Length of SWSWAPINFO structure.
aisAPI Identifies the asynchronous API supported by the application. The value can be one of the following:

0001 NetBIOS
0002 802.2
0003 TCP/IP
0004 LAN Manager pipes
0005 NetWare IPX

aisMajor Major version level of the API supported
aisMinor Minor version level of the API supported
aisSupport Level of support provided by the application

  0001 Minimal support. The application prevents a task switch even after a function has been completed.
  0002 API level support.The application prevents a task switch while requests are outstanding.
  0003 Switcher compatibility. The application allows a task switch even when requests are outstanding. However, some requests may fail due to buffer limits or other factors.
  0004 Seamless compatibility. The API always allows a task switch to occur.

When the task switcher is started, it issues a multiplex interrupt with AX=4B05H to identify any instance memory blocks. TSRs should intercept this function the same way as the enhanced mode Windows launch call (INT 2FH AX=1605H). Fortunately, the SWSTARTUPINFO structure (see Figure 2) of the MS-DOS task switcher is identical to the corresponding structure in enhanced mode Windows. When the MS-DOS task switcher calls this function, CX:DX points to its service function handler. DX is not used here. This is different from the Init Windows call where it was used to differentiate enhanced mode startup from standard mode startup (see Figure 1).

TSR Programming Acrobatics

TSRs should be totally unobtrusive to the system and at the same time be instantly available to the user. This contradictory goal causes TSR programmers to jump through what seems to be an endless series of hoops. These hoops or workarounds take valuable memory, but are necessary to achieve compatibility with an operating system not originally designed to accommodate TSRs.

A TSR is instantly available because it has installed code that is continuously running in the background of the system. The code, in the form of interrupt routines, monitors the state of the system to determine if the TSR should be switched to the foreground. Usually, the switch to the foreground is prompted by a user pressing a hot key combination, but other TSRs such as PRINT periodically activate themselves to perform a background task without user intervention.

Due to the need to monitor the system constantly, TSRs have a complex relationship with the BIOS, MS-DOS, and hardware (see Figure 6). Monitoring the system requires simply hooking a few interrupt vectors. The standard interrupts to be monitored are the timer interrupt (INT 08H) and the MS-DOS idle interrupt (INT 28H). If a hot key is used to activate the TSR, the BIOS keyboard service interrupt (INT 09H) is used to monitor what keys are being pressed.

Figure 6 A TSR's Interaction with DOS, the BIOS, and Hardware

Hooking, or chaining an interrupt vector, must be done with care (see Figure 7). When an interrupt routine is called, the interrupt handler should first push the FLAGS register and perform a far call to the previous interrupt vector. This passes the interrupt down to previous handlers. When the interrupt returns, the TSR can perform any necessary processing.

Figure 7 Hooking an Interrupt Vector

timerint proc far

assume cs:code,ds:nothing,es:nothing

pushf

call cs:[int08h] ;Call old int 8

cmp cs:int08_active,0 ;See if we are in this

jne timerint_exit ; routine already

cmp cs:popflag,0 ;See if we need to pop up

jne timer_check

timerint_exit:

iret ;Return

timer_check:

push ax

inc cs:int08_active ;Set int active flag

call check_system ;See if system OK to pop up

or ax,ax

jne timerint_dec

call main ;Call the TSR

mov cs:popflag,1

timerint_dec:

dec cs:popflag

dec cs:int08_active ;Clear int active flag

pop ax

jmp short timerint_exit

timerint endp

The idle interrupt is called by MS-DOS when it is waiting for input from the user. This interrupt is convenient for TSR use since the frequency of this interrupt can be used to determine the load on the system. Background TSRs such as PRINT can wait until the system is idle to perform their tasks.

The keyboard interrupt is invoked by the keyboard controller. When a key is depressed, the standard BIOS INT 9H handler simply reads the key code from the controller and places it in the keyboard buffer. TSRs that hook into this interrupt can read data from the keyboard controller and determine if the key pressed is the hot key used by the TSR. It is normal TSR practice not to immediately go active during the keyboard interrupt. Instead, a flag is set that is monitored by the idle and timer interrupts. When these interrupts see the flag set, they can bring the TSR to the foreground at the proper time. The timer interrupt is invoked by the system hardware 18.2 times every second. The timer interrupt routine first calls the previous INT 8H routines, then determines if the hot key flag is set. If the hot key has been pressed and the system is in a proper state, the TSR can be switched to the foreground.

Activating the Resident Portion

Determining when to go active is the most important of all the problems associated with TSRs. A handful of rules must be obeyed or the TSR will simply lock up the system, forcing the user to reboot. First, the TSR must not already be active. This is important because TSRs are rarely reentrant since most TSRs switch to an internal stack. Second, the system should not be processing any BIOS functions, since the BIOS is not reentrant. Finally, MS-DOS must be in a stable state.

Determining if MS-DOS is in a stable state has been the bane of TSR programmers since the beginning. Over the years, programmers have learned the proper combination of documented and undocumented functions necessary to determine the state of MS-DOS.

When a program calls an MS-DOS function, MS-DOS saves the current value of SS:SP in the calling program’s Program Segment Prefix (PSP), then it switches to one of three internal stacks. This action, along with other manipulations, would seem to indicate that MS-DOS is not reentrant. But that’s not completely true.

The three internal MS-DOS stacks are used for three purposes. One is used for the character I/O functions, 00H to 0CH, and under DOS 2.x, Functions 50H and 51H. The second stack is used for all remaining MS-DOS functions with the exception of a handful of functions that use the calling program’s stack. The third stack is used by MS-DOS when processing critical errors. The trick for TSRs is to determine what stack MS-DOS is currently using and force MS-DOS to use another stack when it is calling an MS-DOS function.

To determine what stack MS-DOS is currently using, you can check two flags. The InDOS flag is set whenever MS-DOS is processing a function. By checking this flag, the TSR can determine if any of the fancy stack manipulation tricks will be necessary to call an MS-DOS function. The ErrorMode flag, sometimes called the Critical Error Flag, is set by MS-DOS when processing a critical error and therefore when using its third internal stack.

If the ErrorMode flag is set, indicating MS-DOS is processing a critical error, the TSR should not go active. During a critical error, MS-DOS uses the error mode stack to process the error; the function stack is in use since MS-DOS was processing a function when the error occurred. The prudent TSR simply waits until the error has been resolved.

While the ErrorMode flag indicates trouble for the TSR, the InDOS flag is another matter. It is possible to call most MS-DOS functions even when the InDOS flag is set as long as MS-DOS is currently processing a character I/O function. If the TSR is entered via the MS-DOS idle interrupt, MS-DOS is waiting for a character I/O function and therefore the noncharacter functions are available for use.

The character I/O functions can be used by the TSR even with the InDOS flag set as long as the TSR sets the ErrorMode flag before calling those functions. This method was required for getting and setting the PSP under DOS 2.x, since those functions used the character I/O stack. Actually, most character I/O functions can be duplicated using the file handle functions and the BIOS, so they’re not necessary. If you must use the character I/O functions, MS-DOS has to be set in the proper state before each function call. The code in Figure 8 forces MS-DOS to use the DOS error stack instead of the I/O stack for the character I/O functions.

Figure 8 Forcing the Use of the MS-DOS Error Stack

doscall proc near

assume cs:code

cmp ah,0ch ;See if I/O call

jbe doscall_2

cmp cs:[dos_version],300h ;See if DOS = 3.0

ja dospspcall_1

cmp ah,59h

je doscall_2

doscall_1:

cmp cs:[dos_version],30Ah ;See if DOS < 3.1

jae dospspcall_ok ;no, just call DOS

cmp ah,50h

je doscall_2

cmp ah,51h

jne doscall_ok

doscall_2:

push ds

push di

lds di,cs:criterr_ptr ;get crit err flag adr

inc byte ptr [di] ;Set DOS in crit error state

pop di

pop ds

int 21h ;Call DOS

push ds

push di

lds di,cs:criterr_ptr ;get crit err flag adr

dec byte ptr [di] ;Clear DOS crit state

pop di

pop ds

ret

doscall_ok:

int 21h ;Call DOS

ret

doscall endp

Even after the TSR has determined that it can go active, care must be taken in how it affects the system. The TSR should first switch to its own internal stack to avoid overflowing the foreground program’s stack. This makes the TSR nonreentrant but is much safer than simply guessing that the foreground program’s stack is large enough to support the TSR.

The TSR must also save the state of the system. This involves saving the state of all the configurable parameters of the system that the TSR may change while it is active. A simple rule for TSRs is that if any parameter is going to be modified, its state must be saved beforehand and restored before the TSR returns control to the foreground application.

The Critical Error, Ctrl-C, and Ctrl-Break interrupts must be saved and modified to point to your own handlers (inside the TSR). This prevents the TSR from causing an interrupt to the foreground application while the TSR is active. The TSR must save and restore these vectors each time it goes active since both MS-DOS and other applications tend to modify them.

The state of the memory drivers must also be saved. The expanded memory page map must be saved before it is modified by the TSR. Fortunately, this simply means instructing the expanded memory manager to perform a save. The return code should be checked to ensure that the state was actually saved. Many expanded memory managers can run out of space when saving their state.

The new MS-DOS 5 extended memory manager (XMM) and UMBs require their states to be saved by the TSR. For the XMM, this is simply a task of saving the state of the A20 address line. The state of the A20 line must be saved and restored whether or not the TSR uses extended memory, since any call to MS-DOS may modify the state of this line. For the UMBs, the TSR should save the memory allocation strategy as well as the state of the link to the UMBs.

While it is fine for a TSR to allocate memory while active, it should not keep this additional memory after it returns control to the foreground application. Many applications check available memory only once, then assume that that amount is available as long as they run. TSRs that sneak in and grab additional memory may cause problems for the foreground applications. It is best for TSRs to allocate any long-term memory when they are installed.

The address of the current PSP should be saved with the Get PSP function, Function 50H. While the TSR is active, the current PSP should be changed to the TSR’s PSP. While MS-DOS normally uses the PSP to keep track of file operations, the current PSP is also used by other programs such as task switchers to monitor what program is active. Since the Get and Set PSP calls are especially quick, these calls should not affect system performance.

If a TSR will perform any file operations, the address of the Data Transfer Address (DTA) should be saved as well as the extended error information. The address of the DTA must be changed not only for FCB operations but also for the find file functions 4EH and 4FH. The MS-DOS extended error information must also be saved. This is the data returned by the Get Extended Error Information function (Function 59H). Since a TSR can cause disk error conditions to occur, it must not return this error information to the foreground application. Figure 9 shows how to save MS-DOS extended error information. A TSR that performs disk operations often modifies the current drive and directory, so this information should also be saved.

Figure 9 Saving Extended Error Information

mov ah,51h ;Get current PSP

call dospspcall ;Beware DOS 2.0 - 3.0

mov saved_psp,bx ;save it

push cs ;Set active PSP to TSR

pop bx ; PSP.

mov ah,50h

call dospspcall

mov ah,2fh

int 21h ;Get current DTA

mov word ptr saved_dta,bx ;save it

mov word ptr saved_dta[2],es

mov dx,offset command_tail ;use PSP for DTA

mov ah,1ah ;Set DTA

int 21h

cmp word ptr dos_version,030ah ;Save Error info for

jb skip_err_save ; DOS 3.1 and later

push ds ;save DS

mov ah,59h ;Extended error info

int 21h ;Call DOS

mov cs:[errDS],ds ;save returned DS

pop ds ;Restore DS

push bx

mov bx,offset errinfoarray ;Save data in registers

mov [bx],ax ; in this specific order.

pop [bx+2]

mov [bx+4],cx

mov [bx+6],dx

mov [bx+8],si

mov [bx+10],di

mov [bx+14],es

skip_err_save:

mov ah,19h ;Get current disk

int 21h

mov curr_disk,al

mov ah,47h ;Get current directory

xor dl,dl

mov si,offset curr_dir

int 21h

Finally, the state of the keyboard and mouse must be saved. Saving the keyboard state is simply a matter of saving the state of the shift lock keys. The mouse state can be saved using the Save Mouse State call (INT 33H Function 16H) of the mouse driver. The size of the state information can be computed using the return driver storage requirements (INT 33H Function 15H).

While saving the state of all parameters that may change is important, it is also important not to save and restore the states of parameters that will not be modified by the TSR. Limiting the save/restore process to the necessary steps improves performance and saves code.

Once the system state has been saved, the TSR can proceed with its real work. The TSR can use most MS-DOS functions with consideration to the limitations on character I/O calls listed above. With the character I/O limitations, TSR programmers usually implement their own keyboard input routines. The design of these routines should involve more than a simple poll of the BIOS keyboard buffer.

Since TSRs tend to use the MS-DOS idle interrupt to determine when the system is idle, it is important for any program polling the keyboard to issue MS-DOS idle interrupts also. While the TSR is waiting for a key, it should invoke INT 28H to indicate that it is idle. The existence of Windows means issuing an idle interrupt is not enough.

After the idle interrupt has been issued, a TSR should also issue a multiplex interrupt with AX=1680H to release the current timeslice. This call signals Windows enhanced mode or OS/2Ò version 1.2 that the current MS-DOS session is idle and other processes can be run. Issuing this call can improve system performance since the system isn’t wasting time devoting resources to an MS-DOS session that is simply idle.

When the TSR has completed its work, the state of the system should be restored. The process is essentially the reverse of the save state process. The only interesting call during this process is the Set Extended Error Information function. This function, available since DOS 3.1, is now documented in MS-DOS 5.

While the restore state process seems simple enough, it must be performed with care to ensure good system performance. An interesting fact is that many of the calls to MS-DOS to set the state of a parameter are much slower than the corresponding Get State calls. For this reason, you should limit the number of Set State calls. To improve TSR performance, the state of a parameter should be first queried, and reset only if it has changed from its original foreground state.

Starting Up the Transient Portion

While discussing the resident portion of the TSR is interesting, other tasks must be performed by the transient portion of the TSR before it is installed. The transient code must determine if the TSR is previously installed, find the InDOS and ErrorMode flags, as well as hook into the necessary interrupts.

First, the TSR needs to determine if a copy of itself has been installed. There are two standard methods for finding an installed copy of a TSR. The first method simply scans each segment for a block of memory that matches the first few bytes of the program image. Using this method, care must be taken not to mistake an image of the program in a disk buffer for the installed program. Also, 386 memory managers (not just EMM386) could put the installed program in a UMB above conventional memory, so upper memory must be scanned also.

A theoretically easier method of finding an installed copy of the TSR is to use the MS-DOS multiplex interrupt. The problem with the multiplex interrupt method is the scarcity of device IDs. With only 64 available to applications, it is possible that a given device ID may be used by more than one program. To avoid confusion, a TSR must return some confirmation that it is actually the desired program. A simple method is to return a pointer to itself with the install check response (see Figure 10). The calling program then compares its own code to the code pointed to by the returned pointer. This method conveniently provides the segment of the installed program for use in exchanging data. While both methods for finding installed copies of the TSR work equally well, it should be remembered that the multiplex method cannot be used for programs needing to be compatible with DOS 2.x.

Figure 10 Checking Device IDs

mov cx,16 ;Try 16 different IDs.

mov ah,0DBh ;Start with ID 0DBh

mov [DevID],ah

find_copy:

xor ax,ax

mov es,ax

mov ah,[DevID] ;Load ID.

int 2fh

or al,al ;See if ID used.

jne find_copy0

jmp short find_copy1

find_copy0:

push cx

call cmpheader ;See if correct TSR by

pop cx ; comparing file headers.

je find_copy_found

find_copy1:

inc byte ptr [DevID] ;ID used by another program.

loop find_copy ; Change and try again.

stc ;All IDs used, set error flag

ret

find_copy_found:

clc

ret

To determine the location of the InDOS flag, simply use MS-DOS Function 34H to return a pointer to InDOS. This function, previously documented only in the MS-DOS Encyclopedia, is now listed in the MS-DOS Programmer’s Reference. Function 34H, Get InDOS Pointer, has been available since DOS 2.0.

Finding the location of the ErrorMode flag is a bit trickier than finding the InDOS flag. There has never been a function, documented or undocumented, that returns the address of the ErrorMode flag. The undocumented function 5D06H, available since DOS 3.1, returns a pointer to the MS-DOS swappable data area. Since the ErrorMode flag is the first byte in this area, this function has been used by many TSR programmers to determine the ErrorMode flag address.

The MS-DOS Encyclopedia documents a method of scanning the MS-DOS kernel code segment for the first occurrence of a series of opcodes that point to the ErrorMode flag. This bizarre method works for DOS versions from 2.0 up to MS-DOS 5.

While MS-DOS 5 still does not provide a function that returns the ErrorMode flag address, the MS-DOS Programmer’s Reference documents the location of the ErrorMode flag as the byte preceding the InDOS flag. Actually, this has been true since DOS 3.1. For TSRs that need to be compatible with versions before DOS 3.1, the opcode scan method is the best method for finding the ErrorMode flag.

A Generic TSR

TEMPLATE.ASM is a generic TSR template that demonstrates the ideas discussed here (see Figure 11), from using expanded memory to dealing with MS-DOS task switching.

TEMPLATE isn’t a terribly useful program. Once installed, TEMPLATE simply waits for the Alt-Left Shift hot key combination, beeps once, waits for another key, then returns control to the foreground program.

As with all TSRs, the transient installation code of TEMPLATE is located at the end of the code segment to allow it to be discarded when TEMPLATE terminates and stays resident. The installation code simply determines if the program has been previously installed, parses the command line, and if not already installed, installs itself.

TEMPLATE has two command-line switches, /E and /U. The /E switch instructs TEMPLATE to allocate one page of expanded memory, and to save and restore the expanded memory context when it is run. The /U switch instructs the transient portion of TEMPLATE to remove the resident code if possible.

TEMPLATE finds the location of the ErrorMode flag either by using the location of the InDOS flag or using the opcode search method documented in the MS-DOS Encyclopedia. If running version MS-DOS 5 or greater, TEMPLATE simply decrements the pointer to the InDOS flag. Otherwise the opcode scan method is used (see Figure 12). The opcode scan method provides for the early versions of DOS that neither have the 5D06H function nor put the ErrorMode flag immediately before the InDOS flag.

Figure 12 The Opcode Scan Method

; Search the DOS kernel segment for the following code sequence:

; CMP [ErrorMode],0

; JNE near addr

; INT 28h

mov ah,34h ;Get ptr to INDOS in ES:BX

int 21h

mov ax,3e80h ;CMP opcode

mov cx,-1 ;Max segment size

mov di,bx ;Start at INDOS address

find_cef_1:

repne scasb ;Scan for CMP

jcxz find_cef_notfound ;Error if CMP not found

cmp es:[di],ah ;Check other half of CMP opcode

jne find_cef_1

cmp byte ptr es:[di+4],075h ;Check for JNE

jne find_cef_1

cmp word ptr es:[di+6],028cdh ; Check for Int 28h call

jne find_cef_1 ;Resume loop if not found

find_cef_found:

inc di

mov bx,es:[di] ;Get offset of ErrorMode flag

clc

find_cef_found:

ret

find_cef_notfound:

stc

jmp short find_cef_exit

The resident portion of TEMPLATE goes to great pains to save the state of the system. All of the potential state information is saved and then restored later by TEMPLATE. While waiting on a keystroke, TEMPLATE calls an idle routine that issues both an MS-DOS idle interrupt as well as a Release Timeslice call. Routines are provided to allocate, map, and free expanded memory. The state of the A20 line is also saved and restored.

Critical errors are handled by a trivial INT 24H handler. This handler simply returns a flag to MS-DOS to fail the function that caused the error. The critical error handler is installed early in the state save process since a critical error can occur while TEMPLATE is saving the state of the machine. The Ctrl-C and Ctrl-Break interrupts are disabled by pointing those vectors to IRET instructions.

One item you will not find in TEMPLATE is interrupt headers. Interrupt headers were first specified in the original PS/2Ò BIOS references as a method for device drivers to follow an interrupt chain to its end. The intent was to allow device drivers and TSRs that were being unloaded to unhook themselves from an interrupt chain. While this is a noble goal, few TSRs apply this technique. This severely limits the usefulness of having any interrupt headers. In fact, even the MS-DOS 5 TSR, DOSKEY, does not use interrupt headers. Another limitation of using interrupt headers is that even if a TSR can be removed with other TSRs installed after it still in place, the block of memory freed by the removed TSR cannot be effectively used by MS-DOS due to the MS-DOS memory allocation scheme. For these reasons, interrupt headers are not used in TEMPLATE.

TSRs have become more complex over the years, and the task of creating them is becoming more and more of an inexact science. With the documentation of the necessary MS-DOS calls, and the inclusion of a workable task switcher API, MS-DOS 5 has improved the stability of TSRs and the systems that run them.

1For ease of reading, "MS-DOS" refers to the Microsoft MS-DOS operating system. "MS-DOS" is a trademark that refers only to this Microsoft product and does not refer to such products generally.

2For ease of reading, "Windows" refers to the Microsoft Windows graphical environment. "Windows" is a trademark that refers only to this Microsoft product and does not refer to such products generally.