Installing and Removing a DOS INT 1Ch Timer-Tick Hook Safely
Chain an INT 1Ch handler without stealing BIOS timer work, keep interrupt-time code bounded, and remove a TSR hook without corrupting the vector chain.
INT 1Ch is a useful place for a DOS program or terminate-and-stay-resident utility to observe periodic timer ticks without taking ownership of the hardware timer interrupt. The BIOS timer handler invokes the software timer hook as part of its tick processing. That makes INT 1Ch a higher-level notification point, but it does not make the handler safe for arbitrary DOS work: it still runs in interrupt context, can interrupt DOS while DOS is busy, and must finish quickly.
RBIL describes INT 1Ch as called by the INT 08h timer handler on each clock tick. The IBM PC/AT reference documents the underlying BIOS/interrupt behavior. The traditional PC tick cadence is about 18.2 Hz, but software should avoid turning that historical rate into a precision timing guarantee across clones, emulators, or systems that reprogram the timer.
Why hook INT 1Ch instead of INT 08h
INT 08h is the hardware timer interrupt path. The BIOS handler acknowledges and updates system timing state, then invokes INT 1Ch for applications that want a tick notification. Replacing or mishandling INT 08h can break the BIOS time-of-day counter, keyboard-related timing, floppy motor shutdown, and other platform behavior. INT 1Ch provides a less invasive hook point for simple periodic bookkeeping.
It is still a hook into shared system state. The handler may interrupt a program during a DOS critical section or while another ISR is active. Do not call DOS file, memory, console, or allocation services from the handler unless a specific reentrancy protocol proves it safe. The general design is to increment a small counter or set a flag, then perform the real work later in foreground code.
Chain without losing the previous handler
Before installation, capture the old vector through the DOS interrupt-vector service or a documented vector API. Install the new handler only after its code and data are resident. On each tick, preserve every register and flag your code changes, do bounded integer-only work, and pass control to the old handler exactly once. A common assembly pattern ends with a far jump to the old handler so its interrupt return resumes the original interrupt frame. The exact prologue and chaining form must match the assembler, memory model, and old handler convention.
Do not call the previous handler both with a far call and a far jump, and do not return with IRET before chaining if that would skip the previous routine. Either mistake can duplicate or suppress tick processing. Conversely, do not send an extra programmable-interrupt-controller end-of-interrupt command from an INT 1Ch hook; the hardware INT 08h owner is responsible for the hardware interrupt lifecycle. INT 1Ch itself is the BIOS’s software callback point.
Installation and removal must account for races. Updating an interrupt vector is a short critical operation; use the DOS API or a safe interrupt-disabled sequence that preserves the prior interrupt flag. On uninstall, check that your handler is still the current vector before restoring the previous pointer. If another TSR installed after yours, blindly restoring your saved vector would erase that newer handler from the chain. A robust TSR either refuses unsafe removal, cooperates with a documented chain-removal protocol, or leaves itself resident.
Keep the interrupt path tiny
The handler should avoid allocation, blocking, BIOS disk I/O, printing, and lengthy arithmetic. A safe minimal path might increment a tick counter, set a “work pending” flag, and chain. Protect shared multi-byte state if the foreground code can read it while the ISR updates it; on 16-bit CPUs a 32-bit counter is not atomically readable. Briefly disable interrupts around a foreground snapshot or use a sequence-counter pattern designed for 16-bit access.
If the foreground loop may sleep or wait for input, ensure it checks the pending flag at a bounded interval. Do not attempt to reconstruct exact elapsed wall time from a tick counter without considering wrap and timer reprogramming. A lost or delayed tick can happen in poorly behaved environments, and an emulator may coalesce or virtualize timer delivery.
Interrupt handlers should not use floating-point instructions or assume stack depth is unlimited. They execute on the interrupted program’s stack unless a specific environment provides another stack. Keep local storage minimal and preserve segments if changed. A single runaway chain can consume stack on every tick, so test the control flow and ensure each previous handler eventually returns to the original interrupt frame.
TSR residency and lifecycle
A transient utility can install a hook only while it remains resident. A TSR must retain the handler code, saved previous vector, and data the handler touches when it terminates-and-stays-resident. Any referenced buffer on the discarded transient stack becomes invalid. Keep the state within the resident block or allocate it through a lifecycle that remains valid after termination.
At startup, detect whether your TSR is already installed using a stable signature or multiplex interface rather than installing duplicate copies. If a duplicate handler increments the same work counter, it can double the effective timer rate and make removal harder. If the TSR supports unload, it must verify chain position and quiesce its handler before releasing resident memory. A vector pointer pointing into freed DOS memory is a delayed crash, not a clean uninstall.
For an ISR written in assembly, define the register-preservation set explicitly. Save general and segment registers that your code changes, preserve the direction-flag contract expected by interrupted code, and return with a valid interrupt frame through the established chain. If the handler touches shared state, keep the critical section to the few instructions required for the update. Avoid invoking floating-point code, compiler runtime helpers, stack probes, or instrumentation whose reentrancy is unknown; “small C function” does not necessarily mean bounded, self-contained ISR code.
The cadence is also not a deadline scheduler. At the traditional PC rate, a tick arrives only about every 55 milliseconds, so work performed after each tick can be late by nearly one interval even before system load or virtualization adds delay. If the foreground program needs a timeout, represent elapsed ticks with wrap-aware arithmetic and make its polling interval explicit. If it needs finer resolution, choose a separate timer design rather than increasing work inside the callback.
When diagnosing missed or doubled work, instrument the foreground consumer with a sequence number and count how many flags were coalesced. A single Boolean flag means “at least one event occurred,” not “exactly one tick occurred.” Use a counter if multiplicity matters, with a width and atomic-read strategy appropriate to the CPU. Saturate rather than wrap if an overloaded foreground loop can leave the counter unattended for a long time.
Testing and diagnostics
Test under the target FreeDOS version, relevant memory managers, and emulators. Verify that the BIOS time counter still advances, the old handler is called once per tick, and the handler’s stack/register preservation is correct. Exercise install twice, remove while another test TSR sits above the hook, and confirm that unsafe removal fails safely. Log from the foreground loop, not from the ISR.
For timer-sensitive software, compare the hook’s count against a separate clock source only as a diagnostic, not as proof that every system tick was delivered. Test counter wrap and verify that the foreground snapshot cannot tear. If the application needs high-resolution or deadline timing, a simple INT 1Ch hook is usually the wrong abstraction; use a documented timer mechanism and accept responsibility for its hardware interactions.
Operational rule
Treat INT 1Ch as a periodic notification, not an invitation to run a background application inside an ISR. Capture and chain the previous vector once, do only bounded work, keep all referenced state resident, and remove the hook only when you still own the top of the chain. Leave INT 08h timer accounting and PIC acknowledgment to the BIOS handler that owns them.
Related:
- PC Programmable Interval Timer: 8253/8254 Channels and DOS Boundaries
- TSR Programs: How DOS Ran Background Tasks Without Multitasking
Sources: