Skip to content
FreeDOSDeep Dive Published Updated 6 min readViews unavailable

FreeDOS INT 21h/AH=52h: The List of Lists as a Version-Specific Boundary

Trace FreeDOS AH=52h to its kernel source, identify the exact ES:BX result, and inspect internal DOS state without mistaking it for a portable API.

The DOS service INT 21h/AH=52h, commonly called Get List of Lists, attracts low-level utilities because it exposes a pointer into kernel-owned internal state. That makes it useful for version-pinned diagnostics and historical compatibility research, but a poor foundation for portable applications. “A DOS function returned a pointer” does not mean the pointed-to layout is a stable public contract.

This article follows the current FreeDOS kernel source at a specific revision. It does not claim that MS-DOS, PC DOS, DR DOS, or every FreeDOS release returns the same structure or uses the same internal offsets. If a program needs portable behavior, use documented DOS calls and their documented results instead of parsing kernel-private memory.

Read the implementation before dereferencing the result

In the reviewed FreeDOS kernel source, the INT 21h dispatcher labels function 52h as undocumented and implements it by assigning FP_OFF(&DPBp) to BX and FP_SEG(&DPBp) to ES. The global declaration in globals.h identifies DPBp as a far pointer to the first drive Parameter Block.

The distinction between &DPBp and DPBp is essential. On this FreeDOS source revision, the returned ES:BX points to the kernel variable that holds the far pointer to the first DPB. It is not itself the first DPB address. A utility that treats ES:BX as a direct DPB pointer would be one indirection away from the object it intended to inspect. Other DOS implementations can differ, so do not turn this source observation into a universal AH=52h structure diagram.

The call itself is small:

; 16-bit real-mode DOS code; assemble for the target toolchain.
mov  ax, 5200h       ; AH=52h; AL is cleared here but not the contract
int  21h
; For the audited FreeDOS kernel revision only:
; ES:BX is the far address of the DPBp global pointer variable.
; Preserve that exact kernel build before inspecting the target bytes.

The dispatcher selects this service from AH=52h; clearing AL in the example is conventional, not a claim that the low byte is part of the documented contract. The FreeDOS implementation returns the segment and offset in ES:BX. The snippet deliberately stops at reporting the returned pointer. It neither writes to it nor assumes a fixed sequence of List-of-Lists fields. An investigation that follows the returned pointer must separately establish the target kernel build, the compiler’s 16-bit far-pointer representation, the memory bounds, and the exact data structure in that build’s source.

Why the name does not define a stable layout

The historical label “List of Lists” sounds like one standard C structure. In practice, DOS-compatible kernels accumulated internal fields and relationships over time. Function 52h is an undocumented compatibility surface; fields, offsets, pointer indirections, and meanings are implementation- and version-sensitive. FreeDOS itself is an independently implemented DOS-compatible kernel, so matching an observed MS-DOS layout cannot be assumed merely because both expose INT 21h.

The FreeDOS source is specific enough to confirm what the service returns in that revision: an address to the DPBp global. The declaration names the global’s target type, while kernel code uses LoL->DPBp as the head of a DPB chain. Those facts can support a narrowly scoped debugger experiment. They do not publish the full chain as an application ABI, guarantee that every device or drive appears in a particular order, or make it safe to mutate the structures.

Do not use old tables of offsets as if they were a FreeDOS header file. An offset documented for an MS-DOS version may refer to another field or indirection in FreeDOS. Even two kernels that return the same segment:offset register pair format may point to different data. The FreeDOS function catalog also historically records AH=52h as undocumented; its old version table is historical context, not a statement about every current kernel build.

A safer diagnostic workflow

First record what is actually running: the FreeDOS kernel version string, the kernel binary’s exact build or checksum, the emulator or hardware configuration, and the DOS memory configuration. A compatibility version query alone is not a proof of the internal layout. Keep the test in a disposable virtual machine or disk image so a bad pointer interpretation cannot corrupt a working DOS installation.

Next, pin the matching FreeDOS kernel source revision and trace the function dispatch, returned registers, global declaration, and any structure traversals you need. Check every additional dereference against that exact source. A far pointer is a segment:offset pair under 16-bit DOS conventions; do not cast it to a host pointer or apply a protected-mode toolchain’s assumptions. If a debugger or diagnostic program displays memory, record the segment and offset separately and inspect only the smallest range the source establishes.

Keep the probe read-only. Do not change the pointer variable, DPB links, drive records, or neighboring kernel data. DOS kernel internals are live state: an apparently harmless write can redirect filesystem operations, break later API calls, or damage a mounted image. Avoid invoking the probe from a hardware interrupt or an arbitrary TSR callback while DOS is busy; use a normal foreground diagnostic program at a known-safe DOS call boundary.

For production behavior, prefer supported interfaces, and describe their scope precisely: a filesystem-capacity query acts on a caller-known drive, for example, rather than serving as a universal volume-enumeration API. If discovery is required, probe candidate drive letters with calls documented for the exact DOS version and handle each failure explicitly. If a required capability is absent from the documented interface, isolate the compatibility shim behind a kernel-version check, fail closed when the build is unknown, and maintain a test matrix for each exact kernel image you support. A diagnostic that says “layout not recognized” is safer than one that silently reads a guessed offset.

Test the assumptions, not just the interrupt

A useful test plan has multiple kernel builds, at least one empty or differently configured drive set, and a VM snapshot that can be restored after a deliberate bad-pointer experiment. Verify that the call returns without corrupting registers or memory, compare ES:BX with the pinned source’s expected global, and confirm that any later pointer dereference is interpreted with the correct 16-bit compiler model. Keep the raw output, kernel checksum, source commit, and emulator version together so another maintainer can reproduce the result.

If the value changes after adding a block device, that is evidence about the tested kernel’s current state, not a portable disk enumeration API. If the service returns a plausible address under another DOS, that still does not prove the data layout matches. Report exactly which build was inspected, which pointer levels were followed, and which fields were confirmed from source. Anything beyond that boundary should be labeled an experiment rather than a fact.

The practical lesson is not that AH=52h must never be examined. It is that the interrupt is an invitation to inspect one kernel’s internals, not permission to assume all DOS kernels share those internals. Source-pinned, read-only research can be valuable; an undocumented memory structure should never become a hidden dependency in a program expected to survive across DOS versions.

Related:

Sources:

Comments