Static Analysis Findings
Working with the disassembly
Generate the unpacked executable and apply the current symbol map:
tools/analyze_cb_exe.py CB/CB.EXE \
--output build/analysis/CB_UNPACKED.EXE
rizin -b 16 -i analysis/cb.rz build/analysis/CB_UNPACKED.EXE
Rizin’s first recursive pass identifies approximately 340 candidate functions. Several large candidates cross jump tables or data and are false merges, so the names below are limited to routines whose implementation and call sites provide direct evidence.
Program entry and main
The real entry at 0xCB5C is Microsoft C startup code. It checks for DOS 2.0,
resizes the process allocation, clears BSS, constructs argc, argv, and
envp, and calls the near function at 0x8A82 with all three values. That
function is therefore main.
The high-level static flow is:
- If
argv[0]has a drive-qualified path,0x8A09changes to that drive and directory. maininitializes resource-name buffers includingLOGO,seg, andDDGAMES, then parses all remaining arguments.0x3363verifies VGA, readsSOUND.5, loads the configuredSOUND.1throughSOUND.4drivers, opensDD1.DAT, and initializes subsystems.0x7F58reads the save index, and0xB818loadsRUN.ARTas resource slot 253.0x7D2Bcreates a new session and initializes the menu/game state.mainrepeatedly updates audio and dispatches states 0, 1, and 2 to the menu/game loop, reset path, and restore path. It has no ordinary return.
Runtime random source
Startup initialization at 0x3363 calls the clock helper at 0x02CF. That
helper reads the DOS local time and returns
hour * 3600 + minute * 60 + second; startup masks the low word with
0x7FFF and passes the result to the runtime seed function at 0xE954. The
seed is zero-extended into the 32-bit state at DS:3968. New Game does not
repeat this startup initialization.
The runtime function at 0xE966 updates that state with
state * 0x000343FD + 0x00269EC3, retaining the low 32 bits, and returns
(state >> 16) & 0x7FFF. Opcode 0x82 divides that result by its immediate
and stores the remainder. The X text-component shuffle calls the same
function twice for each of 20 swaps. Unused opcode 0x8B also shares the
stream: it takes the result modulo the loaded descriptor count, scans
cyclically to the next set state byte, clears it, and stores 3,000 at
DS:005C.
The call-reference audit found one additional dormant consumer at 0x649F.
While flag 36 and the DS:005C timer established by unused opcode 0x8B
are nonzero, each status refresh subtracts the current batched timer delta,
calls 0xE966, and uses the result’s low two bits as X/Y flip flags for
zero-based STUFF.ART frame 27 at (10,10). The controller invokes this
refresh before and after the scene VM with the same elapsed delta, so an
already-active timer normally consumes two values per update. A refresh that
expires the signed timer still draws, then clamps it to zero; clearing flag
36 freezes the timer and suppresses the draw. No shipped scene executes
opcode 0x8B, so this path does not alter ordinary-game call ordering.
Portable deterministic replacement
The comparison launcher replaces both the self-contained DOS generator above
and the two separate coordinate-indexed reads used by opcodes 8E and 91;
it does not merely hold the clock seed fixed.
tools/patch_deterministic_rng.py first reconstructs the verified unpacked
MZ, then applies four length-preserving, signature-checked edits at load-module
offsets:
| Offset | Replacement |
|---|---|
0xE957 | Replace mov ax,[bp+4] with mov ax,seed, so the existing seed routine ignores the clock argument and stores the requested 16-bit state. |
0xE966 | Replace the 38-byte Microsoft-compatible generator with the portable 16-bit generator, padded with NOPs in the original extent. |
0x5905 | Replace opcode 8E’s unchecked coordinate calculation and memory read with one call to the portable generator and an eight-bit reduction. |
0x5971 | Apply the same replacement to opcode 91. |
The portable recurrence is:
state = (state * 0x6255 + 0x3619) modulo 2^16
value = state >> 1
Both handlers use value & 0xFF. The multiplier is one modulo four and the
increment is odd, so the state period is all 65,536 values. The patch fits in
place, overlaps no relocation, preserves the original calling convention, and
requires no code cave. It is a deliberate comparison/reference patch rather
than a claim about the original executable’s implementation.
Command-line parser
The parser in main lowercases the option letter and handles exactly the six
switches described by MANUAL.TXT:
| Switch | Static behavior |
|---|---|
-t | Sets the no-mature-topics flag. |
-bX | Maps K to 0, N to 1, R to 2, and L or T to 3. |
-c | Sets the no-combat flag. |
-idirectory | Copies the suffix after -i and appends a backslash for configuration and sound-driver paths. |
-sXfilename | Initializes data, applies translation X, and calls the export routine with the filename after X. |
-gXX | Computes 10 * argv[i][2] + argv[i][3] - 0x210, the decimal two-digit export mask. |
An argument without a leading hyphen replaces the DDGAMES save prefix and
implements the manual’s per-player name/path option. An unknown switch calls
the game’s message path with Huh?.
Text export
Function 0x5F92 opens the requested file in text-write mode. It parses the
game’s record tags and writes labeled sections. Direct bit tests recover the
full -gXX mask:
| Bit | Value | Output |
|---|---|---|
| 0 | 1 | Lie/verse number (#00 template) |
| 1 | 2 | CYBER LIE: |
| 2 | 4 | PARAPHRASE: / lock text |
| 3 | 8 | CONVERSATION WITH VICTIM: |
| 4 | 16 | Communications-room material, represented by several record tags |
| 5 | 32 | VERSE: |
The routine writes the heading CAPTAIN BIBLE IN DOME OF DARKNESS, iterates
the available records/buildings, and uses Microsoft C fread/write and text
stream helpers. This is implementation evidence for the manual’s export
feature rather than merely a string search. The text-format chapter documents
the extensionless verse indexes, companion DDL* streams, loader, inspector,
and complete QEMU export validation.
Save files
The default player prefix is DDGAMES; a non-option argument replaces it.
The .SV0 index is nine 27-byte label buffers, normal states are .SV1
through .SV9, and F10/F9 use the independent .SVQ state. Static copy
direction separates checkpoint and live fields in each fixed 2,752-byte state.
The scalar fields include translation, music, effects, and checkpoint/live
text-bank values; the 66 ten-byte descriptors connect exactly to the recovered
text resources. The save-game format chapter gives the full
15-block layout, filename logic, supplied-file comparison, error behavior, and
reproducible inspector.
Input and hardware support
0x90D4 uses BIOS video interrupt 10h functions 1A00h and 12h to classify
the display adapter. Startup requires return value 2 and otherwise prints
VGA not detected.
0x8E0A checks interrupt vector 33h and calls the mouse reset function.
Functions at 0x8D50 and 0x8D5D show and hide the mouse cursor. 0x8D79
reads mouse motion and buttons, clamps coordinates to 320×200, and accumulates
button press/release bits. The event combiner at 0x7BED merges this with the
keyboard path and returns internal event codes used by 0x875D, including
Escape, Enter/click, pointer movement, and the four extended arrow codes.
The position belongs to this persistent input subsystem, so scene loading and
New Game do not reset it.
The independent startup framebuffer comparison isolates the cursor’s exact
19 changed pixels at center (160,100): 16 palette-index-1 crosshair pixels
at axis offsets 3 through 6, and a three-pixel palette-index-15 upper-right
shadow. This live overlay is separate from ART resources and is drawn above
scene and interface output.
The keyboard wrapper tests BIOS interrupt 16h service 01h and consumes an
available word with service 00h. It therefore receives BIOS typematic
entries for a held key rather than maintaining a game-specific repeat timer.
Every repeated word follows the same menu or gameplay dispatcher as its
initial key press.
Converter 0x0010 returns a nonzero BIOS ASCII byte directly and otherwise
adds 80h to the BIOS scan byte. This preserves modifier distinctions that a
host adapter cannot infer from the physical key alone: Shift+Enter is still
0Dh, Ctrl+Enter is 0Ah, Alt+Enter is extended, and Alt+Escape is extended
while the plain, Shift, and Ctrl Escape forms remain 1Bh.
The executable consequently has no separate keypad dispatcher. BIOS keypad
Enter produces the same Enter value; navigation-mode keypad keys produce the
same Home, End, arrow, and page scan values; and numeric-mode keypad keys
produce printable digits or punctuation. Shift reverses the effective Num
Lock state before those BIOS values reach converter 0x0010.
The executable also contains wrappers around DOS interrupt 21h, BIOS video
interrupt 10h, keyboard interrupt 16h, and a loaded-driver interface on
interrupt 66h. The sound-driver chapter assigns all 34 CD 66 sites to
DIGPAK or MIDPAK services and records their register contracts.
Installation lock file and sound drivers
Startup reads all four bytes of SOUND.5 into a local structure. The fields
are installation policy rather than sound-hardware selection:
| Offset | Size | Use |
|---|---|---|
0 | 2 | Bible-translation lock, applied only when no command-line lock was supplied; 0070h means unlocked. |
2 | 1 | Mature-topic marker; only DBh permits mature topics, otherwise the no-mature flag is forced. |
3 | 1 | Value ORed into the no-combat flag. |
The supplied file is 01 00 00 00: translation value 1, forced no-mature
mode, and no installation-level combat restriction. This also explains why
-b, -t, and -c cannot relax installation locks: startup preserves an
already selected command-line translation and ORs the two restriction flags.
The no-mature flag does not remove runtime Bible descriptors. Map loader
0x034F uses it to clear connected kinds 1 through A when either cell
parameter is at least E0; exporter 0x5F92 separately skips descriptors
whose selector is at least E0.
The following four files are independent driver components loaded into far
memory. SETSOUND.BAT records their original configured names:
| Installed file | Original name | Identification |
|---|---|---|
SOUND.1 | soundrv.com | DIGPAK Sound Blaster 16 digital driver, Audio Solution 3.40 |
SOUND.2 | midpak.adv | Miles Design Sound Blaster Pro FM music driver |
SOUND.3 | tmidpak.com | MIDPAK resident music package, Audio Solution 3.0 |
SOUND.4 | midpak.ad | MIDPAK timbre/instrument data |
The dynamic trace confirms that load_file_into_far_memory at 0xACDA
opens each file, measures it with seek calls, allocates its paragraph-rounded
size, reads it in chunks, and closes it.
Main resource archive
The lookup routine at 0x99AB uppercases the requested base name and
extension, scans an in-memory table in 24-byte steps, seeks the persistent
DD1.DAT handle to the record’s 32-bit offset, and verifies the two-byte GC
payload signature. The loader at 0x97D0 then selects either the raw far-copy
path at 0x9BEF or the dictionary decoder at 0x9CA4 from the record marker.
The latter initializes 256 literal dictionary entries, reconstructs codes
from groups of low bytes plus high-bit plane bytes, and expands prefix/suffix
chains through 0x9D98. Static reconstruction plus extraction of every
declared member establishes the full directory and compression format; see
the dedicated DD1.DAT chapter.
Palette and artwork rendering
All PAL resources are raw 256-entry VGA DAC tables. The BIOS palette path at
0xA017 uses video function 1012h; the retrace-synchronized path at 0xA032
writes the same six-bit RGB triplets through ports 03C8h and 03C9h.
Every ART frame has a 12-byte descriptor containing signed X/Y origins,
unsigned width/height, and a 32-bit pixel offset. The direct frame routine at
0xB99C multiplies the requested index by 12, reads width at offset 4, height
at 6, and the far pixel displacement at 8. The pixel body is row-major and
eight bits per pixel. Low-level blitters provide both opaque copying and an
index-0 transparent path. The graphics-format chapter records validation of
all 143 resources and the exact QEMU framebuffer comparison.
Scene bytecode interpreter
The 62 BIN members contain scene programs interpreted by 0x451B. The
routine reads opcodes 0x01 through 0x91 and dispatches through a 145-entry
table at 0x59AB, containing 134 distinct handler addresses. Shared readers
at 0x3A1E, 0x3A30, and 0x3A64 consume bytes, little-endian words, and
pointer-capable strings from a far resource cursor; four resource-name
handlers use separate inline-only string loops. An independent control-flow
audit recovers the operand layout and dispatch effect for all 145 commands.
Of those, 122 occur across 25,829 decoded commands.
initialize_scene at 0x6631 appends .BIN, loads the resource, and starts
the interpreter at file offset zero. update_scene_threads at 0x7997
resumes stored file offsets. Directly identified handlers load .ART and
.PAL members, select XMI music, change scenes, manage timing, manipulate
variables, and implement absolute jumps, calls, and returns. The dedicated
scene-bytecode chapter documents the complete structural schema, startup
sequence, mixed code/data regions, inspection tool, and QEMU memory check.
Unibot and final sequence
The final programs form a fully recoverable script state machine. GANTRY
mirrors seven victim-rescue flags into seven crew-present flags; CP1 counts
them and refuses departure until all seven are aboard. ROBOT clears the five
powerups, initializes the Unibot position and heading, and enters CP2.
The 256-byte trailer of CP2.BIN stores a reciprocal 16-node road graph with
four compass headings, seven pylon endpoints, one Tower endpoint, node types,
and lower-right-map coordinates. Seven persistent variables record pylon
rescues. An incomplete pylon encounter or premature Tower assault enters
OVER; all seven allow the alternating FACE/CP3 confrontation. Its correct
study response reaches KABLAM and WIN, while the wrong response sets state
9 and reaches OVER. A separate flag makes the road-network Annoy Cyber’s
verse loss a one-time event. The dedicated Unibot chapter gives every table
entry, variable, branch, and reproducible inspection command.
Scene display objects
Scene programs append up to 100 ten-byte display records at DS:A2AC, with
the current count at DS:00E2. Direct object records contain signed X/Y,
8.8 scale, an ART-slot/visibility byte, one-based ART frame, render flags,
and a type byte. Other types connect animation sequences or command threads
to the same update list. The ART-slot high bit hides an object; two low bits
in the separate flags byte flip its axes.
The reset function at 0x3AD2 releases every render slot and clears the count
when changing scenes. The update function at 0x3AFF dispatches records by
type, and 0xBCAC submits direct objects to the ART renderer. A visible,
silent QEMU capture of LOGO.BIN contained 13 records whose type order and
direct fields exactly match all 13 linear display definitions in the script.
The dedicated scene-display-object chapter documents the layout, commands,
QEMU addresses, inspector output, and boundary between display state and
scripted gameplay state.
Combat animation and actions
The seven COMBAT*.BIN programs contain 214 animation definitions with 2,596
nine-byte steps and 27 selectable action targets. Animation runtime records
begin at DS:6EBA with a 12-byte stride; they retain the first/current BIN
step offsets, timing, linked slot, mode, and render slot. Dedicated opcodes
start, link, stop, wait for, and branch on those animations.
Selectable actions use a separate ten-byte table at DS:480E. Each record
contains an absolute BIN target, screen coordinates, a selector-string
offset, and an active byte. The first ART resource in every combat is
COMBTAGS; rendering its four frames identifies selectors .11 through
.14 as ATTACK, DEFEND, RETREAT, and COMBAT. Pointer and keyboard
paths search active targets and start a BIN scheduler slot at the selected
target.
Random branches, the Sword and Shield flags, opcode-0x81 faith loss, and
map/progression changes live in script state. Every Retreat target jumps
around the victory mutation into a shared exit. Six ordinary combats mark
the current map cell as kind 0xB; the exceptional guard program uses kind
0xA, copies parameter B to A, omits faith loss, and never sets the
combat-active flag. COMBAT7 implements the Zapper reward by ending a meter
flash at faith 10,000. No enemy-health field exists in the display or
action-target records. The combat-runtime chapter documents the tables,
commands, action outcomes, shared epilogue, and remaining dynamic validation
boundary.
Conversation and choice flow
Scene scripts construct dialogue menus in a separate transient table. Opcode
0x45 clears the table, opcode 0x44 appends a six-byte target/text record,
and opcode 0x46 presents the menu and suspends the scene thread. The record
count is at DS:B428; records begin at DS:B116 and contain an absolute BIN
target followed by a far pointer to the inline choice text. The generic text
menu at 0x2556 returns the selected target through DS:7CBA, allowing the
interpreter to resume directly at the chosen branch.
That renderer builds one 16-byte input record per wrapped row. The record’s
first two words are (text_x + 72,row_y + 2), while words at offsets 8 and
10 place the SELECT sprite to the left or right of the text. Selector
0x6CE5 calls the approximate-distance helper at 0x3315, accepts the
strictly nearest record at a distance no greater than 100, and rejects a
nearest record whose marker byte at offset 15 is FE. The same machinery
serves choices, Game Options, slot lists, and confirmation menus.
Dialogue opcodes 0x14, 0x48, and 0x4E share a presentation handler but
serve distinct channels dominated by the adversary, other characters, and
Captain Bible. Corpus analysis finds 40 choice definitions and 597 dialogue
commands. A visible, silent QEMU capture of the five-choice BOSS.BIN menu
matched every target and far text pointer from the static decode. Selecting
the final row wrote target 0x095C and displayed the dialogue stored at that
exact branch.
Conversation scripts also invoke the study-Bible browser. Opcode 0x7D
selects a victim-conversation, paraphrase, or cyber-lie prompt, and opcode
0x49 requests the browser. A correct descriptor sets state flag 0x14;
leaving without the expected match sets 0x15. The conversation-flow
chapter documents the command lifecycle, runtime structures, BOSS memory
correlation, study integration, and remaining boundaries.
World-map state
The archive contains 21 exact 768-byte map resources: levels A through G for
Easy, Normal, and Difficult modes. Opcode 0x78 constructs their names and
loads one into a mutable row-major 16×16×3-byte grid. Address calculations in
the executable use 48*y + 3*x; the first cell byte contains independently
used connection and location-kind nibbles, followed by two parameters.
The map screen also consults a 16-word exploration bitmap. Scene commands can
process the current cell, mutate each cell field, normalize location kinds,
and mark coordinates explored. The supplied SV3 and SV4 grids match
CE.MAP except for four explained field changes. The dedicated world-map
chapter gives the format, opcodes, save correlation, and inspection tool. It
also decodes connection directions, five room classes and orientations, the
seven hallway Cybers, Scripture stations, hidden Spider triggers, cleared
encounters, level exits, and locked-room actions while keeping three
environmental kinds explicitly unresolved.
Script state and progression
The two 200-byte save blocks are checkpoint and live copies of 100 signed script-variable words. BIN commands encode variables as even byte offsets within this block and provide copy, immediate assignment, signed comparison, branching, arithmetic, increment/decrement, and bitwise operations. Static handlers and complete-corpus validation identify 39 variables used by that core family.
Words 3 through 10 double as a 128-bit state-flag bank. Dedicated scene
commands branch on, set, and clear flags. The executable rebuilds transient
map flags 0x00..0x2F, while flags 0x30..0x34 are the five powerups and
seven victim scenes set distinct rescue flags 0x3A..0x40. Variable 21 is
faith on a 0–10,000 scale; opcode 0x81 applies difficulty-scaled loss.
Separate text-descriptor state bytes connect obtained or completed text
records to scene branches. The script-state chapter documents the complete
recovered families and remaining semantic boundaries.
Digital effects and XMIDI music
Opcode 0x57 passes an effect number and rate to 0x417F. That routine
formats D###.ABT, loads it from the archive, allocates the decoded sample
count from the first word, calls the built-in decoder at 0x92E0, and submits
the resulting PCM state to the DIGPAK interface on interrupt 66h.
The decoder implements absolute samples, run-length commands, and packed
one-, two-, or four-bit adaptive delta blocks. Its helpers at 0x93BE,
0x94CB, and 0x956E add signed table deltas to the preceding sample and
clamp to unsigned eight-bit PCM. All 41 resources decode exactly to 412,282
samples at 9,000 Hz. A QEMU breakpoint immediately before playback captured
D003.ABT’s live 9,064-byte buffer; it is byte-identical to the host decoder.
Music function 0x4091 chooses MUS###.XMI or IBM###.XMI. All 32 resources
are IFF/XMIDI files with FORM XDIR, one INFO sequence count, CAT XMID,
and one FORM XMID containing TIMB and EVNT. The audio-format chapter
documents both formats and their reproducible tools.
Startup initializes the family word at 0x76E8 to zero, then compares
SOUND.2 offsets 0xD2..0xD4 with IBM at 0x35C3..0x35DE. A match changes
the word to one; play_music_resource uses that word to choose the filename
prefix. The supplied Sound Blaster Pro FM SOUND.2 has 00 20 02 there, so
this installation selects the MUS family.
Checked symbol catalog
The complete checked map now contains 140 named functions, 26 named BIN
handlers, and 9 data symbols. Each entry has its own evidence statement and a
Verified or High confidence rating. The dedicated symbol-map chapter defines
those levels, summarizes subsystem coverage, documents address translation,
and gives commands that check the catalog against both analysis/cb.rz and
Rizin’s resolved handler addresses.