Skip to content
FreeDOSDeep Dive Published Updated 7 min readViews unavailable

Device Drivers on FreeDOS: How .SYS Files Extend the Kernel

A source-backed tour of FreeDOS device headers, character and block drivers, request packets, entry points, INIT, status words, and loading risks.

An installable FreeDOS device driver exposes a DOS device header and processes typed request packets from the kernel. The header links the driver into a device chain, classifies it as character or block, and supplies two entry offsets called strategy and interrupt. The latter name is easy to misread: it is the request-processing entry point in the DOS driver protocol, not necessarily a hardware interrupt service routine.

The interface, not the filename, makes a driver

FDCONFIG.SYS or CONFIG.SYS loads a driver with DEVICE= or DEVICEHIGH=. The file often ends in .SYS, but FreeDOS configuration also loads some driver-form binaries with other extensions. What matters is the expected in-memory header and entry protocol, not a modern executable-file association.

DEVICE=C:\FDOS\BIN\HIMEM.SYS
DEVICEHIGH=C:\FDOS\BIN\XCDROM.SYS /D:FDCD0001

The path and options are driver-specific. /D:FDCD0001 in the kernel documentation is an example for an optical driver; a later CD redirector must use the same identifier. Inventing switches from another driver can prevent boot or direct hardware operations incorrectly.

DEVICEHIGH attempts an Upper Memory Block and falls back to conventional memory. It does not turn an unsafe driver into protected code. DOS drivers share the kernel’s address space and privilege; a bad pointer can overwrite the operating system or application memory.

The device header is a compact registration record

FreeDOS’s struct dhdr in hdr/device.h defines the contract used by the kernel:

Offset  Size  Meaning
00h     4     Far pointer to next device header
04h     2     Attribute word
06h     2     Strategy routine offset
08h     2     Interrupt routine offset
0Ah     8     Character name or block-device unit data

The next pointer builds a chain. The kernel can traverse the installed devices without a registry or dynamic object model. The attribute word’s high bit distinguishes character devices, while other flags advertise IOCTL, removable-media, console, clock, and related capabilities.

For a character driver, the final eight bytes hold a fixed-width device name. For a block driver, the corresponding header area conveys unit information rather than a filename-like name. A developer must follow the correct interpretation; treating the union as text for every device corrupts registration semantics.

Character and block drivers expose different behavior

Character devices handle streams and named endpoints. DOS built-ins such as CON, NUL, PRN, and AUX explain why filenames can refer to devices in many directories. Opening NUL uses a character device, not an ordinary directory entry on disk.

Block drivers expose one or more sector-addressable units that DOS can assign drive letters and mount through a BIOS Parameter Block. They receive operations such as media check, build BPB, read, write, and removable-media query. A RAM disk can present block semantics even though its storage is memory rather than a physical disk.

Not every peripheral belongs in either category as an installable driver. CTMOUSE, for example, is commonly loaded as an executable resident program. Hardware may also be accessed through BIOS services, packet-driver APIs, or a TSR-owned interrupt interface. The .SYS model is important but not the only DOS extension mechanism.

Strategy and interrupt form a two-entry request protocol

The kernel prepares a request packet and calls the driver’s strategy entry with a far pointer, conventionally in ES:BX. The strategy routine records that pointer. The kernel then calls the driver’s interrupt entry, which reads the saved request and performs or dispatches the operation.

A minimal conceptual outline is:

strategy:
    mov  word [request_ptr], bx
    mov  word [request_ptr+2], es
    retf

interrupt:
    ; inspect command in saved request packet
    ; set status bits and command-specific results
    retf

The far segment and offset must both be preserved. Saving only BX, as simplified examples sometimes show, loses the segment and can address unrelated memory.

The historical reason for the split should not be improvised as a general reentrancy solution. The specification defines two entries and DOS calls them in sequence; hardware interrupt handling, asynchronous completion, and internal driver serialization remain the individual driver’s responsibility.

Every request begins with a common 13-byte prefix

FreeDOS’s primary request structure starts with a byte length, unit byte, command byte, 16-bit status, and eight reserved bytes. Command-specific fields follow in a union.

Byte 0      request length
Byte 1      unit number
Byte 2      command code
Bytes 3-4   status word
Bytes 5-12  DOS-reserved
Bytes 13+   command-specific payload

Read and write requests add media information, a far transfer-buffer pointer, a count, and a starting sector. Generic IOCTL requests add category, function, registers, and a parameter pointer. A driver must first validate that it supports the command and packet shape before using later fields.

Command codes in FreeDOS include initialization, media check, build BPB, IOCTL input/output, input, nondestructive read, status, flush, output, open, close, removable-media checks, and generic IOCTL. Character and block devices are not required to implement every command. Unsupported requests should return the documented error status rather than executing arbitrary fall-through code.

The status word reports done, busy, and error

FreeDOS defines S_DONE as bit 0100h, S_BUSY as 0200h, and S_ERROR as 8000h; the low byte carries an error code when the error bit is set. Defined errors cover write protection, unknown unit, not ready, unknown command, CRC, bad length, seek, media, sector-not-found, paper, write, read, and general failure.

A driver must set completion state consistently. Returning without DONE, reporting an error without a meaningful code, or writing past the request length can leave the kernel waiting, misreport the failure, or corrupt the caller.

These status bits are the device-request protocol. They are separate from the Carry Flag and AX error convention applications see through many INT 21h file calls; the kernel translates between layers.

INIT is a one-time negotiation

Command 00h, C_INIT, is sent when the driver loads. The INIT payload lets a block driver report its number of units and BPB array; it also returns an ending address. Drivers commonly place one-time detection and message code after the resident portion and return an end address that allows DOS to release the unused initialization tail.

Initialization may parse the command-line tail, probe hardware, reserve memory, install a hardware vector, or reject an unsupported configuration. Any probe with side effects must be conservative. A driver that guesses at I/O ports can hang a machine before the shell or recovery batch runs.

The driver should not assume initialization will be called again. Settings that must survive belong in the retained resident area, not discarded code or temporary loader storage.

Driver order changes behavior and memory use

FreeDOS processes DEVICE lines top to bottom. Memory managers must exist before drivers that depend on their services. An optical redirector must come after the low-level device it names. Filter or replacement drivers may need an earlier device in the chain and preserve a pointer for forwarding.

Upper-memory allocation makes order a packing problem. Loading several small drivers can fragment UMBs so a later large driver falls low. Measure with FreeDOS memory tools; do not assume every DEVICEHIGH request succeeded.

The chain pointer is also not permission to patch arbitrary headers. A driver that hooks or replaces another device must preserve the exact forwarding contract and be prepared for a different implementation or attribute set.

Hardware interrupts are an additional layer

A serial, network, sound, or storage driver may install a CPU interrupt handler for its physical device. That ISR can receive data or acknowledge hardware independently of the DOS driver’s “interrupt routine.” The names overlap historically, but the call paths differ.

Real hardware can interrupt while DOS or an application is active. Drivers must protect shared state, observe controller timing, and avoid unsafe DOS calls from an ISR because DOS is generally not reentrant. A strategy/interrupt request pair does not remove those constraints.

Test drivers as kernel-level code

Start in an emulator snapshot with a copied configuration and a minimal boot option that omits the new driver. Validate unknown-command handling, missing hardware, repeated reads, boundary counts, write protection, and unload/reboot behavior. For storage drivers, use expendable images and compare sectors before and after tests.

On physical hardware, image disks first and retain separate boot media. Add one driver line at a time. A boot hang after the driver’s banner is evidence about initialization; corruption after I/O points to request parsing, transfer pointers, counts, or hardware handling.

DOS’s interface is small enough to study directly in FreeDOS source, but its lack of isolation makes precision essential. The header, far pointers, 13-byte prefix, command-specific union, and status bits are an ABI, not suggestions. That compactness makes the model educational—and makes one incorrect offset capable of crashing the entire system.

Related:

Sources:

Comments