Skip to content
FreeDOSDeep Dive Published Updated 7 min readViews unavailable

DOS File Control Blocks: Record I/O and Legacy Compatibility

Understand the DOS FCB layout, DTA hazards, sequential and random records, extended attributes, and when FreeDOS programs should use handles instead.

The DOS File Control Block (FCB) is a caller-supplied structure used by the original record-oriented file services exposed through INT 21h. An FCB carries a classic drive-and-8.3 filename together with record size and position fields. The DOS API can open a file through that structure, then read or write sequential records, address a record by number, or transfer a block of records.

FCBs still matter when maintaining older DOS software, reading a PSP created for a child program, or investigating a compatibility problem. They are not the preferred starting point for new FreeDOS applications: DOS 2.0 introduced handle-based calls, and Microsoft’s later programming references recommend those calls for ordinary opens, reads, writes, and seeks. The distinction is architectural, not merely syntactic.

The standard FCB is a fixed-width structure

A standard FCB is 37 bytes (25h). The first twelve bytes hold a drive number and a filename padded into an eight-byte name plus a three-byte extension. The remainder stores DOS bookkeeping and the caller-controlled record position. The fields are byte offsets relative to the start of the FCB:

Offset Size Field
00h 1 Drive number; zero means the current/default drive
01h 8 Filename, space padded
09h 3 Extension, space padded
0Ch 2 Current block
0Eh 2 Logical record size, 128 bytes by default
10h 4 File size
14h 2 Date
16h 2 Time
18h 8 Reserved bytes
20h 1 Current record within the block
21h 4 Random record number

The date and time fields use DOS’s packed representation, not a Unix timestamp. The current block and current-record fields form the sequential position; the four-byte random-record field addresses a record independently. Do not treat the structure as a portable disk format or assume its reserved bytes are application storage. Its fields form an API contract interpreted by DOS.

An extended FCB prepends a seven-byte prefix: a marker byte FFh, five reserved bytes, and a file-attribute byte, followed by the regular FCB. DOS uses this form for calls where an attribute mask is required, such as operations that search for or create files with non-default attributes. A routine accepting an ordinary FCB must not accidentally receive the extended form without accounting for that prefix: the standard fields would then appear seven bytes later.

FCB file services are record-oriented

The classic services include AH=0Fh to open an existing FCB file and AH=10h to close it. AH=14h and AH=15h perform sequential reads and writes. AH=21h and AH=22h read or write the record selected by the FCB’s random-record field, while AH=27h and AH=28h transfer multiple consecutive records. AH=29h can parse a filespecification into an FCB. These calls generally return small status values in AL; callers must interpret the function-specific result rather than assuming the handle API’s carry-flag convention.

The record-size field is set to 128 bytes when a file is opened. A program may change it after a successful open and before file I/O. Sequential services advance the block and current-record fields as records are processed. Random single-record calls use the random-record number to choose a location; software that continues random access should set that field for each request. Random block operations have their own count and position-update behavior, so a loop that assumes all FCB calls advance identically can skip, repeat, or overwrite data.

For example, after a successful open, a program could choose 256-byte records and request record number three. The 32-bit random record field occupies offsets 21h through 24h; on an 8086, write it as two 16-bit words rather than using a 32-bit operand:

; DS:DX points to the opened FCB when calling INT 21h.
; Prepare a 256-byte record and select zero-based record 3.
mov word ptr [fcb+0Eh], 256
mov word ptr [fcb+21h], 3
mov word ptr [fcb+23h], 0

; Point DOS at a buffer of at least 256 bytes first.
mov dx, offset record_buffer
mov ah, 1Ah
int 21h

mov dx, offset fcb
mov ah, 21h
int 21h
; AL=00h: complete record; 01h: EOF/no record;
; AL=02h: DTA boundary/size failure; 03h: partial final record.

This illustrates the interface rather than a complete application: a real program must check the open result, preserve the intended data segment, allocate an adequate buffer, and handle every documented return. The random record number is zero based, so record number three means the fourth record. The partial-record status is distinct from both a complete read and an end-of-file result with no data.

The DTA is a hidden part of every transfer

FCB reads and writes use the process’s Disk Transfer Area (DTA), a buffer whose address DOS tracks separately from the FCB. INT 21h/AH=1Ah changes that address. If a program has not set one, DOS uses the default at offset 80h in the Program Segment Prefix, the same area that initially contains the command tail. An FCB read can therefore overwrite command-line bytes unless the program moves the DTA before it performs I/O.

The DTA must hold the requested record or entire block. Its segment and offset also matter: an FCB operation that would cross a segment boundary can fail with a DTA/segment-wrap status. Size the buffer from the record size and the requested record count, and keep it within the addressable region instead of assuming the default 128-byte buffer is sufficient. This is especially important for block calls, where a seemingly correct FCB can still direct DOS to write beyond the allocated memory.

Why handle-based calls became the normal interface

The handle API uses INT 21h/AH=3Dh to open, AH=3Fh and AH=40h to read and write byte counts, and AH=42h to move a file pointer. It is better suited to stream-oriented and redirected I/O than an FCB’s fixed filename and record model. The open call also accepts access and sharing modes that FCB open does not express in the same way. Its errors use carry-flag and extended-error conventions, unlike the compact FCB return codes.

This does not make FCBs irrelevant or invalid. A compatibility layer may need to translate an older caller’s FCB into a handle-based operation while preserving details such as record position, partial final records, wildcard parsing, and legacy status values. Such translation requires tests against the DOS versions and applications being supported; replacing every FCB call with a superficially similar handle call can change observable behavior.

FreeDOS retains the configuration directive FCBS=nnn for compatibility, but its documentation says that FreeDOS ignores the requested reservation count and dynamically simulates FCBs from handle data as needed. This is an implementation-specific fact, not a universal DOS rule. Do not infer from the directive’s presence that modern FreeDOS reserves the same fixed FCB table that an older DOS configuration may have controlled.

A practical debugging checklist

When an old program reports a damaged file or corrupted memory, check the structure and buffer together:

  1. Confirm whether the pointer addresses a standard FCB or a seven-byte-prefixed extended FCB.
  2. Confirm that the drive/name/extension fields are correctly padded and the record-size field matches the intended format.
  3. Set the DTA before any FCB transfer; ensure it is large enough and cannot wrap across the segment boundary.
  4. For random access, write all four bytes of the relative-record number, then check the exact AL result.
  5. For writes, test against a disposable disk image and verify that the target record range and file length are correct.
  6. Compare behavior on the actual FreeDOS or DOS version being targeted, especially for wildcard parsing, partial records, extended attributes, and any redirector or network filesystem involved.

For new code, use file handles unless a specific compatibility requirement calls for FCB semantics. For old code, treat the FCB, DTA, record number, and status byte as one coupled interface. Most FCB bugs are not mysterious: they come from a stale DTA, a size mismatch, a misaligned extended prefix, or assumptions about record-position updates that the selected function does not make.

Related:

Sources:

Comments