Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Dynamic Analysis

QEMU tracing workflow

tools/qemu_dos_trace.c is a QEMU TCG plugin that observes BIOS keyboard interrupt 16h, DOS interrupt 21h, mouse interrupt 33h, and sound-driver interrupt 66h without changing the game executable or disk image. Build and run it through the normal launcher:

./run.sh --trace-dos

Trace mode keeps the required Cocoa display visible, uses QEMU’s silent audio backend, and creates these ignored artifacts:

build/qemu-trace/qemu_dos_trace.so
build/qemu-trace/dos-calls.log
build/qemu-trace/monitor.sock

The plugin filters on the current game code segment 0627 and remains dormant until the reconstructed entry point 0627:CB5C executes. That prevents an earlier FreeDOS program that temporarily occupies the same segment from polluting the trace. Each call record contains its exact CS:IP, physical linear address, live AX, argument registers, data segments, and an escaped pathname when the DOS API uses one. A paired return record preserves AX, the other general and segment registers, and carry.

QEMU 11.0.2 represents the first x86 register’s opaque plugin handle with value zero. The tracer tracks descriptor presence separately instead of mistaking that valid EAX handle for a missing register. It retains static MOV AH/AX inference only as a fallback for targets that do not expose EAX. Trace mode also sets one-insn-per-tb=on; without it, plugin register reads describe the beginning of a longer translation block rather than the interrupt boundary.

For a focused instruction-boundary memory probe, invoke the plugin directly with a code offset and a data-segment range:

-plugin build/qemu-trace/qemu_dos_trace.so,\
log=build/qemu-trace/probe.log,cs=0x627,start=0xCB5C,\
probe=0x5913,probe-offset=0x008A,probe-size=0x0100,calls=off

probe is an IP within the filtered code segment. probe-offset and probe-size select up to 4,096 bytes from the live data segment. Each PROBE record contains the instruction-boundary registers and a hexadecimal memory snapshot. calls=off suppresses interrupt-call tracing for faster probes; the default remains calls=on. This mode produced the exact opcode 8E and 91 DOS coordinate-indexed-object-overrun captures documented in the scene-bytecode chapter.

Preserved startup capture

The verified capture ran from process startup into the first story text. The monitor stopped at:

RegisterValue
CS:IP0627:C668
DS=ES=SS14E1
FS0617 (PSP)
GS04C7

This independently repeats the earlier load and data segment values. The screen contains the opening narration beginning “There once was a city far from us in place and time,” establishing the visible runtime point. QEMU also saved the first MiB of physical memory to build/qemu-trace/startup-physical-1m.bin.

ArtifactSHA-256
dos-calls.logf8013fb529444c409a6309a5bbc57336d674382f4e20dcde9185a4d67658e3c9
startup-physical-1m.bin7fee3fdda30db225711d0db84d1f292efb9b087c4a91deb2e035025cd31bf71e
startup-screen.png85a46bbf6345d5cd88393596706ded3daadbbe0ecb9853cdd0bcecf610077c79

Comparing the captured memory with the independently unpacked and relocated load module again produces the exact 0x905A-byte static prefix and 5,612 runtime differences across 75,264 bytes, matching the earlier title capture.

The trace contains 195 completed game DOS calls. Its high-level file timeline is:

First callActivity
13Change directory to C:\CBDOME.
22Open and read the four-byte SOUND.5 installation-lock file.
29Load SOUND.1.
43Load SOUND.2.
68Load SOUND.3.
90Load SOUND.4.
105Probe DD1.DAT, then reopen it as the persistent main-data handle.
121Read DDGAMES.SV0, the 243-byte save-slot index.
135 / 142Load DDLC twice during early resource initialization.
181Reopen DDGAMES.SV0 before the story introduction.

All listed returns have carry clear. The path sequence is runtime evidence, not a prediction from embedded strings.

Sound-driver correlation

SOUND.5 is read in requests of one byte and three bytes. Each driver file is then opened, queried, measured with three seek calls, allocated in DOS memory, read in chunks, and closed. The observed allocation request exactly matches the file size rounded up to a 16-byte paragraph:

FileBytesAllocation (BX)Paragraph bytes
SOUND.14,824012E4,832
SOUND.216,26303F916,272
SOUND.313,312034013,312
SOUND.43,62200E33,632

This connects the static far-pointer loader at 0xACDA with the DOS API calls inside the Microsoft C low-level I/O routines. The trace supports the names libc_lowio_close (0xDA82), libc_lowio_seek (0xDAA2), libc_lowio_open (0xDB1C), libc_lowio_read (0xDCC0), and libc_lowio_write (0xDD9E).

Live decoded effect capture

For an independent codec check, launched QEMU with the same visible Cocoa display and silent audio devices, plus its GDB remote stub. A breakpoint at physical 0xA499 stopped Captain Bible at 0627:4229, after D003.ABT had been decoded and immediately before its state was submitted to interrupt 66h.

The state at DS:A0DE pointed to 5A45:0000 and declared 9,064 samples at 9,000 Hz. The 9,064-byte physical-memory dump from 0x5A450 exactly matches the independently implemented ABT decoder. Both have SHA-256 ca97ad22acf3cc39d078b619168fa026deb1606082999bfb8b9a1aac4957422b. The dedicated audio-format chapter records the codec and conversion tool.

Main container correlation

Startup first opens and closes DD1.DAT through normal stream setup, then opens it again as DOS handle 5. Subsequent resources cause seeks and reads on that handle; no DOS open is made for the static resource name RUN.ART. This is direct evidence that names such as RUN.ART refer to members indexed inside DD1.DAT, not sibling files in the DOS directory.

DDLC, by contrast, is opened as a separate DOS pathname twice during the captured interval. Later static and format analysis identifies it as tagged companion text bank C; DDLA through DDLG and DDLR use the same recovered record stream. The direct opens are therefore runtime confirmation that DDL text lives beside the main container, while the extensionless verse indexes live inside DD1.DAT. The text-format chapter documents their exact join.

Focused input and save capture

A later visible, silent QEMU run added int 16h and int 33h to the same instruction-boundary tracer. The game polls BIOS keyboard service 0101 at 0627:E9DC, mouse motion service 000B at 0627:8D8A, and mouse position/buttons service 0003 at 0627:8DCD. These sites independently match the statically named input wrappers.

Moving the monitor mouse changed the returned position from X/Y 0140:0064 to 01D0:0088, or (320, 100) to (464, 136). Holding the left button then produced repeated service-0003 returns with BX=0001 at the new position. This proves that the guest driver state reaches the game’s own polling path, not merely that QEMU accepted host events.

F10 produced BIOS scan/ASCII word 4400 first through the non-consuming service and then through service 0000. The next DOS activity opened the existing DDGAMES.SV0, then created and wrote DDGAMES.SVQ. After QEMU stopped, the quick save was exactly 2,752 bytes with SHA-256 5e329e21f32d2e6c3e564d3a3ad717ab07ad55aaedde2587725756945597e43f. The before/after .SV0 hashes were identical, confirming that quick save does not rewrite the normal-slot label index.

The normal in-game Escape/Save Game path ended name entry with BIOS word 1C0D and then rewrote the 243-byte DDGAMES.SV0 followed by the selected 2,752-byte DDGAMES.SV2. The low-level writes follow the recovered state layout: 200-byte snapshot and live blocks, 66 flags, 660 bytes of text descriptors, four 20-byte strings, five words, and two 768-byte maps. Parsing the copied slot with tools/inspect_save.py recovered the expected settings, bank C, INTRO/seg scene strings, and named script-variable state. This connects BIOS input, the menu path, write_save_state, Microsoft C low-level I/O, and the two on-disk formats in one run.

Adding more interrupt vectors exposed a tracer bookkeeping bug: an interrupt instruction inside DOS could have the same registered return callback active while a game call was pending. Return records from the affected capture are therefore not used for DOS-result claims; its call-entry paths and write arguments remain valid. The plugin now stores the pending call’s exact linear return address and ignores callbacks at every other CS:IP. A fresh bounded run recorded 119,824 paired calls—39,903 keyboard, 115 DOS, and 79,806 mouse—with no false DOS-internal return.

Representative interactive coverage

The visible Cocoa session exercised and captured these distinct paths:

SystemObserved evidence
StartupMain title, landscape title transition, and difficulty selector.
StoryOpening narration and multi-step commander conversation.
ExplorationCaptain Bible on the exterior platform and inside a hall.
StudyF1 Bible interface reporting no loaded verses.
NavigationF2 map interface with the current node graph.
StateF3 faith overlay reporting 100 percent.
Menus/savesGameplay options, normal-save slot list, and name entry.
CombatCOMBAT1 action screen, A-key attack effect, and defeat screen.

Function keys were sent only after normal gameplay began; an earlier F1 during the introductory sequence advanced into the difficulty selector instead of opening the Bible. The distinction is useful evidence that the same input is routed by scene state rather than handled as an unconditional global hotkey. The combat chapter records the controlled scene-entry provenance and table dump in detail.