DOS INT 21h FindFirst and FindNext: The DTA Is Search State
Use DOS directory-search calls without overwriting the command tail, losing continuation state, misreading attributes, or confusing end-of-search with a fatal error.
DOS directory enumeration through INT 21h functions AH=4Eh and AH=4Fh looks like a simple pair: ask for the first match, then ask for the next one until the carry flag is set. The subtle part is the Disk Transfer Area (DTA). The DTA is not merely an output record. The search functions also store private continuation data there, and FindNext expects the same buffer contents and address that were active for the search.
Treat each enumeration as a stateful operation with an owned DTA. Allocate a separate buffer, set it before the first search, preserve it through every next call, and distinguish the expected end-of-search result from path and I/O failures. That discipline avoids corrupting the command tail in a small program and makes nested or interleaved searches possible.
The API contract
FindFirst (AH=4Eh) receives an ASCIIZ pathname at DS:DX and an attribute mask in CX. On success, carry is clear and DOS writes a directory result plus continuation state into the current DTA. FindNext (AH=4Fh) does not receive the pathname or mask again; it continues the previous search using information in the current DTA.
The traditional result begins at the DTA address. The first 21 bytes are reserved for the search continuation. Following fields contain the attributes, packed write time and date, a 32-bit file size, and an ASCIIZ 8.3 name. Preserve all 43 bytes until the search is finished. Copy fields you need into application-owned records before reusing the DTA for another operation.
The API is older than long-filename conventions. Do not assume the returned short name is a Unicode path, a canonical security identity, or a stable file handle. It is a directory-search result intended for classic DOS file operations. If a system has an optional long-filename driver, check that driver’s documented extensions separately.
Do not leave the search in the default PSP buffer
When a program has not selected a DTA, DOS uses the default 128-byte area at offset 80h in the Program Segment Prefix (PSP). That is also where the process command tail is stored. A search can therefore overwrite arguments the program still intends to parse.
Before FindFirst, point the DTA at a dedicated buffer with INT 21h, AH=1Ah, using DS:DX for the buffer address. Allocate at least the complete search record, not merely the 21-byte private prefix. The buffer may be larger; it must remain addressable and writable for the duration of enumeration.
An assembly-level outline is:
; DS:DX points at an ASCIIZ pattern such as "C:\\DATA\\*.DAT"
; CX is the requested attribute mask; dedicated_dta is at least 43 bytes
mov dx, offset dedicated_dta
mov ah, 1Ah ; Select the process DTA
int 21h
mov dx, offset file_pattern
xor cx, cx ; Normal files only
mov ah, 4Eh ; Find first match
int 21h
jc search_failed
; Consume the result at dedicated_dta before it is reused.
; Keep the DTA address and its first 21 bytes intact for AH=4Fh.
The code is a calling-convention sketch, not a complete assembler module: labels, segment initialization, register preservation, and error reporting belong to the program. If the buffer is not in the segment currently addressed by DS, load the correct segment before each call. Keep the pathname ASCIIZ-terminated and use DOS wildcard syntax only in the filename or extension portion.
Preserve both the pointer and continuation bytes
The AH=4Fh call uses the current DTA address and data left by AH=4Eh or an earlier AH=4Fh. A helper that changes the DTA, or another search that overwrites the same memory, invalidates that continuation. Saving only the pointer is insufficient if the buffer contents change; preserving only a copy of the bytes is insufficient if DOS is pointed somewhere else.
This matters when a program needs to start a nested search while one is in progress. Save the old DTA pointer with AH=2Fh (ES:BX returns the current address), preserve the active search buffer, select a second DTA, run the nested search, then restore the old DTA pointer and its original contents before resuming the first search. A stack of search contexts is a useful design if nesting depth is known and bounded.
Do not keep pointers into the current DTA as long-lived references. Copy the result fields you need into a program-owned structure before issuing another call that may overwrite it. Also avoid assuming directory enumeration is a snapshot: entries can change while the program is running, and ordering is filesystem-dependent.
Attribute masks have specific semantics
With CX=0, FindFirst includes normal files. Setting one or more of the hidden, system, or subdirectory bits expands the search to include normal files plus entries with the selected attributes. Setting the volume-label bit selects matching volume labels instead of ordinary file results. Read-only and archive bits are ignored by this function; they do not filter the results the way a casual reading of file attribute flags might suggest.
For example, a search that requests hidden and system files should set only those bits along with the normal search semantics; it should not assume that every attribute bit is an independent include/exclude predicate. Inspect the returned attribute byte when the application needs to distinguish a file from a directory or hidden/system entry.
The old FCB search functions AH=11h and AH=12h remain relevant to compatibility code, but they have narrower pathname behavior. For path-aware searches on DOS 2.0 and later, the encyclopedia recommends 4Eh and 4Fh instead. Do not mix the FCB pair’s DTA output shape with the handle-style pair’s record layout.
Interpret carry and AX in context
On FindFirst, carry indicates failure. Typical documented results include file not found (AX=02h), path not found (AX=03h), and no match (AX=12h). On FindNext, AX=12h is the normal completion signal when no more entries remain, including when no previous successful FindFirst exists. Do not present end-of-search as an application error to users unless the application expected at least one match.
In assembly, test the carry flag immediately after the DOS call, before an instruction changes flags. Save AX before calling another DOS function if you need to report the exact error. DOS 3.0 and later provide AH=59h for extended error details, but call it immediately after the failing function and follow the documented register convention for the DOS version being targeted.
; After a successful AH=4Eh call, preserve the current DTA and continue:
next_match:
; Read or copy the current result here.
mov ah, 4Fh
int 21h
jnc next_match ; Carry clear means another result is ready.
cmp ax, 12h
je search_complete ; Expected exhaustion of the search.
; Other AX values indicate an error that should be reported.
This loop omits application-specific result copying and cleanup, but it shows the key invariant: do not invoke AH=4Fh after a failed AH=4Eh, and treat AX=12h as normal exhaustion for a started search.
Make cleanup independent of the last result
The DTA pointer is a piece of process state. A library routine should usually save the caller’s existing pointer, install its own buffer, and restore the prior pointer on every exit path. A top-level process that is about to terminate can simply finish, but a routine that returns to its caller must not leave a pointer to a stack-local buffer that no longer exists.
Avoid returning from a helper while DOS still expects a DTA buffer in storage that has gone out of scope. In a language with stack frames, a local DTA is safe only when the whole search completes before the frame is released and the prior DTA is restored. In assembly, the equivalent rule is to keep the buffer allocated for the entire call chain.
A safer mental model
FindFirst creates a continuation, FindNext consumes and updates it, and the DTA is the storage that carries it. The pathname, mask, pointer, and bytes together define the active search state. Once that is understood, the practical rules are simple: install a dedicated DTA, preserve it, copy out results you need, handle AX=12h as end-of-search, and restore process state before returning.
Related:
- DOS File Control Blocks: Record I/O and Legacy Compatibility
- DOS INT 21h Function 59h: Read Extended Error Diagnostics
Sources: