| Index | Manual | Download | Source | Macros | Embedded word Case ins.
Sitemap | Links | Forum | Tests | Projects

# tsrup.htm
AllocUMB
Header
InstallationCheck
InstallConventional
InstallUpper
Interrupt_routine
Main
Uninstall

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:

  - it deallocates the block of memory with copy of environment variables assigned to the program,
  - it utilizes the useless tail of PSP, which starts at PSP.Reserved53,
  - it installs itself to the memory above videoram (HIMEM), so the installed TSR doesn't decrease the memory available to other programs at all,
  - it is able to uninstall itself from memory completely, without reboot.

Format
  COM TSR
Platform
  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..
Build
  `euroasm tsrup.htm`
Run
  `tsrup.com /I`

Header

  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.

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

Main
  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
    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 from PSP.Reserved53 upwards.
         ; The resident code TsrBottom .. 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

InstallationCheck

  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.

Input
  -
Output
  ZF=1 if not installed yet,
  ES=0.
Error
  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
        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

AllocUMB

  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:

  DEVICE=HIMEM.SYS DEVICE=EMM386.EXE RAM and/or NOEMS

  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.

Input
  CX= is the amount of requested memory in paragraphs (OWORDs).
Output
  CF=0 if the allocation of HIMEM was successful.
  ES=paragraph address of allocated memory.
Error
  CF=1 if upper memory could not be allocated.

    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
  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.
         MOV DX,TsrTop-Shift+15
         SHR DX,4
         TerminateStayResident DX, Errorlevel=0
        ENDP InstallConventional

InstallUpper

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

Input
  ES= is preallocated segment of the upper memory (as returned by AllocUMB. Its size must be at least `TsrTop - Shift` bytes.
Output
  is not applicable.

    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

Uninstall

  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.

Input
  ES= segment of TSR which is being uninstalled (conventional or upper memory).
Output
  CF=0, TSR memory is freed, TsrIdentifier status is marked Passive.
Error
  CF=1 if TSR cannot be uninstalled.

    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

▲Back to the top▲