Skip to content
FreeDOSDeep Dive Published Updated 7 min readViews unavailable

Understanding Interrupts on FreeDOS: INT 21h and the DOS API

A register-level guide to the interrupt vector table, FreeDOS INT 21h dispatch, file and process calls, Carry Flag errors, BIOS services, TSR hooks, and reentrancy.

Real-mode DOS programs request operating-system services mainly through software interrupts. INT 21h is FreeDOS’s principal application API dispatcher: a program places a function number in AH, other arguments in registers or memory, executes INT 21h, and interprets registers and flags on return. It is not the entire DOS interface—process termination, absolute disk I/O, multiplex services, BIOS calls, and hardware IRQs also use other vectors.

The real-mode vector table contains far pointers

At physical address zero, the Interrupt Vector Table holds 256 entries of four bytes each. Every entry is a little-endian offset and segment. Vector n begins at n * 4, so the table occupies the first 1,024 bytes of memory.

When real-mode software executes INT n, the processor saves return state, obtains the target far address from the vector, and transfers control. The handler eventually uses IRET to restore the saved state. This is a CPU mechanism; DOS builds an API on it by installing handlers and defining register contracts.

Hardware interrupts can enter through the same vector mechanism after the programmable interrupt controller maps IRQs to vector numbers. On a conventional PC mapping, timer IRQ0 appears at INT 08h and keyboard IRQ1 at INT 09h, but remapping is possible. The vector number and physical interrupt line are related by controller configuration, not by the INT instruction itself.

INT 21h is a dispatcher

FreeDOS installs an INT 21h handler during kernel initialization. The handler examines AH and routes control to the requested DOS function. FreeDOS source separates low-level interrupt entry from C routines implementing files, memory, processes, devices, dates, and other services.

This resembles a system-call number, but register conventions differ by function. Some calls use AL as a subfunction, some take a pointer in DS:DX, some use BX as a handle, and some return structures through caller-supplied memory. Saving every register “just in case” is safer than assuming a function preserves values its contract marks undefined.

Programs should target documented DOS behavior, not a transient FreeDOS implementation detail. FreeDOS aims at compatibility, and internal function organization can change while INT 21h register contracts remain stable.

String output requires a dollar terminator

Function AH=09h displays a string at DS:DX until a dollar-sign byte. It does not accept a C-style zero-terminated string and cannot print an embedded dollar sign through the same buffer.

mov  ah, 09h
mov  dx, message
int  21h

message db 'Hello from FreeDOS', 13, 10, '$'

The data segment must actually address message. A COM program often starts with code and data in one segment, while an EXE must initialize DS according to its memory model. Passing an uninitialized DS:DX makes DOS scan unrelated memory until it happens to find $.

Handle-based output through AH=40h accepts an explicit byte count and can write a dollar sign, binary data, files, or standard output handle 1. Choosing the API based on the data contract prevents sentinel bugs.

File handles use Carry Flag error reporting

Function AH=3Dh opens an ASCIIZ path at DS:DX; access and sharing bits are supplied in AL. On success, Carry Flag is clear and AX contains a handle. On failure, Carry Flag is set and AX contains a DOS error code.

mov  ax, 3D00h       ; open read-only
mov  dx, filename
int  21h
jc   open_failed
mov  bx, ax          ; handle for later calls

The caller owns the successful handle and should close it with AH=3Eh. Leaking handles matters because DOS process and system tables are deliberately small. A program must not treat AX as a handle before checking Carry Flag.

The Carry/AX convention applies to many, not all, functions. AH=59h provides extended-error information after a qualifying failure, including class, recommended action, and locus. This is richer than comparing AX to one value, but support can vary across DOS versions and redirectors.

Memory allocation is measured in paragraphs

DOS memory function AH=48h takes a count of 16-byte paragraphs in BX. On success it returns the allocated segment in AX; on failure Carry is set, AX gives an error, and BX can report the largest available block.

mov  ah, 48h
mov  bx, 100h        ; 4096 bytes
int  21h
jc   no_memory
mov  [block_seg], ax

The program later releases the block with AH=49h and the block segment in ES. Passing an arbitrary interior segment can damage the DOS memory-control-block chain. There is no process-level virtual-memory isolation to absorb that mistake.

Resize function AH=4Ah has similar ownership constraints. Resident programs intentionally keep part of an allocation, but ordinary applications should release temporary blocks and terminate through a documented path.

Process termination communicates an exit code

INT 21h function AH=4Ch terminates the process and places its return code in AL. FreeCOM batch scripts can test the resulting level with IF ERRORLEVEL, remembering that the test means greater than or equal.

mov  ax, 4C01h       ; terminate with code 1
int  21h

Older termination mechanisms such as INT 20h have stricter segment assumptions and do not carry the same explicit return-code interface. New DOS software normally uses 4Ch.

An exit code has meaning only by the program’s documentation. Code 1 may mean generic failure, “differences found,” or a user cancellation. Shell scripts should not invent semantics beyond nonzero unless the tool defines them.

BIOS interrupts sit below DOS abstractions

BIOS video INT 10h, disk INT 13h, keyboard INT 16h, and other firmware services operate below many DOS facilities. Direct video calls are common for full-screen DOS programs, while DOS file calls remain preferable for filesystem access because they understand mounted volumes, redirectors, handles, and sharing.

Raw BIOS disk access bypasses FAT allocation and DOS caches. Writing a sector behind DOS’s back can make in-memory metadata disagree with disk and corrupt an active filesystem. Absolute disk interrupts INT 25h and INT 26h are similarly dangerous. Use them only in controlled utilities with locked/exclusive media and verified backups.

BIOS availability also varies under UEFI-only systems and emulators. FreeDOS expects legacy BIOS services or a compatibility layer; an interrupt number alone does not create firmware support.

INT 2Fh is a multiplex extension point

INT 2Fh lets DOS components, redirectors, network software, memory managers, and resident utilities publish multiplex functions. Callers normally probe an installation-check function before invoking other services. The namespace is conventional rather than centrally safe from every collision, so software must follow the owning specification.

FreeDOS kernel 2044 added or refined several INT 2Fh compatibility paths used by redirectors and Windows 3.x. This illustrates why saying INT 21h is the “entire” API is too broad: DOS-compatible software often coordinates through additional standardized interrupts.

TSRs hook vectors but DOS is not reentrant

A TSR can obtain a vector with INT 21h AH=35h, save the returned ES:BX, and install a new handler with AH=25h and DS:DX. Its handler may process the event and chain to the previous far address according to that interrupt’s contract.

mov  ax, 3509h       ; get INT 09h vector
int  21h             ; ES:BX = previous handler

Hooking is not enough for correctness. DOS is generally non-reentrant: calling a normal DOS service from a hardware ISR while another INT 21h is active can corrupt internal state. Resident software checks the InDOS flag obtained via AH=34h, tracks critical-error state where required, or defers work until a safe context such as an appropriate idle callback. Even then, only functions documented as safe for that context should run.

A keyboard ISR must acknowledge hardware/PIC requirements and return quickly. It should not perform file I/O merely because INT 21h is easy to call. Buffer the event and process it later.

Uninstalling is also conditional. A TSR should restore a saved vector only if the current vector still points to itself; otherwise it may remove another program installed later and break the chain.

Interrupt contracts are executable ABI documentation

Register inputs, flags, pointer segments, string termination, and preserved state form an Application Binary Interface. One wrong assumption can write to an unintended device or memory block because real-mode DOS offers little containment.

Use Ralf Brown’s Interrupt List and FreeDOS source together: RBIL records broad DOS and BIOS conventions, while the kernel implementation shows what FreeDOS currently does. Test on the DOS versions claimed by the program. An undocumented behavior observed on one emulator is not automatically a portable API.

Understanding INT 21h is understanding the principal gateway, not every layer. The IVT and CPU provide dispatch, FreeDOS implements DOS services, BIOS interrupts expose firmware, and multiplex or hardware vectors extend the environment. Keeping those responsibilities separate is the foundation of safe DOS systems programming.

Related:

Sources:

Comments