Implementing Background Utilities in MS-DOS using TSRs
Learn how to use the INT 21h/31h 'Keep Process' function to create MS-DOS TSRs, allowing background utilities to run without occupying a program slot.
03 Dec 2025, 16:29 UTC

The Problem: Single-Tasking Limitations
MS-DOS is a single-tasking operating system. Under normal operation, when a program finishes executing, it exits and returns control to the command shell, releasing its memory. This creates a problem for utilities that need to provide global services—such as print spoolers, keyboard macros, or system monitors—because they cannot run "in the background" while another application is active.
The solution is the Terminate and Stay Resident (TSR) technique. A TSR program tells DOS to return control to the user while keeping a specific block of the program's memory allocated and active. This allows the utility to intercept system events via interrupts and execute code transparently to the user.
Mechanics of the "Keep Process" Call
To become a TSR, a program uses the DOS API to mark its memory as reserved. The primary method is INT 21h, Function 31h (Keep Process). Unlike a standard exit, this function requires the programmer to specify exactly how much memory should remain resident.
- Memory Specification: The size is passed in the
DXregister, measured in paragraphs (16-byte blocks). - Return Code: The
ALregister specifies the return code passed back to the parent process. - Interrupt Hooking: Once resident, the program typically "hooks" an interrupt vector. It saves the address of the original handler (e.g., the keyboard handler at
INT 09h), replaces it with its own address, and then calls the original handler at the end of its routine to ensure the system continues to function.
Worked Example: A Minimal Keyboard Hook
The following example is written for MASM/TASM. It creates a TSR that prints a dot (.) to the screen every time a key is pressed. This demonstrates the basic pattern of hooking a hardware interrupt and chaining back to the original handler.
; tiny_tsr.asm
; Assemble with: MASM /ml tiny_tsr.asm
; Link with: LINK tiny_tsr.obj
.model tiny
.code
org 100h
start:
; Save original INT 09h vector
mov ax, 3509h ; Get interrupt vector 09h
int 21h
mov [old_int09], bx ; Save offset
mov [old_int09+2], cx ; Save segment
; Set new INT 09h vector
mov ax, 2509h ; Set interrupt vector 09h
mov dx, offset tsr_handler
int 21h
; Terminate and Stay Resident
mov dx, offset tsr_end ; Size of resident portion
mov al, 0 ; Return code
mov ah, 31h ; Keep Process
int 21h
; --- Resident Handler ---
tsr_handler:
pushf
push cs
call dword ptr cs:[old_int09] ; Chain to original handler
mov al, 2Eh ; ASCII for '.'
mov ah, 02h ; DOS print character
int 21h
iret
old_int09 dd ?
tsr_end:
nop
end start
Verification and Diagnostics
To verify the TSR is functioning and analyze its impact on the system, follow these steps in a DOS environment (such as DOSBox):
- Execution: Run
tiny_tsr.com. You will immediately return to theC>prompt. - Functional Check: Press any key. A dot should appear on the screen, confirming the
INT 09hhook is active. - Memory Check: Run the
MEMcommand before and after loading the TSR. You should see a decrease in "Largest Executable Space" corresponding to the size of the resident block. - Vector Verification: Use the
DEBUGutility to examine the interrupt vector table. Runningg=0000:0044(the address for INT 09h) should show the offset pointing to your TSR's handler rather than the original BIOS handler.
Trade-offs and Engineering Limitations
While powerful, TSRs introduce significant risks to system stability:
- Conventional Memory Pressure: TSRs occupy the first 640KB of RAM. Because this space is contiguous, multiple TSRs can fragment memory, preventing large applications from loading even if total free memory seems sufficient.
- Re-entrancy Issues: Most DOS
INT 21hfunctions are not re-entrant. If a TSR calls a DOS function while the system was already inside a DOS function (which is common during an interrupt), it can cause a system crash. - Chain Corruption: If a TSR fails to call the original interrupt handler or corrupts the stack/flags during execution, the system will likely hang or reboot.
Summary of Action
When implementing a TSR, always prioritize a minimal resident footprint and ensure strict adherence to interrupt chaining. Verify memory consumption using MEM and ensure that any code running inside the interrupt handler avoids complex, non-re-entrant DOS calls to maintain system stability.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.