Understanding Interrupts on FreeDOS: INT 21h and the DOS API
How DOS exposes its entire system call interface through the software interrupt mechanism, with INT 21h as the single most important entry point.
DOS has no syscall instruction, no VDSO, no structured system call table in the modern sense — the entire DOS API is exposed through the x86 software interrupt mechanism, and one interrupt in particular, INT 21h, is where the overwhelming majority of DOS programs spend their time talking to the kernel.
Software interrupts: a function call via a CPU instruction
The x86 INT instruction triggers a CPU interrupt exactly like a hardware interrupt would, except triggered deliberately by software rather than by a device. Each interrupt number (0x00–0xFF) has a corresponding entry in the Interrupt Vector Table, a simple array of far pointers at the very start of memory, and the CPU jumps to whatever handler that entry points to.
Interrupt Vector Table (at address 0000:0000)
INT 00h → divide-by-zero handler
INT 08h → timer tick (hardware IRQ0)
INT 09h → keyboard (hardware IRQ1)
INT 21h → DOS API dispatcher
INT 13h → BIOS disk services
DOS itself installs its handler for INT 21h during boot — that handler is the actual entry point for essentially the entire DOS system call interface.
INT 21h: one interrupt, hundreds of functions
Rather than each DOS API call getting its own interrupt number, INT 21h is a single dispatcher: the calling program loads a function number into the AH register, sets up any other required registers, then executes INT 21h, and DOS’s handler dispatches based on that function number — the direct equivalent of a Unix syscall number in rax before a syscall instruction.
; Print a string (function 09h)
mov ah, 09h
mov dx, offset message
int 21h
; Read a character (function 01h)
mov ah, 01h
int 21h
; Open a file (function 3Dh)
mov ah, 3Dh
mov al, 0 ; read-only
mov dx, offset filename
int 21h
; on return: AX = file handle, or error code if carry flag set
Error handling: the carry flag and AX
DOS API calls signal success or failure through the carry flag — clear on success, set on failure, with the error code then placed in AX — which is the DOS-era equivalent of checking errno after a failed Unix syscall, just communicated through a CPU flag plus a register rather than a separate global variable.
int 21h
jc error_handler ; jump if carry flag set (call failed)
Other interrupts worth knowing
INT 21h covers the DOS-level API (files, memory, process control, string I/O), but lower-level hardware access typically goes through BIOS interrupts instead — INT 13h for raw disk sector access, INT 10h for video services, INT 16h for keyboard hardware access below the DOS buffered-input layer. A DOS program mixing both — using INT 21h for file I/O and INT 10h for direct screen writes — is normal and reflects the layering: DOS itself is partly built on top of these same BIOS interrupts.
; BIOS: set video mode (INT 10h, function 00h)
mov ah, 00h
mov al, 03h ; 80x25 text mode
int 10h
TSRs: hooking an interrupt to extend the system
A Terminate-and-Stay-Resident program’s whole trick is exactly this interrupt mechanism: it installs itself by saving the current handler address for some interrupt (often a hardware one like the keyboard, INT 09h, or the DOS multiplex interrupt, INT 2Fh), replacing it with its own handler that does its work and then chains to the original — the same “wrap and chain” pattern a kernel module hooking a syscall table entry uses conceptually, just via interrupt vectors instead.
mov ax, 3509h ; get interrupt vector (function 35h) for INT 09h
int 21h
; ES:BX now holds the old handler address, saved for chaining
Why this design is worth understanding
The entire DOS API being exposed through a single dispatched interrupt, with function numbers in registers and errors signaled via a CPU flag, is a strikingly minimal design for an operating system’s calling convention — no memory-mapped syscall tables, no VDSO trampoline, just a CPU instruction whose entire behavior is “look up a vector, jump there.” Understanding INT 21h end to end is genuinely understanding the entire programmatic interface FreeDOS exposes to software — there’s no larger, more complex API surface hiding behind it the way there is with a modern OS’s thousands of syscalls, library calls, and IPC mechanisms layered on top of each other.