Devlog #013 - Anatomy of the Custom Code Loader
Published April 10, 2026
The overlay system has been in the background of this project since Phase 1. We knew it existed. We knew Darklands appends its overlay payload to the EXE itself. We had names for the RTLink loader stubs and the dispatch table segment. But we had never actually followed a load in motion, step by step through the machine.
This session did exactly that. What came out the other end is something more interesting than expected.
First surprise: it is not standard RTLink
The operating assumption going in was that the loader was a relatively standard Borland RTLink mechanism, and that instruction-level tracing would mostly confirm what the static analysis already suggested. That assumption did not survive contact with the debugger.
What the DOSBox-X instruction trace shows is a custom record-driven loader with its own file format, its own globals, and its own two-stage load-and-relocate protocol. It may be built on top of RTLink infrastructure, but the mechanics are specific to Darklands. No RTLink standard behavior should be assumed until each piece is independently verified.
Two modes
The loader operates in two observably different modes depending on when it runs:
Startup bulk-loading mode fires immediately at launch, before any intro or menu. It reads large chunks of data (likely initialization data, resources, or the resident code image itself) in a boundary-limited loop. Each iteration reads as much as will fit without crossing a paragraph boundary, advances the destination segment, and loops until nothing remains.
Quickstart code-loading mode fires when the player triggers a Quickstart game, loading a small executable code chunk into a new segment, then applying a relocation pass before handing off. This is the path that looks most like a traditional overlay loader, and it is the one worth examining in detail.
The loader globals
The loader maintains a small set of working globals inside segment 11E3:
| Global | Confirmed meaning |
|---|---|
[09E6] | Relocation delta base: the base segment shift used during fixup |
[09E8] | Current record pointer: the address of the active load record |
[09EC] | Open file handle: confirmed as handle 0005 in all observed runs |
The file handle 0005 is open before any menu code runs. The loader is not invoked on demand
from a menu action; it is resident infrastructure.
The record format
The loader uses 18-byte records pointed to by [09E8]. Two concrete records were captured and
decoded from live memory: a startup-path record at 11E3:150A and a Quickstart code-load
record at 11E3:0B8C.
Here is the current evidence-backed field map:
Offset Size Name Status Notes
+00 2 dest_seg CONFIRMED Load destination segment; matched DS after load
+02 2 field_02 UNRESOLVED Same value (0x1763) in both observed records
+04 2 para_lo CONFIRMED* Participates in seek-address construction
+06 2 field_06 PARTIAL Low byte used in Quickstart seek; +07 is a flags byte
+08 2 para_count_total STRONG Total paragraph count
+0A 2 fixup_count CONFIRMED Number of 4-byte relocation entries
+0C 2 sentinel STRONG Always 0xFFFF in observed records
+0E 2 field_0E UNRESOLVED Small categorical value; likely type/class
+10 2 para_count_work CONFIRMED Working (remaining) paragraph countThe Quickstart code-load record in full:
Address: 11E3:0B8C
Bytes: 99 16 63 17 68 11 00 04 9E 00 04 00 FF FF 03 00 9E 00
+00 = 0x1699 dest_seg (load code here)
+02 = 0x1763 field_02 (unresolved)
+04 = 0x1168 para_lo (packed seek address)
+06 = 0x0400 field_06 (low byte 0x04 used by seek builder)
+08 = 0x009E para_count_total (158 paragraphs = 2528 bytes)
+0A = 0x0004 fixup_count (4 relocation entries)
+0C = 0xFFFF sentinel
+0E = 0x0003 unresolved
+10 = 0x009E para_count_work (same as total = one-shot load)The load mechanics
Seek address construction
Before reading the code chunk, the loader computes a seek offset from the record. The address is packed across two fields using a nibble-shuffle that effectively shifts a 20-bit paragraph address left by 4 to produce a 24-bit byte offset:
packed_paragraph = (record[+06] & 0x00FF) << 16 | record[+04]
seek_offset = packed_paragraph << 4For the Quickstart record: (0x04 << 16) | 0x1168 = 0x041168, shifted left by 4 gives
0x411680. This is the byte offset in the EXE file where the code chunk begins.
Read-size clamping
The read loop computes a boundary limit from a DI register value to avoid overrunning a paragraph boundary, then reads whichever is smaller:
actual_paragraphs = min(remaining_paragraphs, boundary_limit)
read_bytes = actual_paragraphs << 4For the Quickstart record, remaining = 0x009E and the boundary limit was 0x0F97, so the
full chunk loaded in a single pass. Startup loads clamp to smaller reads and loop.
The loaded code
Immediately after the Quickstart read, a dump of 1699:0000 showed valid x86 code bytes. This
is not a data load. The loader just put executable code into a fresh segment in real memory.
The relocation pass
This is the most technically interesting part. After loading the code chunk, the loader:
- Seeks to a second position in the EXE file (computed from the same record fields)
- Reads
fixup_count × 4bytes into a scratch buffer at11E3:09F0 - Applies each fixup to the newly loaded code
The four fixup entries read for the Quickstart record:
001C 0E76
0049 0E76
002D 0E76
0792 0E76Each entry is two 16-bit words: a target offset and a segment addend. The relocation loop
applies them using the relocation delta (computed as [09E6] + 0x0010 = 0x0823 in this run):
for each fixup entry:
target_segment = relocation_delta + entry.seg_addend
word[target_segment : entry.offset] += relocation_deltaThis was verified against live memory. Entry 0:
target_segment = 0x0823 + 0x0E76 = 0x1699 (the just-loaded segment)
target_offset = 0x001C
Before: word at 1699:001C = 0x06A1
After: word at 1699:001C = 0x0EC4
Check: 0x06A1 + 0x0823 = 0x0EC4 ✓The relocation arithmetic checks out exactly.
What this means
The Darklands code loader is not a passive lookup table. It is an active load-and-relocate engine. When the game transitions to a new state and the associated overlay needs to run, the loader:
- computes a file offset from a packed-paragraph address in the record
- reads a code chunk into a freshly designated segment
- reads a companion fixup table
- patches every segment-sensitive word in the loaded code to account for where the code actually landed in memory
This is what allows the same on-disk overlay image to be loaded into different segments at different times without hardcoded addresses. The fixup table carries all the segment-sensitive references; the relocation loop fixes them up at load time.
What remains
The one confirmed gap is the final execution handoff. We have proven:
- load: the code chunk arrives in memory at the correct segment
- relocate: all segment-sensitive words are patched
The remaining question is: how does control reach the loaded code? The trace ends at the relocation loop. What follows (a far jump, a far call, a stored entry point retrieved by the dispatch system, or something else) is the next target.
The document from this session ends with a precise recommendation: continue stepping from
11E3:03F6 (the instruction after the relocation loop) and capture the first far transfer
of control into the loaded segment. That will complete the picture.
The short version
- The Darklands runtime loader is custom, not provably standard RTLink, despite the RTLink infrastructure elsewhere in the binary
- Two confirmed modes: startup bulk-loading (chunked, looped) and code-chunk loading (seek → read → relocate)
- 18-byte record format mostly decoded: dest_seg, para count, fixup count, packed seek
address, and
0xFFFFsentinel all confirmed;+02and+0Estill unresolved - Relocation pass fully understood: 4-byte fixup entries
{offset, seg_addend};word[reloc_delta + seg_addend : offset] += reloc_delta - Verified with actual before/after memory values; arithmetic confirmed to the word
- One open question: the final execution handoff after the relocation loop completes