EuroAssembler Index Manual Download Source Macros

Sitemap Links Forum Tests Projects


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:

  1. one small for the copy of master environment strings, and
  2. the second block for the Program Segment Prefix (PSP) and adjacent program body.

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.

Before installation After InstallConventional │ │ │ │ │ free, │ │ │ │ allocated │ │ │ │ to COM │ │ │ │ program │ │ │ │ │ │ │ ┌ │░░░░░░░░░░░│ │ │ │ │░░░░░░░░░░░│ │ │ │ │░░░░░░░░░░░│ │ │ │ │░░░░░░░░░░░│ │ │ │ │░░░░░░░░░░░│ │ │ │ │░░install░░│ │ │ │ │░░░code░░░░│ │ │ │ │░░░░░░░░░░░│ │ │ │ Main│░░░░░░░░░░░│ │ │ │ │░░░░░░░░░░░│ │ │ │ TsrTop├───────────┤ ┐ │ │ │ │▓▓▓▓▓▓▓▓▓▓▓│ │ │ │ COM ─┤ │▓▓resident▓│ │ │ │ file │ │▓▓▓code▓▓▓▓│ ├─┐ │ │ │ │▓▓▓▓▓▓▓▓▓▓▓│ │ │ │ │ │ │▓▓▓▓▓▓▓▓▓▓▓│ │ │ │ │ │ │▓▓▓▓▓▓▓▓▓▓▓│ │ │ │ │ │ NewInt├───────────┤ ┘ │ │ │ │ │░JMP Main░░│ │ │ │ └ 100h ├───────────┤ │ │ │ ┌ │░░░░░░░░░░░│ │ │ │ │ │░░░░░░░░░░░│ │Shift │ │ │ │░░░░░░░░░░░│ │ │ │ │ │░░░░░░░░░░░│ │ │ │ │ │░░░░░░░░░░░│ │ │ free │ │ │░░░░░░░░░░░│ │ │ │ │ │░░░░░░░░░░░│ │ │ │ │ │░░░░░░░░░░░│ │ │ │ │ │░░░░░░░░░░░│ │ ┌ ├───────────┤ ┐ │ │░░░░░░░░░░░│ │ │ │▓▓▓▓▓▓▓▓▓▓▓│ │ PSP ─┤ │░.DTA░░░░░░│ │ │ │▓▓resident▓│ │ │ 80h ├───────────┤ └>┤ │▓▓▓code▓▓▓▓│ │ │ │░.FCB░░░░░░│ │ │▓▓▓▓▓▓▓▓▓▓▓│ │ │ ├───────────┤ │ │▓▓▓▓▓▓▓▓▓▓▓│ │ │ │.Reserved53│ │ │▓▓▓▓▓▓▓▓▓▓▓│ │ │ 53h ├───────────┤ └ 53h ├───────────┤ │occupied │ │░░░░░░░░░░░│ │░░░░░░░░░░░│ │ by TSR │ │░░░░░░░░░░░│ │░░░░░░░░░░░│ │ │ ├───────────┤ ├───────────┤ │ │ │░.EnvSeg░░░│─┐ │░.EnvSeg░░░│ │ │ 2Ch ├───────────┤ │ 2Ch ├───────────┤ │ │ │░░░░░░░░░░░│ │ │░░░░░░░░░░░│ │ │ │░░░░░░░░░░░│ │ │░░░░░░░░░░░│ │ └ 00h ├───────────┤ │ 00h ├───────────┤ ┘ │ MCB │ │ │ MCB │ └───────────┘ │ └───────────┘ ┌───────────┐ │ ┌───────────┐ │░░░░░░░░░░░│ │ │ │ │░env. vars░│ │ │ free │ │░░░░░░░░░░░│ │ │ │ ├───────────┤<┘ ├───────────┤ │ MCB │ │ MCB │ └───────────┘ └───────────┘

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:

DOS. NumLock is kept on in Windows 95|98 too, but not in Windows NT and higher versions.
See also
tsrclock for a more primitive TSR program..
euroasm tsrup.htm
Run /I
%Active   %SETC '$'            ; Markers used to modify the first byte of TsrIdentifier.
%Passive  %SETC '-'
tsrup  PROGRAM Format=COM
          INCLUDE "doss.htm"   ; Uses the structure PSP.
          INCLUDE "dosapi.htm" ; Uses macros DosAPI and StdOutput.
          JMP Main:            ; Fixed entry point of COM programs requires to skip the resident division.
Interrupt routine

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).

Interrupt handlers should perform very fast, without waiting for user interaction or huge data manipulation.

When routine NewInt08 is called by hardware-invoked interruption signal, CS:IP points to NewInt08, other registers are undefined and should 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:
          PUSH DS      ; Execute only once per 16 interruptions.
           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
         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.
         ALIGN 16
TsrTop:                ; Here the resident code ends.
The main program body reads the switch of requested action (installation or uninstallation) from the command-line, performs the action and writes informative or help message to the Standard Output.

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
     ; Simplified parsing will get the requested action (DL='I','C','U')
     ;  from the first letter found on the command line.
     MOV SI,PSP.CmdArgSize
     SUB AX,AX
     MOV CX,AX     ; Number of characters in command line.
     JECXZ Help:   ; If program launched without arguments.
.10: LODSB         ; Get the next character.
     OR AL,'X'^'x' ; Convert a letter to 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
.u:  AND AL,~'X'^'x' ; Convert the letter to upper case.
     MOV DL,AL       ; Requested action ('C','I' or 'U') is now in 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 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 from PSP.Reserved53 upwards.
     ; The resident code TsrBottom .. TsrTop will be shifted backward to spare occupied memory.
Shift EQU OFFSET#TsrBottom - PSP.Reserved53 ; The distance is known at assembly-time.
      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,OFFSET# TsrTop / 16     ; Number of paragraphs for the resident code.
      CALL AllocUMB
      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.
ZF=1 if not installed yet,
ZF=0 if the TSR was already installed in memory.
ES= paragraph address of the installed resident (either in upper or in conventional memory).
InstallationCheck PROC Dist=Near
     MOV BX,TsrIdentifier-Shift ; Offset in the installed instance.
     MOVB [BX],'%Active' ; Temporarily switch ShiftedTsrIdentifier to active state.
     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.
     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 current segment DS,
     JE .40    ;  ignore this and continue searching bellow the current instance.
.90: MOVB [BX],'%Passive' ; Restore the passive status back.
    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 paragraph address 0x0A000) 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 automaticly allocated by DOS function 0x48. DOS must be explicitly told to add upper memory to the chain of free memory blocks and we must also temporary change its default allocation strategy, which by default prefers conventional memory.

CX= is the amount of requested memory in paragraphs (OWORDs).
CF=0 if allocation to HIMEM was successful.
ES=paragraph address of allocated memory.
CF=1 if upper memory could not be allocated.
AllocUMB PROC Dist=Near
     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 we should restore previous state.
      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).
This procedure installs the resident in a standard way, leaving the shifted resident part in memory.
InstallConventional PROC
     MOVB [TsrIdentifier-Shift],'%Active'
     DosAPI AX=0x2508,DX=NewInt08-Shift ; Set Interrupt Vector 08 to the new handler at DS:DX.
     TerminateStayResident (OFFSET#TsrTop-Shift)/16, 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 this is achieved with two device drivers: HIMEM and EMM386. Resident programs can be loaded to 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 are helpless.

TSRup installed to upper memory occupies mere 128 bytes of resident memory, see MEM /C.

ES= is preallocated segment of upper memory (as returned by AllocUMB. Its size must be at least OFFSET# TsrTop - Shift bytes.
is not applicable.
InstallUpper PROC Dist=Near
     SUB SI,SI
     SUB DI,DI
     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.
       MOV CL,SIZE# TsrIdentifier - 1
     REP MOVSB      ; Name of the resident (without status prefix) is now set in its MCB.
     DosAPI AH=0x50 ; Set 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.
     TerminateStayResident (OFFSET#TsrTop-Shift)/16,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.

ES= segment of TSR which is being uninstalled (conventional or upper memory).
CF=0, TSR memory is freed, TsrIdentifier status is marked Passive.
CF=1 if TSR cannot be uninstalled.
Uninstall PROC
     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 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.
   ENDP Uninstall
      ENDPROGRAM tsrup

▲Back to the top▲