Before DOS Had Handles

Modern programmers take file handles for granted. A program opens a file, receives a handle, and uses it for subsequent operations such as reading and writing. On Unix-like systems, that handle is a small integer known as a file descriptor:

int fd = open("report.txt", O_RDONLY);
ssize_t count = read(fd, buffer, sizeof buffer);

The descriptor refers to operating-system-managed state for the open file, including its current file offset. The application does not have to manage that state directly.

Early DOS offered a different arrangement. The application allocated a File Control Block in its own memory and passed its address to DOS file system functions. The FCB stored the drive and filename, the file’s size, the length of each logical record, the current position for reading and writing, and fields reserved for DOS. FCBs were also used for operations such as directory searches, so their purpose extended beyond managing opened files.

To understand why DOS employed the File Control Block, we must trace its origins to an earlier operating system: CP/M.

Developed by Gary Kildall in 1974 for Intel’s 8080 processor, CP/M also ran on the compatible Zilog Z80. It became the dominant operating system for many early 8-bit microcomputers, providing applications with a standardized interface for accessing disk files through File Control Blocks.

By 1980, the industry was beginning to adopt Intel’s 16-bit 8086 processor. Kildall’s company, Digital Research, was developing an 8086 version of CP/M called CP/M-86, but its release was repeatedly delayed. Seattle Computer Products, which manufactured 8086 processor boards, grew impatient waiting for an operating system to support its hardware. Its programmer, Tim Paterson, developed an alternative, initially called QDOS (Quick and Dirty Operating System) and subsequently renamed 86-DOS. To make existing CP/M applications easier to translate from the 8080 and Z80 to the 8086, Paterson deliberately reproduced portions of CP/M’s programming interface, including its system calls and File Control Blocks.

Meanwhile, IBM was developing its first personal computer, based on Intel’s 8088 processor. In 1980, IBM approached Digital Research about licensing CP/M-86, but the companies failed to reach an agreement. IBM subsequently turned to Microsoft, which had already agreed to supply its BASIC interpreter, to provide an operating system as well. Microsoft did not have a suitable operating system of its own, so it initially licensed 86-DOS from Seattle Computer Products, later purchasing the rights outright. Microsoft adapted the operating system for the IBM PC, where it became PC DOS. Although its internal implementation differed from CP/M, its programming interface retained much of that operating system’s design.

DOS therefore inherited CP/M’s approach to file management, in which programs used File Control Blocks to identify and manipulate files.

Anatomy of an FCB

For this discussion, we will use the conventional 37-byte FCB layout documented for DOS 2.0 and later. All offsets below are hexadecimal and are measured from the beginning of the ordinary FCB body. Multibyte numeric fields use the x86 little-endian representation.

Offset Bytes Field
00h 1 Drive
01h 8 Filename
09h 3 Filename extension
0Ch 2 Current block
0Eh 2 Record size
10h 4 File size in bytes
14h 2 Last-write date
16h 2 Last-write time
18h 8 DOS-reserved state
20h 1 Current record within block
21h 4 Random-record number

The drive byte contains zero to indicate the default drive (the currently selected disk drive), or a positive number to specify a particular drive (one for A:, two for B:, and so forth). When a file is opened using the default drive, DOS replaces the zero with the actual drive number.

The filename and its extension are stored in separate, space-padded fields, without an intervening dot or terminating NUL character.

Thus, an ordinary FCB for REPORT.TXT on the default drive can begin like this:

fcb             db 0                 ; Default drive
                db 'REPORT  '        ; Exactly eight bytes
                db 'TXT'             ; Exactly three bytes
                db 25 dup (0)        ; Remainder: 37 bytes total

Here and throughout, the examples use MASM-style Intel syntax. DB emits bytes, DUP repeats an initializer, and a trailing h marks a hexadecimal number. MOV places its source operand into its destination operand, written first.

Notice what is absent from the declaration: there are no seven precautionary bytes before fcb.

Also notice what the FCB does not contain: a pathname. An eight-character name and three-character extension leave no place for C:\WORK\REPORT.TXT. Under DOS versions with subdirectories, FCB operations generally work relative to the current directory of the selected drive rather than accepting a complete path in the control block. This was a genuine limitation of the interface.

Opening a File, the DOS Way

With the FCB initialized, we’re ready to ask DOS to open the file. First, however, we need to understand a peculiarity of the original IBM PC’s memory architecture.

The Intel 8088 used a scheme called segmented addressing, in which memory locations were specified using a segment:offset pair. DS:DX means that the DS register supplies the segment and DX supplies the offset within that segment. Loading DX alone is sufficient only when DS already identifies the correct segment. The examples below assume that initialization has been performed.

Applications requested DOS services through software interrupt 21h. For the FCB open operation, AH—the upper byte of the 16-bit AX register—contains 0Fh, and DS:DX identifies the FCB. The result comes back in AL, the lower byte of AX: zero means success and FFh means failure.

                mov dx, OFFSET fcb
                mov ah, 0Fh
                int 21h

                cmp al, 0
                jne open_failed

OFFSET fcb supplies the offset component of the address of the FCB, rather than its contents.

open_failed and similar labels stand for the surrounding program’s error handling.

After a successful open, DOS has filled in information such as the file’s size and its own bookkeeping. The application must not treat the whole structure as scratch space simply because it owns the memory. The interface exposes some fields for application use while reserving others for the operating system.

Reading Records

Unlike the byte-oriented file operations familiar to modern programmers, FCB reads and writes operate on records: fixed-length sequences of consecutive bytes that DOS treats as units of file data.

When DOS opens a file, it initializes the FCB’s record size to 128 bytes. With this default, each sequential read attempts to transfer the next 128 bytes of the file, regardless of their contents. The application can change the record size after opening the file. The data itself is transferred through a separate memory buffer called the Disk Transfer Area (DTA).

To track the current position within a file, DOS uses a block number and a record number within that block. Each block represents 128 logical records, regardless of their size. These blocks are simply a bookkeeping convention and should not be confused with physical disk sectors or allocation units.

For sequential access, the corresponding byte offset is:

byte_offset = (current_block × 128 + current_record) × record_size

The following example allocates a 128-byte buffer to accommodate one record, initializes the FCB’s record position, selects the buffer as the DTA using DOS function 1Ah, and reads a record using function 14h:

record_buffer   db 128 dup (0)

; Code, with DS already initialized:

                mov word ptr [fcb+0Ch], 0     ; Start at block zero
                mov word ptr [fcb+0Eh], 128   ; Use 128-byte records
                mov byte ptr [fcb+20h], 0     ; Start at record zero

                mov dx, OFFSET record_buffer
                mov ah, 1Ah                   ; Set DTA
                int 21h

                mov dx, OFFSET fcb
                mov ah, 14h                   ; Sequential read
                int 21h

Function 14h returns a status in AL, not a byte count. Zero indicates a complete record; one indicates end-of-file without data; two indicates a segment-wrap problem; and three indicates a partial final record, padded with zero bytes. In particular, a nonzero result does not invariably mean that no data was read.

The FCB and DTA serve distinct purposes: the FCB identifies the file and maintains its record size and current position, while the DTA provides the memory buffer for transferring data. Reading or writing a record requires both.

Seven More Bytes

The extended FCB adds attribute information without rearranging the existing body. Its seven-byte prefix contains a marker, five reserved bytes, and an attribute byte. The ordinary 37-byte body follows it, producing a 44-byte object.

Measured from the beginning of the complete extended object, its first fields are:

Offset Bytes Meaning
00h 1 Marker: FFh
01h 5 Reserved; initialize to zero
06h 1 Attributes
07h 37 Ordinary FCB body

The attribute byte supports operations involving specially attributed entries, such as hidden or system files and volume labels. Attribute interpretation depends on the operation; for example, a search mask that includes hidden files is not necessarily a request to return only hidden files.

An extended FCB can be declared in assembly language as follows:

xfcb            db 0FFh              ; Extended-format marker
                db 5 dup (0)         ; Reserved prefix bytes
                db 02h               ; Hidden-file attribute bit

xfcb_body       db 0                 ; Default drive
                db 'REPORT  '
                db 'TXT'
                db 25 dup (0)

There are two useful labels: xfcb identifies the complete representation and xfcb_body identifies the embedded ordinary FCB. A program accessing the record-size field can use xfcb_body+0Eh; the equivalent address is xfcb+15h.

When submitting the extended FCB to DOS, the program loads DX with OFFSET xfcb, not OFFSET xfcb_body. Together, DS and DX form a pointer to the beginning of the extended structure.

That is the essential rule. DOS examines the first byte at the supplied address. If it is FFh, DOS recognizes the extension and advances seven bytes to the ordinary FCB body. Otherwise, the first byte is interpreted as the drive number. Because FFh is not a valid drive identifier, the two representations are unambiguous. DOS never needs to examine memory preceding the supplied address.

Straight from the Source

We needn’t speculate about how DOS handled these structures. In 2014, Microsoft made the original source code for early versions of MS-DOS publicly available through the Computer History Museum, later publishing it on GitHub under an open-source license. We can therefore examine the implementation itself.

The MS-DOS 2.0 source, dating from 1983, reveals exactly how DOS distinguishes an ordinary FCB from an extended one. Here is the relevant sequence from FCB.ASM, with its comments omitted:

                MOV     SI,DX
                LODSB
                MOV     [EXTFCB],AL
                XOR     AH,AH
                CMP     AL,-1
                JNZ     HAVATTRB
                ADD     DX,7
                ADD     SI,6
                MOV     AH,[SI-1]
                LODSB
HAVATTRB:

The routine begins with the caller’s pointer. With the processor’s direction flag clear, indicating forward traversal through memory, LODSB fetches the byte at DS:SI into AL and advances SI by one byte. Comparing the byte register with -1 tests the bit pattern FFh.

For an ordinary FCB, the branch skips the adjustment. For an extended FCB, DX advances seven bytes. SI, already advanced once, advances another six; [SI-1] therefore identifies the attribute byte at original offset six. The final LODSB fetches the drive byte. There is no preliminary read seven bytes before the supplied object.

This was not a later repair to an interface that had behaved as Norton described. The MS-DOS 1.25 source code contains the same first-byte test and seven-byte adjustment, demonstrating that this behavior predates DOS 2.0.

For application code that merely wants a pointer to the ordinary body, the idea reduces to:

; Input:  DS:DX points to a valid ordinary or extended FCB.
; Output: DS:SI points to its ordinary body.
; DX is left unchanged.

                mov si, dx
                cmp byte ptr [si], 0FFh
                jne body_ready
                add si, 7
body_ready:

Leaving DX unchanged is deliberate. SI is now convenient for accessing body fields, while DX still identifies the representation the caller originally supplied. Passing the adjusted body address to DOS would discard the extension’s meaning.

A Matter of Address

Immediately following his criticism, Norton writes:

The FCB is addressed from the beginning of the main part, and all fields within the FCB are referred to in terms of their offset from this address. This places the FCB extension at a negative offset of −7.

Recall our earlier assembly example, where xfcb identifies the beginning of the seven-byte extension and xfcb_body identifies the ordinary FCB that follows it. Norton’s statement suggests that he believed programs should always use the address of xfcb_body.

If DOS always received that address, even when the extension was present, it would have to examine the byte seven positions before it to determine whether an extension existed. Under those circumstances, Norton’s objection would make sense: programs that did not use the extension would have to reserve space for it, merely to indicate its absence.

But DOS works differently. When using an extended FCB, DS:DX points to xfcb. DOS examines the first byte at that address, recognizes the FFh marker, and advances seven bytes to reach the ordinary FCB body. When using an ordinary FCB, DS:DX points directly to its drive byte. DOS never examines memory preceding the supplied address.

Norton’s misconception probably survived because ordinary file operations would generally work correctly, giving him no reason to question his assumption. DOS would treat the supplied address as the beginning of a standard FCB, allowing the program to open, read, and write ordinary files without issue. The seven preceding bytes would simply be ignored.

The mistake would become apparent when working with files that actually required the extension. For example, a program searching for a hidden file might initialize the extension with the FFh marker and the hidden-file attribute, but then pass OFFSET xfcb_body to DOS. The search would not find the hidden file because DOS would never examine the extension. Passing OFFSET xfcb instead would allow the attribute information to be recognized.

A Change of Address

The 1988 second edition, The New Peter Norton Programmer’s Guide to the IBM PC and PS/2, credited to Peter Norton and Richard Wilton, presents the situation differently. Although the extension is still described as “more than a little peculiar,” the explanation has changed. The book now provides separate layouts for the 37-byte ordinary FCB and the 44-byte extended FCB. Under “Extended FCB Fields,” it states:

DOS distinguishes between normal and extended FCBs by examining this byte.

The byte in question is the FFh marker at the beginning of an extended FCB. The text goes on to explain that the corresponding byte in an ordinary FCB contains the drive identifier, which cannot legitimately be FFh. This is precisely how DOS distinguishes the two structures, without examining memory preceding the supplied address.

That edition accurately describes the original DOS implementation. Gone are the complaints about reserving seven bytes for an unused extension—and the “clumsy design” that drove responsible programmers wild.

A Better Handle on Files

DOS 2.0, released in 1983, introduced a Unix-influenced handle-based API alongside hierarchical directories. The older FCB interface remained available for compatibility with existing programs.

Under the new interface, an application supplied a NUL-terminated pathname, and DOS returned a numeric handle. Subsequent operations used that handle to access the file, much as modern programs do today:

pathname        db 'C:\WORK\REPORT.TXT', 0

; Reuse record_buffer from the earlier example.

; DS has already been initialized.

                mov dx, OFFSET pathname
                mov ax, 3D00h                 ; Open, read-only
                int 21h
                jc open_failed

                mov bx, ax                    ; Returned handle
                mov dx, OFFSET record_buffer
                mov cx, 128                   ; Requested byte count
                mov ah, 3Fh                   ; Read through handle
                int 21h
                jc read_failed

; AX contains the number of bytes actually read.

Both functions clear the processor’s carry flag on success and set it on failure. A successful open returns a handle in AX, while a successful read returns the number of bytes transferred. Unlike the FCB interface, which operates on fixed-length records, the new interface allows applications to request an arbitrary number of bytes. Once finished with the file, the application closes the handle using DOS function 3Eh, releasing the associated operating system resources.

The improvement goes beyond replacing a pointer with an integer. DOS now maintains the file’s current position and other bookkeeping internally. The application simply specifies how much data to transfer and where to put it, without having to deal with record sizes or a separate Disk Transfer Area. Unlike the FCB, whose layout is part of the programming interface, a file handle conceals the operating system’s internal data structures. This allows DOS to modify or expand those structures in future versions without requiring changes to applications that use the handle-based interface.

A Long Retirement

Replacing the FCB interface did not mean removing it. The original INT 21h FCB functions survived through MS-DOS 6.22 and into the DOS-based Windows 95, Windows 98, and Windows Me. However, when Windows 95 OSR2 introduced FAT32 in 1996, the old FCB functions could no longer perform ordinary file I/O on FAT32 volumes. A program that worked on a FAT16 drive might fail on a FAT32 drive.

Meanwhile, Microsoft introduced Windows NT in 1993, a separate operating-system family that did not run on top of MS-DOS. Instead, it provided a compatibility environment called the NT Virtual DOS Machine (NTVDM), which allowed older DOS applications, including those using FCBs, to continue running.

The NT architecture became the foundation for Windows XP and every subsequent version of Windows. NTVDM provided DOS compatibility through 32-bit Windows 10, but it was never included in 64-bit Windows. Thus, an interface introduced with DOS 1.0 in 1981 was still supported, albeit imperfectly, in 32-bit Windows 10 when it was released in 2015. Because Windows 11 is exclusively 64-bit, it includes neither NTVDM nor support for the original DOS FCB functions.

But FCBs have yet to disappear. In January 2026, the FreeDOS kernel maintainers released fixes for FCB filename parsing, renaming, and directory searches. An interface superseded in 1983 was still receiving bug fixes more than four decades later.

The Final Count

FCBs certainly had their shortcomings. Their record-oriented operations, fixed-length filenames, and dependence on separate data buffers made them cumbersome compared with the newer handle-based interface. But requiring an unused seven-byte extension to announce its own absence was not among them.

Norton’s explanation was plausible, his frustration understandable, but his conclusion was mistaken because ordinary file operations worked under his interpretation.

So, how many bytes does it actually take to tell DOS that seven bytes aren’t needed?

None.

When the extension is absent, there’s nothing to announce. You simply pass DOS the address of the ordinary FCB.