Terminate and Stay Resident (TSR) are 16bit programs which provide services for other DOS programs. This is similar to device drivers which are installed to memory during DOS boot. Unlike devices, properly written TSR programs can be installed ad hoc and easily removed from memory when they are no longer needed.
To understand how TSRs are installed into memory, we must be familiar with the concept of memory allocation in DOS. MS-DOS quantitizes memory into 16 bytes long chunks called paragraphs. Each block of memory is prefixed with one additional paragraph, Memory Control Block (MCB), which holds information about its block and links to the next, immediately following MCB. When a process requests a memory block allocation, DOS must rearrange the chain of memory blocks, reserve continuous block of requested size and mark it as occupied by the process in question. When user launches a COM program from command line, the command interpreter (typically COMMAND.COM) allocates two memory blocks:
All available conventional memory is given to the executed program.
DOS will deallocate (=mark as free) both memory blocks when the program terminates normally. If TSR program wants DOS to not deallocate all its memory upon termination, it has to exit with call to special DosAPI function AH=31h and tell DOS how many paragraphs of terminating program's memory should be left allocated. Such resident block decreases the amount of memory available for launching other programs.
The largest possible executable program size can be inspected with the DOS utility
MEM.EXE
.
Conventional memory is a critical resource in DOS. All devices and TSR programs should be optimised to minimise the occupied amount of resident memory.
This example TSR shifts the resident code at installation and overwrites FCB and DTA portion of PSP which would otherwise remain unused. With this technique it occupies only 128 bytes.
TSRUP is a skeleton of simple Terminate and Stay Resident (TSR) program for DOS which keeps the NumLock indicator ON all the time while it is installed. Of course, this is not much useful function, but its functionality is easy to extend.
This sample also demonstrates some techniques how to keep the size of memory permanently occupied by the TSR as low as possible:
euroasm tsrup.htm
tsrup.com /I
All program with Format=COM must start at CS:256, so we must put a JMP instruction in the beginning to skip the resident code.
EUROASM %Active %SETC '$' ; Markers used to modify the first byte of TsrIdentifier. %Passive %SETC '-' tsrup PROGRAM Format=COM,Width=16 INCLUDE "doss.htm" ; Uses the structure PSP. INCLUDE "dosapi.htm" ; Uses macros DosAPI and StdOutput. [COM] SEGMENT Purpose=CODE+DATA+RODATA JMP Main: ; Fixed entry point of COM programs requires to skip the resident division.
New handler for interrupt(s) does the actual TSR's job. The far jump at its end chains this handler to previously installed handlers (if any) and, finally, to the original BIOS handler which sends acknowledgement to the interrupt controller 8259 and provides the final IRET from the interruption.
This program hooks interrupt 8 which is invoked by hardware timer every 55 ms. As we don't need to update NumLock status that often, the update code will be executed only once per 16 interruptions.
The actual function of this TSR, keeping NumLock ON permanently, is achieved by periodical setting NumLock flag in ROM-BIOS area. The NumLock LED indicator will keep pace with this bit (at least in DOS and Windows9x).
When routine NewInt08 is called by hardware-invoked interruption signal, CS:IP points to NewInt08,
other registers are undefined and must be preserved.
Instruction JMP 0:0
near its end (immediate segment:offset in the JMP instruction body)
will be referred as OldInt08 and replaced with current INT 08 vector taken from the interrupt table
at installation-time.
Actual resident code is claimed between labels TsrBottom and TsrTop.
TsrBottom: ; Here starts the resident code. NewInt08 PROC DIST=FAR ; New handler for interrupt which does the actual TSR's job. PUSHF ; All registers and flags must be preserved. PUSH AX MOV AL,[CS:IntCounter] INC AL CMP AL,16 JB .Skip: ; Execute only once per 16 interruptions. PUSH DS SUB AX,AX MOV DS,AX ; Keyboard status in ROM-BIOS will be referenced at segment 0. OR [0x0417],0x20,DATA=BYTE ; Set NumLock status bit in ROM-BIOS area at 0:0417h. POP DS .Skip: MOV [CS:IntCounter],AL POP AX POPF JMP 0:0 ; Continue with older INT 08 handlers. OldInt08 EQU $-4 ; Previous vector value is kept in the body of JMPF instruction. ENDP NewInt08 IntCounter DB 0 ; Incremented on every INT 08 invocation, zeroed when it reaches 16. TsrIdentifier DB '%Passive','%^PROGRAM' ; Unique identifier of this program in memory, actually -tsrup. TsrTop: ; Here the resident code ends.
MsgHelp DB "Installed resident program '%^PROGRAM' keeps the NumLock permanently ON.",13,10 DB "Options: /C alias Conventional memory installation",13,10, DB " /I alias Install to upper memory (HIMEM)",13,10 DB " /U alias Uninstall from memory.",13,10,0 Main PROC ; Simplified parsing will get the requested action (DL='I','C','U') ; from the first letter found on the command line. MOV SI,PSP.CmdArgSize CLD SUB AX,AX LODSB MOV CX,AX ; Number of characters in the command line. JECXZ Help: ; If program launched without arguments. .10: LODSB ; Get the next character. OR AL,'X'^'x' ; Convert the letter to a lower case. Dispatch AL,'c','i','u' ; Acceptable first letters of supported actions. LOOP .10: ; Otherwise try the next character. Help:StdOutput MsgHelp ; If no valid action was specified. TerminateProgram Errorlevel=8 .c: .i: .u: AND AL,~'X'^'x' ; Convert the letter to upper case. MOV DL,AL ; Requested action ('C','I' or 'U') is now in the register DL. ; Program doesn't use environment variables, they can be freed now. MOV ES,[PSP.EnvSeg] ; Paragraph address of environment is at DS:0x002C. DosAPI AH=0x49 ; Release the block of memory at segment ES. ; Prepare to hook interrupt handlers. DosAPI AX=0x3508 ; Get current INT 08 vector to ES:BX. MOV [OldInt08+0],BX ; Let the new handler continue with the old vector ES:BX. MOV [OldInt08+2],ES ; PSP is no longer needed, it may be overwritten fromPSP.Reserved53
upwards. ; The resident codeTsrBottom .. TsrTop
will be shifted backward to spare the occupied memory. Shift EQU TsrBottom - PSP.Reserved53 ; The amount is known at assembly-time (0xB0 bytes). PUSH DS POP ES ; Restore ES back to the common PSP segment. MOV SI,TsrBottom ; Start of resident code. MOV DI,PSP.Reserved53 ; New offset of resident code. MOV CX,TsrTop - TsrBottom ; Size of the shifted resident in bytes. REP MOVSB ; Perform the shift. Dispatch DL,'I','U' ; Perform the requested action stored in DL. ; Action 'C': Conventional TSR installation to lower memory. CALL InstallationCheck JZ .40: .30: StdOutput =B"'%^PROGRAM' is already installed, use '%^PROGRAM Uninstall' first.",Eol=Yes TerminateProgram Errorlevel=4 .40: StdOutput =B"'%^PROGRAM' was installed to conventional memory.",Eol=Yes JMP InstallConventional .I: ; Action 'I': Install TSR to upper memory. CALL InstallationCheck JNZ .30: MOV CX,TsrTop-Shift+15 SHR CX,4 CALL AllocUMB ; Allocate CX paragraphs of the upper memory. JNC .50: StdOutput =B"'%^PROGRAM' could not be installed to upper memory. Use '%^PROGRAM Conv' instead.",Eol=Yes TerminateProgram Errorlevel=4 .50: StdOutput =B"'%^PROGRAM' was installed to upper memory.",Eol=Yes JMP InstallUpper .U: ; Action 'U': Uninstall from memory. CALL InstallationCheck JNZ .60: StdOutput =B"'%^PROGRAM' was not installed yet.",Eol=Yes TerminateProgram Errorlevel=4 .60: CALL Uninstall JNC .70: StdOutput =B"'%^PROGRAM' could not be uninstalled, some other TSR program was installed after it.",Eol=Yes TerminateProgram Errorlevel=4 .70: StdOutput =B"'%^PROGRAM' was uninstalled from memory.",Eol=Yes TerminateProgram Errorlevel=0 ENDP Main
Good TSR program will not allow installation if it already was installed. This requires some kind of installation check. This procedure provides detection by searching for a string TsrIdentifier at its shifted offset, which appears in installed resident in %Active state (the TsrIdentifier is by default in %Passive state).
During the InstallationCheck will be the status temporarily switched to %Active and the active TsrIdentifier is searched for at all possible segments (0xFFFF0..0x00000).
TSR identifier should be unique among other TSR programs which use the same identification method to find out if they are installed in memory. It is predecessed with %Active or %Passive character ($ or -) which distinguishes between the active TSR resident block and its other copies.
Without the active/passive status prefix the installation check would be fooled by old invalid copies of TsrIdentifier which might accidentally occur in deallocated memory or in cache buffers.
InstallationCheck PROC Dist=Near PUSHAW MOV BX,TsrIdentifier-Shift ; Offset in the installed instance. MOVB [BX],'%Active' ; Temporarily switch ShiftedTsrIdentifier to active state. CLD SUB AX,AX ; This register will hold inspected segment address. .40: DEC AX ; Check all segments from 0xFFFF downto 0x0000. MOV ES,AX ; Returned segment candidate. JZ .90 ; Jump with ZF=1 if bottom reached and still not found. MOV DI,BX ; Offset in the searched segment. MOV SI,BX ; Offset in the current segment. MOV CX,SIZE# TsrIdentifier ; Including the status-byte prefix. REPE CMPSB JNE .40 ; If not found, repeat with the next lower paragraph address. MOV CX,DS ; Match was found but CMP CX,AX ; if it was at the current segment DS, JE .40 ; ignore this and continue searching bellow the current instance. .90: MOVB [BX],'%Passive' ; Restore the passive status back. POPAW RET ; ZF=1 if not installed yet else ZF=0 and ES=segment found. ENDP InstallationCheck
This procedure tries to allocate block of upper memory and returns its segment in ES.
Upper memory (above linear address 0x0A0000) is not available in MS-DOS without two memory devices which must be loaded in CONFIG.SYS:
Even with those devices the upper memory is not automatically allocated by DOS function 0x48. DOS must be explicitly told to add upper memory to the chain of free memory blocks and we also need to temporary change its default allocation strategy, which by default prefers conventional memory.
AllocUMB PROC Dist=Near PUSHAW DosAPI AX=0x5800 ; Get alloc strategy (0=first fit, 1=best fit, 2=last fit). JC .90 ; Fails on DOS version older than 3. MOV SI,AX ; Save the current strategy to SI to be restored later. DosAPI AX=0x5802 ; Get UMB Link Status (0=convent.only 1=including upper). JC .90 ; Fails on DOS version older than 5. MOV DX,AX ; UMB Link Status will be saved to DL. DosAPI AX=0x5803,BX=1 ; Set UMB Link Status to 1=include upper memory. DosAPI AX=0x5801,BL=0x41 ; Set Allocation Strategy to 0x41=upper best fit. DosAPI AH=48h,BX=CX ; Try to allocate upper memory block, paragraph size is in CX. PUSHF ; Save the result of allocation (CF=error). MOV ES,AX ; Segment address of UMB if allocation succeeded. SUB BX,BX ; In any case previous state should be restored. DosAPI AX=0x5803,BL=DL ; Restore UMB Link Status from DL. DosAPI AX=0x5801,BX=SI ; Restore Allocation Strategy from SI. POPF ; Return with ES=allocated segment (valid only if CF=0). .90:POPAW RET ENDP AllocUMB
InstallConventional PROC MOVB [TsrIdentifier-Shift],'%Active' DosAPI AX=0x2508,DX=NewInt08-Shift ; Set Interrupt Vector 08 to the new handler at DS:DX. MOV DX,TsrTop-Shift+15 SHR DX,4 TerminateStayResident DX, Errorlevel=0 ENDP InstallConventional
With version 5, MS-DOS brought out the concept
of using memory above the conventional limit 0x9000:0xFFFF, also called
UPPER memory. In Microsoft DOS it is achieved with two device drivers:
HIMEM and EMM386. Resident programs can be loaded to the upper memory
using internal command LOADHIGH but there's a rub.
Upper memory is often fragmented and LOADHIGH must find continuous
block of upper memory large enough for the whole program, not only for the
portion which remains resident.
This example uses better solution: the program is launched in conventional
memory, it allocates only a little slot in upper memory and moves
the resident part there by itself. Thanks to this technique it can benefit
from upper memory in cases where LOADHIGH or MEMORYMAKER don't work.
TSRup installed to upper memory occupies mere 128 bytes of resident memory, see MEM /U
.
TsrTop - Shift
bytes.
InstallUpper PROC Dist=Near
SUB SI,SI
SUB DI,DI
CLD
MOV CX,TsrTop-Shift ; Head of PSP and adjacent resident size.
REP MOVSB ; Copy the block with shifted resident to the upper memory.
MOVB [ES:TsrIdentifier-Shift],'%Active'
PUSH DS
PUSH ES
POP DS ; DS is temporarily set to upper memory segment ES.
DosAPI AX=0x2508,DX=NewInt08-Shift ; Set interrupt vector 08 to the upper memory at DS:DX.
POP DS
MOV AX,ES
MOV BX,AX ; Save the upper memory segment to BX.
DEC AX ; Its Memory Control Block is 1 paragraph below. Let's update the name of its owner.
MOV ES,AX ; ES:0 is now MCB of our upper memory block.
MOV DI,MCB.Name ; At this offset in MCB should be the name of the block owner.
MOV SI,TsrIdentifier+1-Shift ; The name displayed with MEM /C
, actually tsrup.
%IF SIZE# TsrIdentifier > 1+8
MOV CL,8 ; Maximum possible size of block owner name.
%ELSE
MOV CL,SIZE# TsrIdentifier - 1
%ENDIF
REP MOVSB ; Name of the resident (without status prefix) is now set in its MCB.
DosAPI AH=0x50 ; Set the current PSP to BX=segment of upper memory.
PUSH DS
POP ES ; ES now points to the running PSP in conventional memory.
DosAPI AH=0x49 ; Now deallocate the whole program whose PSP is pointed by ES.
; The following instructions actually run in deallocated memory but they are not overwritten yet by DOS.
MOV DX,TsrTop-Shift+15
SHR DX,4
TerminateStayResident DX,Errorlevel=0 ; Terminate PSP in upper memory.
ENDP InstallUpper
Uninstallation of previously installed TSR program is possible only if the current vector INT 08 points to the instance of our resident program. Otherwise it means that some other TSR program has been installed after this TSRup and hooked the same INT 08 routine.
Uninstall PROC PUSHAW PUSH DS SUB AX,AX MOV DS,AX ; Interrupt table is at segment 0. MOV AX,ES ; Segment of previously installed instance. CMP AX,[4*08h+2] ; Uninstalled TSR should be pointed to by the interrupt table. POP DS STC ; Return with carry flag if the interrupt vector JNE .90 ; does not point to the segment of deallocated resident. PUSH DS ; Otherwise unhook the interrupt. LDS DX,[ES:OldInt08-Shift] DosAPI AX=0x2508 ; Restore Interrupt Vector 08 from DS:DX. POP DS MOVB [ES:TsrIdentifier-Shift],'%Passive' ; Prevent InstallationCheck to find this uninstalled instance. DosAPI AH=0x49 ; Deallocate TSR memory at segment ES. .90:POPAW RET ENDP Uninstall
ENDPROGRAM tsrup