how Civilization & Colonization ran a planet in real-mode DOS

CHAPTER 2 · MEMORY

THE DIVISION DANCE

At every launch, Sid Meier's Colonization performs the same fixed arithmetic ritual against DOS — probe, shrink, grow, claim, shrink back, probe, claim, cut. Eight INT 21h calls later, DOS's ledger records three tidy blocks; the game, meanwhile, is running its own allocator — complete with its own forged block headers — inside the ≈319 KB DOS has just been told is free.

8
INT 21h calls in the startup ritual — traced live, in order, every run
512 B
viceroy.exe's MaxAlloc headroom over MinAlloc — the linker asks for exactly what it needs
228
calls to undocumented AH=52h as the game walks DOS's own memory-block chain
18,752 B
the fixed gap between FAB source and destination buffers — a constant of the binary
373,184 B
the middle zone the game claims — then mostly hands back, on paper only
+8,192 B
extra free bytes that flip a clean run into an R6003 crash (629,728 vs 621,536)

The deal DOS offers

Real-mode DOS manages the 640 KB arena with one of the simplest allocators ever shipped: a singly-linked chain of Memory Control Blocks. Each MCB is a 16-byte header sitting one paragraph (16 bytes) before the block it describes: a signature byte — 'M' mid-chain, 'Z' for the last block — the owner's PSP segment, the size in paragraphs, and, from DOS 4, an eight-character owner name. Three calls drive it: AH=48h allocate, AH=49h free, AH=4Ah resize. There is no "query free memory" call; the idiom is to ask for the impossible — resize to, or allocate, 0xFFFF paragraphs — and read the maximum DOS reports back in BX when it refuses. Colonization uses that refusal, twice, as a measuring instrument.

What a program starts with is negotiated in its MZ header. MinAlloc is the extra memory it must have beyond its load image; MaxAlloc is the most it wants. Almost every DOS linker stamped MaxAlloc FFFFgive me everything — and left the program to hand back what it didn't need. RTLink/Plus, the linker behind viceroy.exe (Chapter 1), does the opposite:

What four MicroProse executables ask DOS for
FileLoad imageMinAllocMaxAllocHeadroom
viceroy.exe (1994)123,073 B3,017 paras / 48,272 B3,049 paras / 48,784 B32 paras = 512 B — a fixed ask of ~168 KB, total
opening.exe (1994)64,199 B1,007 paras / 16,112 B4,095 paras / 65,520 B3,088 paras
closing.exe (1994)60,209 B816 paras / 13,056 B4,095 paras / 65,520 B3,279 paras
CIV.EXE (1991)177,599 B3,773 paras / 60,368 BFFFF — everythingtake it all, shrink later

Viceroy's MaxAlloc sits exactly 512 bytes above its MinAlloc. Add it up — 123,073-byte load image + 48,784-byte allowance + 256-byte PSP — and the whole program asks DOS for 172,113 bytes, about 168 KB, and refuses more. The linker knows to the paragraph what the resident root needs, because everything else — the other 361,168 bytes of code appended to the EXE — will be paged in through its own virtual-memory machinery (Chapter 1). The file on disk is 493,470 bytes; DOS is shown less than a third of it. (Executable file sizes lie so routinely that our own emulator's EXEC loader once sized a child program from the file length instead of the MZ page count — fatal for overlay EXEs with big appended tails, and worth a bug of its own, MDD-00052.)

Eight calls, three blocks

Then the dance begins. Traced live under our emulator, viceroy.exe issues the same eight-call sequence at every start. First it resizes its own block to 0xFFFF paragraphs — impossible, deliberately — and reads DOS's refusal: the block could grow to 97BE paragraphs, 621,536 bytes. Then the trick: it shrinks to bare code (165,456 bytes), re-grows to precisely total − 143Bh − 1 paragraphs — a wall covering nearly the whole arena — and allocates 5,179 paragraphs, which can now fit in exactly one place: the hole it left at the top. The 82,864-byte staging block lands at 8BC5–9FFF, pinned hard against video RAM at A000:0000, at every MCB base we tested, regardless of what else is loaded. DOS's first-fit allocator has been used as a placement tool.

THE DIVISION DANCE — 640 KB, EIGHT CALLS ▶ play · ⏭ step · hover any region for its size
DOS + drivers program block (root) middle zone top block (staging) free — DOS's view game sub-allocations (game view)
Every call, size and address measured from live INT 21h traces of viceroy.exe under mddosem (MCB base 0800, 2026 campaigns). DOSBox-X reproduces the identical split with its own constants (probe 97ECh, grow 83B3h, top block 1438h) — same choreography, arithmetic recomputed per machine. In the game's-ledger view the sub-block layout is schematic — the S$MPSLOG header bytes are live-captured (§ below), though from the opening cinematic's layout, not this end state.
The dance, call by call — live INT 21h trace at MCB base 0800
#CallDOS answersEffect
1AH=4Ah BX=FFFFrefused; BX=97BEprobe: the block could grow to 621,536 B
2AH=4Ah BX=2865okprogram block shrunk to 10,341 paras — 165,456 B of resident code
3AH=4Ah BX=8382okgrow to 97BEh − 143Bh − 1 = 538,656 B — wall off everything but one hole
4AH=48h BX=143Bblock at 8BC5top block: 82,864 B at 8BC5–9FFF, pinned under A000
5AH=4Ah BX=2865okshrink back to code — the middle falls free again
6AH=48h BX=FFFFrefused; largest = 23,324 parasprobe: 373,184 B free between root and staging
7AH=48h BX=5B2Ablock at 30A8middle zone claimed
8AH=4Ah BX=0B4Fokmiddle block cut to 2,895 paras (46,320 B); the tail returns to DOS — nominally

The arithmetic is self-consistent almost to the paragraph: the grow argument 8382 is literally probe result minus 143Bh minus one, and the middle zone's span — segment 30A8 up to the staging block's MCB at 8BC4 — is exactly the 23,324 paragraphs the second probe reported. (The one wrinkle is call 7's recorded ask — 5B2Ah, fourteen paragraphs more than the probe had just reported.) Nothing here is hard-coded to a memory size; it is a fixed procedure evaluated against whatever machine it wakes up on. That will matter later.

Bring your own allocator

Why claim 373,184 bytes and immediately cut the block back to 46,320? Because the game never intended DOS to manage that memory. The claim-then-cut establishes where the arena is; from then on the engine runs its own allocator over the region DOS was told to mark free. It is not shy about it, either: it lays down its own MCB-style headers — real DOS bookkeeping, forged by a game — inside memory the DOS chain does not cover. We caught one live, during the opening cinematic's logo sequence:

2F79:0000  4D 42 08 4D 15 ·· ·· ··    'M'  owner=0842  size=154D paras (87,248 B)
2F79:0008  53 24 4D 50 53 4C 4F 47    "S$MPSLOG"

An 87,248-byte sub-block, tagged S$MPSLOG — MPS Labs' own house style in eight characters — holding the MPSLOGO decompression buffer, sitting on top of what our emulator's allocator recorded as one large free block. The engine also refuses to take DOS's word for the map: during startup it issues 228 calls to the undocumented AH=52h "List of Lists" service and walks the MCB chain itself, byte by byte, through guest memory.

This cost us a lesson in DOS authenticity. When the game eventually hands one of its home-made blocks to AH=49h (free), real DOS doesn't check any ownership table — it reads the 16-byte header out of RAM, sees a plausible 'M' signature and size, and obliges. Our emulator originally tracked allocations in a tidy Rust Vec and refused to free blocks it had never issued. Wrong model: real DOS's allocator state is guest memory. We had to teach DosMem::free() to validate the signature in RAM and honour blocks it never handed out — matching the signature-driven behaviour Colonization depends on.

The pay-off for bringing your own allocator is control. A DOS call costs an interrupt and gives paragraph-granular blocks at addresses of DOS's choosing; the game's private arithmetic gives it byte-exact, deterministic placement. Determinism is measurable: the engine always places a FAB decompression source and its destination exactly 18,752 bytes apart — we verified the constant at four different MCB bases, against machines with different amounts of free memory. The layout is a property of the binary, not the machine. Which is a strength, and also the next section.

Fragile by design

The second machine-shaped assumption is baked into that 18,752-byte constant. The title screen, OPENMENU.PIK, carries a 64,000-byte Mode 13h frame FAB-compressed to 45,855 bytes — 2.4 times the gap — so in the crash-era layout the decompressor ran in place, source and destination overlapping in flight. The decompressor (Chapter 3 has the format; the disassembly lives at root offset 0466) takes three far pointers and no length argument — it stops when the stream says stop. We watched the write pointer catch the read pointer at output byte 27,400: from there the input was being overwritten before it had been read.

18,752 BYTES OF HEADROOM — DECOMPRESSION IN PLACE ▶ replay the overrun · ⏭ step · hover the regions
decompressed output (write) compressed source (read) corrupt from byte 27,400
Buffer geometry, the 18,752-byte constant (verified at four MCB bases), the collision offset (write 27,400 = read 8,648 + gap) and the corrupted bytes are measured — mddosem live runs, April 2026, crash-era layout. The pointer schedule is schematic. How real 1994 machines survived this arithmetic is formally open: the repo's hypothesis — stale boot residue beyond the buffer happening to decode as an end-of-stream marker, an accident our zero-filled RAM lacked — is flagged, not proven, and the repo's own analysis notes that zeroes decode as end-of-stream too.

The third assumption we violated ourselves. Our emulator used to conjure 32 KB of phantom UMBs whenever a program called INT 21h AH=58h/AL=03h (link upper memory blocks) — upper memory with none of the ~25 KB of conventional overhead a real provider like EMM386 charges. No 1994 PC was ever shaped like that. Colonization probed, believed the machine, and obediently took a buggy split-memory allocation path. The fix was emulation honesty: link-UMBs is a no-op when no UMB provider exists. The flip-side is real: offered upper memory, the game genuinely uses it — under that (then-phantom) UMB-linked layout we observed overlay staging at DS=C804, linear C8040, comfortably above the 640 KB line. And the pattern generalises: Ultima VII walks the MCB chain itself and got equally confused by our UMB-linked layout ("requires 561144 bytes of DOS memory", bug MDD-00046), fixed with the same per-application profile lever built for Colonization.

Civilization's simpler arithmetic

Three years earlier, Civilization's memory story is plainer — and stingier. CIV.EXE declares a 177,599-byte load image plus 60,368 bytes of MinAlloc: 237,967 bytes, roughly 232 KB, before DOS, drivers and buffers. Its MaxAlloc is FFFF: take the whole machine, then trim — and its overlay manager's own vocabulary ("Unable to shrink overlay memory allocation") records the trimming. Code beyond the root is 23 Microsoft LINK overlays totalling 121,720 bytes, of which only the largest — about 10.4 KB — needs to be resident at a time (Chapter 1).

There is no expanded or extended memory in the picture at all. The EMS driver device name EMMXXXX0 appears nowhere in CIV.EXE (viceroy.exe carries it at file offset 1337A — RTLink probes for an EMS page cache); there is no XMS install check; the single CD 67 byte pair in the file sits inside a data table. Every byte of Civilization ran under 640 KB. What it does have is an overlay manager with opinions, shipped in the binary as a $-terminated string block:

; CIV.EXE, string block at file offset 29B4Fh
Allocated 1Mb of space????
MS-DOS lied to us about how much memory was available
Not enough memory to load file
Overlay has overrun allocated memory
Unable to shrink overlay memory allocation
Error releasing overlay memory

Someone at MPS Labs had been burned by a memory map before. Notably, CIV.EXE contains no "you need NNN K" message of its own — the only memory complaints it can print are the C runtime's Not enough core and the manager's paranoia above.

Colonization, fatter in every dimension, moved the fight to the user. Its manual demands "at least 575,000 bytes (approx. 565 Kb) of free conventional memory" and adds: "We strongly recommend that you not have any Terminate-and-Stay-Resident programs (TSRs) other than disk caching programs (SMARTDRV, for example) loaded into memory." Note the unit: not megabytes of RAM — free conventional bytes. It is a CONFIG.SYS test, not a hardware test. An 8 MB machine with a sloppy CONFIG.SYS failed it; a 2 MB machine tuned with QEMM passed. This was the era's defining chore: MS-DOS 5's DOS=HIGH (1991) parked the kernel in the 65,520-byte High Memory Area to free up to 46 KB; Quarterdeck's QEMM-386 (v4.2, launched 11 November 1988) and its OPTIMIZE utility computed driver load orders — Jimmy Maher records tuned systems reaching ~634 KB free — and Microsoft answered with MemMaker in MS-DOS 6 (1993), two reboots and a SIZER.EXE pass to measure each TSR. A mid-range 1994 PC was a 486DX2 with 4 MB of RAM; Colonization still budgeted itself against the same 640 KB arena as the 1981 IBM PC, because DOS and the installed base had not moved.

The 640 KB ledger

Put the dance's end state on one page and Colonization's whole memory strategy is legible at a glance:

Colonization at steady state — 655,360 bytes, accounted for
RegionSegmentsBytesSteward
IVT · BIOS data · DOS 5.0 · drivers0000–07FF32,768DOS
Root program block — resident code, the 1,031-thunk farm, the VM manager0801–3066165,456DOS's ledger
Middle block (retained)30A8–3BF746,320DOS's ledger
"Free" — actually the game's arena: page pool (≥ 29,520 B, sized for the largest page payload), FAB staging, sprite buffers, S$MPSLOG sub-blocks3BF8–8BC3326,848the game's own allocator
Top block — overlay & file staging, pinned under video RAM8BC5–9FFF82,864DOS's ledger, game-filled
Total conventional memory0000–9FFF655,360

(The remaining 1,104 bytes are MCB headers and alignment slivers between blocks.) Notice what is not on the list: the world. The entire New World — every tile, unit, colony, tribe and market price — needs about 24 KB (Chapter 4), a rounding error in this ledger. The 640 KB problem was never the simulation. It was code (Chapter 1) and art (Chapter 3), and the division dance is the floor those two systems stand on: a private arena, at a deterministic address, carved out of DOS with eight calls and a straight face.

Sources for this chapter — byte-level: viceroy.exe / opening.exe / closing.exe / CIV.EXE MZ headers and string sweeps (fresh 2026-07-18 analysis); runtime: live INT 21h traces, MCB-chain captures, MCB-base sweeps and the phantom-UMB / R6003 / DosMem::free war stories under mddosem (bug ledger MDD-00046, MDD-00052); context: the Colonization manual (archive.org), The Digital Antiquarian, and Wikipedia's QEMM / MemMaker / HMA histories — full credits in How We Know.