TSR Programs: How DOS Ran Background Tasks Without Multitasking
A forensic guide to DOS TSR residency, interrupt hooks, chaining, INT 28h and InDOS reentrancy, multiplex IDs, memory ownership, safe unloading, and testing.
FreeDOS normally gives one foreground program control of the machine, yet mouse drivers, keyboard utilities, clocks, caches, and pop-up tools can remain active after their installers return to the shell. A Terminate and Stay Resident program does this by preserving part of its memory and arranging for interrupts or explicit calls to re-enter that resident code. It is event-driven residency, not a hidden preemptive scheduler.
Termination and residency are separate design steps
A normal program commonly exits through DOS INT 21h, function 4Ch. DOS records the return code, restores the parent context, and releases the process’s memory. Function 31h changes the memory outcome: AH=31h terminates while keeping the number of 16-byte paragraphs specified in DX; AL supplies the return code.
The kept size must cover the Program Segment Prefix, resident code, resident data, and any resident stack or buffers. Converting bytes to paragraphs requires rounding up, not truncating:
paragraphs = (resident_end - PSP_segment_base + 15) / 16
The exact linker layout matters. A label relative only to the program’s code origin can undercount the PSP or over-retain transient installer code. A production TSR should inspect its memory model and map file rather than paste a formula whose symbols came from another assembler.
The older INT 27h service also stays resident, but it has tighter historical semantics and limitations. INT 21h/AH=31h is the normal DOS 2-and-later interface. Neither service makes parked bytes execute again: the installer must establish a callback path before terminating.
Interrupt hooks are a shared linked structure
The real-mode Interrupt Vector Table contains 256 far pointers. During installation, a TSR normally obtains the existing handler with DOS function 35h, saves the complete segment:offset address, and installs its own handler with function 25h.
Conceptually:
mov ax,3509h ; get vector 09h
int 21h ; ES:BX = previous handler
; save both ES and BX
mov dx,new_handler ; DS:DX = replacement
mov ax,2509h ; set vector 09h
int 21h
Saving only the offset is a fatal bug because handlers need not share a segment. The installer should perform DOS calls while it is still an ordinary foreground program; directly writing the IVT adds synchronization and portability hazards without providing an advantage here.
Most hooks must chain to the handler they replaced. That preserves BIOS behavior and TSRs installed earlier. The chain is order-sensitive: the newest hook points to the previous hook, which eventually reaches the original handler. A TSR must use a chaining convention appropriate for that vector and ensure the interrupt is acknowledged exactly once.
Hardware handlers run in a hostile context
A hardware interrupt can arrive while the foreground application is manipulating its own data, while DOS is inside a non-reentrant service, or while another interrupt handler is active. The CPU initially enters using the interrupted program’s stack. A correct handler preserves every register and segment value its contract requires, establishes access to its own data, bounds stack use, and finishes quickly.
INT 09h is the traditional keyboard IRQ vector, but intercepting it requires understanding keyboard-controller and programmable-interrupt-controller behavior. A handler that consumes a scancode incorrectly, acknowledges the controller twice, or fails to chain can disable input. A hotkey detector should do the minimum necessary and set a resident flag; it should not open a file or draw a complex interface inside the keyboard IRQ.
The hardware timer reaches INT 08h at roughly 18.2 ticks per second in the traditional PC configuration. The BIOS normally performs its bookkeeping and invokes INT 1Ch as a user timer hook. When INT 1Ch is sufficient, using it avoids taking ownership of the hardware-level acknowledgement path. A program that hooks INT 08h directly must preserve the BIOS timer chain or system time and other timer clients may stop working.
DOS services are generally not reentrant
Calling an ordinary INT 21h function from a random hardware handler is unsafe. The interrupted foreground program may already be inside DOS, whose internal stacks and data structures are not generally reentrant. A second file operation can corrupt state even though both calls are individually valid.
DOS function AH=34h returns a pointer associated with the InDOS state. Resident software also needs to consider the critical-error state; an InDOS value that appears idle is not a universal license to call every DOS function. Version-specific rules belong in an interrupt reference and must be tested on the DOS versions claimed.
INT 28h is DOS’s idle interrupt, invoked while certain console input functions wait. It gives TSR software a documented historical opportunity to do deferred work, but it is not a general background thread. Ralf Brown’s Interrupt List records which DOS calls are allowed from this context and documents version caveats. A TSR should set a small flag in the hardware hook, then act only when a verified safe context arrives.
For work that does not need DOS—updating a private counter, buffering a byte, or writing directly to already-owned memory—the handler can remain independent. File I/O, memory allocation, program execution, and device operations should be deferred or avoided unless their reentrancy contract is explicit.
A pop-up utility must preserve more than the screen
A pop-up over another application inherits unknown video mode, cursor state, active page, keyboard state, stack depth, and memory ownership. Saving only visible character cells may be insufficient for graphics modes or applications which use direct video access. The pop-up also cannot assume that the foreground program permits DOS re-entry merely because the hotkey occurred.
Robust designs separate three phases:
- A minimal keyboard or timer hook detects the trigger and records a request.
- A safe-context hook confirms DOS and critical-error state, prevents recursive activation, and switches to private state if necessary.
- The resident restores all altered video, keyboard, interrupt, and process context before returning to the chain.
An “already active” guard is essential. Without it, a second timer tick or key event can enter resident code while its first invocation is still operating.
Multiplex services prevent accidental duplicate loads
Many TSRs use INT 2Fh, the DOS multiplex interrupt, for installation checks and control functions. The installer calls a chosen multiplex identity and function; the resident copy returns a recognizable signature. This can prevent two instances from claiming the same hotkey and memory.
The byte pattern in a tutorial is not automatically an unused global identifier. INT 2Fh is shared by DOS components, redirectors, drivers, and other TSRs. A real program should follow an established allocation convention such as the Alternate Multiplex Interrupt Specification where applicable, probe for collisions, and return a signature strong enough to distinguish itself from an unrelated service.
The multiplex API also provides a safer path for status queries or an uninstall request than exposing undocumented resident addresses. Inputs, outputs, preserved registers, and version negotiation should be treated as an ABI.
Unloading is safe only from the top of every hook chain
A resident program cannot restore an old vector merely because that is the value it saved at installation. Before unloading, it must read each current vector and verify that it still points to its own handler. If another TSR installed later, the current vector points to that newer handler; restoring the older address would orphan the newer hook and leave it chaining into freed memory.
Every claimed vector must pass the test, not just the keyboard vector. An uninstall routine must also account for its environment block and resident memory ownership, disable new callbacks during teardown, and avoid freeing code that is executing. These details are why many TSRs deliberately provide no unload feature.
Reverse installation order is a necessary rule for simple hook chains, but it is not by itself proof of safety. A buggy program may have replaced a vector without chaining, changed the handler later, or retained a far pointer into the TSR.
Resident memory competes directly with applications
The retained block reduces memory available to subsequent programs. Loading an installer with LOADHIGH or LH may place its resident portion in an Upper Memory Block when DOS and the memory manager can satisfy the allocation. Success depends on available block size and the TSR’s allocation behavior; the command cannot force arbitrary software high.
Load order therefore affects both memory and interrupt order. Changing it to gain a few kilobytes also changes which TSR is above another in shared chains. After optimizing, re-test hotkeys, mouse input, timer behavior, sound, networking, printing, and unloading—not just the number printed by MEM.
Test failures where TSR bugs actually appear
A useful validation matrix includes FreeDOS and every DOS version claimed, physical hardware and the selected emulator, rapid repeated hotkeys, long idle periods, foreground disk activity, critical-error prompts, nested shells, and different video modes. Instrument a development build to detect recursion and stack-water marks. Keep a minimal boot option so a broken handler can be bypassed.
TSRs are compact systems programs operating without memory protection. Their reliability comes from strict interrupt contracts: retain the correct paragraphs, save complete far pointers, chain correctly, defer non-reentrant services, verify ownership before unloading, and measure every configuration change.
Related:
- Understanding Interrupts on FreeDOS: INT 21h and the DOS API
- FreeDOS Memory Management: Conventional, Upper, and Extended Memory
Sources: