Skip to content
FreeDOSDeep Dive Published Updated 6 min readViews unavailable

COM vs EXE: How DOS Actually Loads and Runs a Program

Two genuinely different DOS executable formats, with real structural differences in how each loads into memory and why COM files had a strict 64K limit.

DOS supported two distinct executable file formats — .COM and .EXE — and the difference between them isn’t a superficial naming convention; it reflects a genuinely different loading model, with real, structural consequences for what each format could and couldn’t do.

The COM format: a direct memory image

A .COM file is, essentially, a raw binary memory image — the file’s contents are loaded directly into memory starting at a fixed offset within a single 64K segment, and execution simply begins at the start of that loaded image. There’s no header, no relocation information, no metadata describing the program’s structure at all: what’s in the file is exactly what ends up in memory, verbatim, ready to execute immediately.

Why this simplicity came with a hard 64K ceiling

Because a .COM file’s entire code, data, and stack all had to fit within that single 64K segment (the file is loaded into one segment, with the stack initialized at the top of that same segment, growing downward toward the code and data occupying it from the bottom), any program whose total memory footprint — code plus data plus whatever stack space it needed — exceeded 64K simply couldn’t be built as a .COM file at all, regardless of how the rest of the system’s available memory looked. This wasn’t a configurable limit or a recommendation; it was a hard structural consequence of the single-segment loading model itself.

The EXE format: a proper structured executable with a header

.EXE files, by contrast, begin with a structured header (recognizable by its “MZ” signature bytes, initials of Mark Zbikowski, one of the format’s original designers) describing the program’s actual segment layout, entry point, initial stack pointer, and — critically — a relocation table listing every place in the code where a memory segment address needs to be patched to reflect wherever DOS actually decided to load the program in memory.

Why relocation entries were necessary at all

Because an .EXE program could be loaded starting at different memory addresses depending on what else was currently resident in memory at load time, any absolute segment address baked into the compiled code needed to be adjusted (relocated) to match wherever loading actually placed it — the relocation table is exactly the list of locations in the file needing this adjustment, and DOS’s loader walks through this table at load time, patching each listed location with the correct, actual runtime segment address. This is precisely the flexibility .COM’s simpler direct-memory-image model gave up in exchange for its simplicity — a .COM file has no relocation table at all, because it’s designed to always load at a single, fixed offset with no segment-address flexibility needed.

Why both formats coexisted rather than one replacing the other

.COM’s simplicity made it genuinely well suited for small utilities and simple programs that comfortably fit under 64K total — less loading overhead, a smaller file, and no relocation processing needed at load time. .EXE’s more complex, structured format was necessary for anything larger, or anything needing multiple separate segments (separate code and data segments, for instance, rather than everything crammed into one). Rather than one format being simply “better,” each served genuinely different, legitimate use cases based on a program’s actual size and structural needs — which is why DOS-era software distributions commonly included both formats side by side, using whichever fit a given utility’s actual requirements.

How DOS’s loader actually decides which format it’s dealing with

When DOS loads a file for execution, it checks the first two bytes for the “MZ” signature — if present, the file is treated as an .EXE and processed according to its header and relocation table; if absent, DOS falls back to treating it as a raw .COM-style memory image, loaded directly at the fixed offset with no header processing at all. This is a deliberately simple detection mechanism, checking actual file content rather than solely relying on the file extension, though in practice the extension and actual format nearly always matched given how development tools of the era generated each type.

Why this distinction still matters for retrocomputing and reverse engineering

Anyone examining DOS-era executables today — for retrocomputing preservation, reverse engineering, or simply understanding why a specific piece of period software behaves the way it does — benefits directly from recognizing which format they’re looking at, since a .COM file’s straightforward, header-free structure requires a genuinely different analysis approach than an .EXE’s structured header and relocation table, and the two formats’ very different size constraints and loading behavior explain real, observable differences in how period software was built and distributed.

The PSP: a structure every loaded program shares regardless of format

Immediately before the loaded portion of either a .COM or .EXE program, DOS constructs a 256-byte Program Segment Prefix (PSP) — a data structure holding the program’s command-line arguments, pointers to environment variable data inherited from its parent process, and addresses of the terminate and error handlers DOS uses when the program exits. Every DOS program, regardless of which of the two executable formats produced it, begins execution with this same PSP structure already in place immediately before it in memory, which is precisely why a .COM file’s fixed load offset is specifically 100h (256 decimal) bytes into its segment — the first 256 bytes of that segment are reserved for the PSP, with the actual loaded program image beginning immediately afterward. That specific 100h offset is itself a deliberate holdover from CP/M, the earlier microcomputer operating system DOS’s original command interface was modeled on, which reserved that same leading region of a program’s address space for comparable operating-system bookkeeping — DOS’s loader convention wasn’t chosen arbitrarily, it directly preserved a CP/M-era layout convention.

EXE headers grew more elaborate as Windows loomed on the horizon

The simple MZ-header EXE format described here is specifically the format DOS itself understood natively. As Windows developed alongside and then eventually beyond DOS, a more elaborate executable format (the “New Executable,” and later the fully different Portable Executable format Windows still uses today) layered additional header structures on top of, or entirely replacing, the original MZ header — with a small stub program included specifically to print a message like “This program cannot be run in DOS mode” for anyone attempting to run a Windows-targeted executable directly under plain DOS. Recognizing an MZ header alone doesn’t guarantee a file is a genuine DOS-targeted EXE, particularly for executables originating from the later, transitional DOS/Windows era.

Why FreeDOS’s loader had to replicate this precisely, not approximately

Because so much existing DOS-era software depends on exact, specific loader behavior — the precise PSP layout, the exact relocation semantics, specific edge cases in how a malformed or unusual header is handled — FreeDOS’s own program loader had to replicate MS-DOS’s actual observed behavior with considerable precision, not just a general approximation of “loads COM and EXE files.” Subtle discrepancies in loader behavior are exactly the kind of compatibility gap that surfaces only when running genuinely old, specific software relying on undocumented but consistent MS-DOS loader quirks, rather than showing up in more general, surface-level compatibility testing.

Related:

Sources: