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.
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 FFFF — give 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:
| File | Load image | MinAlloc | MaxAlloc | Headroom |
|---|---|---|---|---|
| viceroy.exe (1994) | 123,073 B | 3,017 paras / 48,272 B | 3,049 paras / 48,784 B | 32 paras = 512 B — a fixed ask of ~168 KB, total |
| opening.exe (1994) | 64,199 B | 1,007 paras / 16,112 B | 4,095 paras / 65,520 B | 3,088 paras |
| closing.exe (1994) | 60,209 B | 816 paras / 13,056 B | 4,095 paras / 65,520 B | 3,279 paras |
| CIV.EXE (1991) | 177,599 B | 3,773 paras / 60,368 B | FFFF — everything | take 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.
| # | Call | DOS answers | Effect |
|---|---|---|---|
| 1 | AH=4Ah BX=FFFF | refused; BX=97BE | probe: the block could grow to 621,536 B |
| 2 | AH=4Ah BX=2865 | ok | program block shrunk to 10,341 paras — 165,456 B of resident code |
| 3 | AH=4Ah BX=8382 | ok | grow to 97BEh − 143Bh − 1 = 538,656 B — wall off everything but one hole |
| 4 | AH=48h BX=143B | block at 8BC5 | top block: 82,864 B at 8BC5–9FFF, pinned under A000 |
| 5 | AH=4Ah BX=2865 | ok | shrink back to code — the middle falls free again |
| 6 | AH=48h BX=FFFF | refused; largest = 23,324 paras | probe: 373,184 B free between root and staging |
| 7 | AH=48h BX=5B2A | block at 30A8 | middle zone claimed |
| 8 | AH=4Ah BX=0B4F | ok | middle 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.
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:
| Region | Segments | Bytes | Steward |
|---|---|---|---|
| IVT · BIOS data · DOS 5.0 · drivers | 0000–07FF | 32,768 | DOS |
| Root program block — resident code, the 1,031-thunk farm, the VM manager | 0801–3066 | 165,456 | DOS's ledger |
| Middle block (retained) | 30A8–3BF7 | 46,320 | DOS'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-blocks | 3BF8–8BC3 | 326,848 | the game's own allocator |
| Top block — overlay & file staging, pinned under video RAM | 8BC5–9FFF | 82,864 | DOS's ledger, game-filled |
| Total conventional memory | 0000–9FFF | 655,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.