BRE 0.988 static disassembly map¶
This repository contains a machine-readable map of the original BRE 0.988
executable and overlay, but no original program bytes. The map makes ordinary
analysis static and repeatable: an overlay call in BRE.EXE can be followed to
an exact byte in BRE.OVR, and reachable code can be decoded from known
instruction boundaries without first running BRE.
The companion tool is scripts/bre-disasm.py; the generated catalog is
docs/dev/bre-v0988-disassembly.json. Both are deliberately limited to the
main runtime (BRE.EXE, BRE.OVR, and the resident Turbo Pascal runtime).
BREDATA.EXE is not another game executable: it is an ARJ self-extracting
installation payload containing documentation, help, artwork, samples, and
initial/template data. It has no role in runtime overlay or floating-point
loading and therefore needs no disassembly map.
Quick start¶
Use a BRE 0.988 distribution you obtained lawfully. Do not add its binaries, memory dumps, debugger logs, strings, or disassembly output to this repository.
# Verify an existing copy.
python3 scripts/bre-disasm.py verify --directory /path/to/bre
# Find procedures, basic blocks, or data chunks without having the binaries.
python3 scripts/bre-disasm.py list --kind procedure --filter pirate
python3 scripts/bre-disasm.py list --kind block --filter pirate
python3 scripts/bre-disasm.py list --kind data --filter message
python3 scripts/bre-disasm.py list --kind dispatch
python3 scripts/bre-disasm.py lookup crown_tax
# Find all functions and blocks referring to a Pascal string containing text.
python3 scripts/bre-disasm.py find-string --directory /path/to/bre "bank"
python3 scripts/bre-disasm.py find-string --directory /path/to/bre \
--function create_trade_offer "Trade Deal"
# Resolve a linked far-call target or an OVR file offset.
python3 scripts/bre-disasm.py map --directory /path/to/bre --address 084d:0020
python3 scripts/bre-disasm.py map --directory /path/to/bre --ovr-offset 0x4ba48
# Disassemble a named procedure, or a whole/ranged overlay unit.
python3 scripts/bre-disasm.py disasm --directory /path/to/bre \
--procedure create_trade_offer
python3 scripts/bre-disasm.py disasm --directory /path/to/bre --unit ovr_04b9d0
python3 scripts/bre-disasm.py disasm --directory /path/to/bre \
--unit ovr_04b9d0 --start 0x1d50 --end 0x21a0
# Show synchronized context around an OVR file offset, resident address, or site ID.
python3 scripts/bre-disasm.py disasm --directory /path/to/bre \
--around 0x0268a1 --instructions 40
python3 scripts/bre-disasm.py disasm --directory /path/to/bre \
--around 056d:01bf --instructions 24
# Inspect a procedure's graph without binaries, then show its exact call sites.
python3 scripts/bre-disasm.py xrefs create_trade_offer
python3 scripts/bre-disasm.py xrefs create_trade_offer --directory /path/to/bre \
--show-sites --direction callers
# Verify that the committed names and spans exhaustively partition both files.
python3 scripts/bre-disasm.py check-catalog
Downloading is always explicit. fetch retrieves the official 0.988 archive,
checks its pinned SHA-256, extracts BRE.EXE and BRE.OVR, and verifies both
files. It never runs the archive or any DOS program. Add --include-docs to
unpack the nested BREDATA.EXE archive into DESTINATION/reference; this uses
the host extractor and does not execute the self-extracting DOS payload.
python3 scripts/bre-disasm.py fetch /path/to/private/bre-0.988
python3 scripts/bre-disasm.py fetch /path/to/private/bre-0.988 --include-docs
The supported files are exact. A different release is rejected instead of being silently interpreted with 0.988 addresses:
| Artifact | Size | SHA-256 |
|---|---|---|
official brev988.exe archive |
337980 | 40c9d78066add460176a326ebe9f01f3b39df4f70a7bc060ec8ecf09d875b3d5 |
BRE.EXE |
91712 | ae1ce21a01b6b21840e603090e674286fd6848462298f479498d6c17ef31dde6 |
BRE.OVR |
364835 | c9d6a40261634f6b29c0b3bbf7e8fe8582106fb39ee3f49059c775434164d2c0 |
The address model¶
BRE.EXE is an MZ executable with a 0x2940-byte header. Addresses quoted as
logical segment:offset are relative to the beginning of its load module, not
to the PSP and not to a particular DOS run. Convert one to an EXE file offset
with:
file_offset = 0x2940 + logical_segment * 16 + offset
BRE.OVR starts with FBOV and a four-byte payload length. Its first overlay
unit begins at file offset 0x000008. The EXE contains 103 overlay descriptors
and 414 exported stubs. Their OVR records form an exact, gap-free chain through
the end of the file.
Each descriptor has this on-disk prefix:
| Offset | Size | Meaning |
|---|---|---|
+0x00 |
4 | CD 3F 00 00 descriptor marker |
+0x04 |
4 | OVR code file offset |
+0x08 |
2 | code size in bytes |
+0x0a |
2 | fixup-stream size in bytes |
+0x0c |
2 | exported stub count |
+0x0e |
2 | previous descriptor's logical segment |
+0x10 |
2 | runtime load segment (zero on disk) |
+0x14 |
2 | runtime loaded-list link |
+0x20 |
5 each | exported stubs |
An unloaded stub is exactly:
CD 3F <entry:u16le> 00
The interrupt handler reads the entry word, loads the unit, and rewrites every stub in that descriptor as:
EA <entry:u16le> <runtime-unit-segment:u16le>
It then returns to the rewritten instruction. Consequently the static mapping is exact and does not depend on the runtime unit segment:
canonical OVR root = descriptor.ovr_code_offset + stub.entry
For example, logical stub 084d:0020 contains entry 0679 in the descriptor
whose unit starts at OVR offset 0x582d7. It maps to BRE.OVR 0x58950 and has
the stable name ovr_0582d7_entry_0679.
Fixups and materialized units¶
Each unit's code is immediately followed by its fixup stream. The stream is a
sorted array of little-endian uint16 offsets into that unit's code. For every
entry, the loader performs:
word[unit + fixup_offset] += DOS_EXE_load_segment
These words are linked logical segments for resident routines and other overlay descriptors. They are not adjusted by the dynamic segment where the overlay unit itself happens to be stored. Therefore the untouched OVR is the preferred canonical image for cross-references; use load base zero for normal static analysis.
materialize reproduces the bytes from a particular DOS run when a runtime
load base is useful:
python3 scripts/bre-disasm.py materialize \
--directory /path/to/bre --unit ovr_0582d7 --load-base 0x1a2 \
--output /tmp/unit.bin
The model was checked against a debugger dump of unit ovr_0582d7: its 98
fixup words all differed by 0x01a2, the resulting image matched byte for byte,
and no byte outside a fixup word changed. The reusable check is:
python3 scripts/bre-disasm.py compare-dump \
--directory /path/to/bre --unit ovr_0582d7 --dump /tmp/MEMDUMP.BIN
Procedures, blocks, chunks, and names¶
The catalog gives every exported stub root a deterministic fallback name:
ovr_<six-digit-unit-file-offset>_entry_<four-digit-entry>
Direct near-call targets reachable from those exports are also proven procedure roots and receive:
ovr_<six-digit-unit-file-offset>_proc_<four-digit-entry>
Known routines and internal jump targets get semantic primary names such as
run_bank, calculate_crown_tax,
allocate_turn_budget__armed_forces_maintenance, or
text_read_shortstring; their old address name remains an alias. Curated names
live in scripts/bre-semantic-names.json, separate from the reachability
engine. Each record carries one of four honest naming states:
identified: behavior is supported by call-graph, owned-data, instruction, public RTL, or existing repository evidence;contextual: an internal loop, branch, join, or return is named within one identified procedure without claiming more behavior than the CFG proves;structural: an overlay fixup stream or descriptor boundary is proven by the file format;unclassified: only the stable address-derived fallback is known, and the catalog says so explicitly.
Interior addresses already cited by the project's mechanics work are recorded separately as landmarks. A landmark is not falsely promoted to a procedure merely because it is useful. The semantic manifest records paraphrased topics, not strings copied from the proprietary binary.
Reachability uses Capstone in 16-bit x86 mode. Decoding begins independently at every exported root and follows only fallthrough and typed direct-control-flow operands. Newly encountered direct calls become procedure roots. Every direct jump destination and every conditional fallthrough becomes a named basic-block boundary. Stable fallback names are:
ovr_<unit-file-offset>_loc_<unit-offset>
exe_<logical-segment>_loc_<logical-offset>
The resident pass uses segmented 8086 addresses rather than pretending that the MZ load module is one flat code segment. It begins at the MZ entry point, the traced overlay loader, the known runtime helpers, and every direct resident far target found in reachable overlay code. Direct resident near and far calls and jumps then extend that graph. Overlay descriptor records are excluded from resident decoding and get their own names.
Every byte not reached from those proven roots belongs to a named, contiguous complementary chunk:
ovr_<unit-file-offset>_data_<unit-offset>
exe_data_<load-offset>
ovr_<unit-file-offset>_fixups
ovr_<unit-file-offset>_descriptor_record
Pure zero, NOP, and breakpoint-fill chunks are classified as such. Other
chunks are conservatively classified as unreached_data_or_indirect_code:
they are safe boundaries for a rooted disassembly, but may contain code that
can only be reached through one of the explicitly recorded indirect calls.
Existing semantic landmarks split a chunk and supply its primary name.
The catalog therefore records:
- a stable name, source edge, and half-open span for every procedure and basic block target;
- the union of root-reachable instruction ranges for each unit;
- a named partition of every complementary unreached range and every overlay fixup stream;
- grouped direct and calculated call edges, including the complete closed target set and assignment evidence for every reachable indirect transfer;
- caller and callee lists, exact body ranges, and code-segment data references
for every procedure, plus the inverse
referenced_byrelation on data; - a bidirectional procedure call graph whose caller, callee, and instruction sites use durable address IDs rather than friendly names as their identity;
- an index of directly referenced Pascal strings, keyed by durable address IDs and retaining only lengths, hashes, and durable code references;
- any target that conflicts with an already decoded instruction boundary.
Ranges use half-open bounds: [start, end). They are intentionally not broad
"from this prologue to the next prologue" envelopes. The current catalog has
603 overlay procedure roots, 8,495 overlay basic blocks, 319 overlay data/code
chunks, 389 resident procedure roots, 2,921 resident basic blocks, 231 resident
data/code chunks, and 103 named fixup streams containing 16,672 fixups. Its
12,668 stable location names are unique. It indexes 2,350 directly referenced
Pascal strings and 2,571 block-use records without retaining their text. The
naming pass identifies 400 of the
992 proven procedures and ties 282 of 550 complementary chunks to identified
behavior. Of the basic blocks, 6,862 targets have procedure-context names and
441 entries have behavior-specific names. The remaining 592 procedures and 165
non-structural chunks are explicitly unclassified rather than being given
speculative names. The procedure graph contains 6,541 grouped outgoing edges at
20,324 call sites. All edges resolve bidirectionally by durable ID. Recursive
target discovery reaches a fixed point with 13 closed calculated-transfer
groups, 23 indirect call sites, 29 group-to-target memberships, zero unresolved
transfers, and zero decode-boundary conflicts.
check-catalog independently verifies that named block spans exactly cover all
reachable bytes, named chunks exactly cover all remaining bytes, together they
cover each overlay code area and the complete resident load module, every
procedure has a same-name entry block, names are unique, and the recorded
summary counts agree. It also verifies that every durable ID matches its file
address and that every indexed string use resolves to known block and procedure
IDs. Known call edges must appear identically in the caller and callee directions,
and call-site IDs are recomputed from their containing binary addresses.
list --kind procedure|block|data|fixup|dispatch|all emits TSV, Markdown, or JSON,
and --status identified|contextual|structural|unclassified selects a naming
state. lookup NAME_OR_ID returns the matching records, evidence, call graph,
and data references for a stable name, semantic alias, or durable ID (a
procedure entry is also its first block).
Every procedure record is directly walkable as a graph node. Follow
callees[].to_id to descend into things it calls and callers[].from_id to
walk back to its callers; pass either ID to lookup to load the next node.
Each grouped edge retains its kind and local sites, while site_ids identify
the exact instructions as canonical EXE load offsets or OVR file offsets.
An indirect edge has kind calculated_call and a dispatch_id linking it to
the closed-set proof described below.
xrefs NAME_OR_ID is the compact graph lookup intended for this traversal. It
needs only the committed catalog unless --show-sites is requested. With
private binaries available, --show-sites --direction callers|callees|both
renders bounded instruction windows at the recorded site IDs; --context and
--max-sites control their size and count.
Closed calculated transfers¶
calculated_transfers records why every reachable indirect call is finite.
Each durable dispatch record contains its exact instruction site_ids, source
model, complete procedure target list, and concise assignment-tracing evidence.
The models found in this linked release are far procedure parameters, fixed
global procedure slots, a heap-linked callback list with a sole constructor,
Turbo Pascal TextRec method fields, and a near scanner callback passed in AX.
There are no reachable indirect jumps and no open-ended indirect calls.
Target discovery is recursive. The original 22 indirect sites supplied roots
for callback-only code; decoding those roots exposed one further indirect call
inside the text driver. Resolving that site exposed no more, establishing the
23-site fixed point. check-catalog requires every calculated target to be a
procedure root, every dispatch to appear in the bidirectional call graph, every
site to belong to exactly one group, and both unresolved-transfer lists to be
empty.
find-string SUBSTRING is the bridge from private binary text to the static
map. It loads the committed table first, verifies the exact BRE 0.988 binaries,
materializes the indexed Pascal strings, and performs a case-insensitive search.
The default JSON lists every currently named function and basic block that uses
a match. Address-derived IDs such as bre0988:ovr:block:02ef0f are stored in
the table, so improving a friendly name does not break the relation. Add
--case-sensitive for an exact-case search or --details to include the
matching private text and instruction sites. Repeat --function NAME_OR_ID to
restrict uses to one or more exact procedures after resolving their durable
IDs. Detailed output contains original program text and must not be committed.
disasm accepts three boundary-safe selectors. --procedure prints the exact
catalogued body ranges for an overlay or resident procedure. --unit prints an
overlay unit and optionally accepts a half-open, unit-relative --start/--end
range. --around accepts a canonical OVR file offset, resident logical
SEGMENT:OFFSET, durable site ID, or exact procedure selector.
An --around address is never assumed to be an instruction boundary. The tool
finds the containing catalogued code block, supplies all catalogued roots to
the decoder, and anchors the window at the nearest containing/preceding root.
It prints both the requested offset and synchronization anchor in the header,
never prints instructions before that anchor, and rejects an address in a
catalogued data span instead of guessing. This same synchronization is used by
xrefs --show-sites.
Useful audit queries are:
python3 scripts/bre-disasm.py list --kind procedure --status identified
python3 scripts/bre-disasm.py list --kind procedure --status unclassified
python3 scripts/bre-disasm.py lookup resolve_received_trade_offer
python3 scripts/bre-disasm.py lookup bre0988:ovr:procedure:0389d6
python3 scripts/bre-disasm.py lookup bre0988:dispatch:text_scanners
python3 scripts/bre-disasm.py find-string --directory /path/to/bre "trade"
python3 scripts/bre-disasm.py find-string --directory /path/to/bre \
--function resolve_received_trade_offer "trade"
python3 scripts/bre-disasm.py xrefs resolve_received_trade_offer
python3 scripts/bre-disasm.py disasm --directory /path/to/bre \
--around bre0988:ovr:site:03b7b0
Capstone 5's Python binding and native library are required to regenerate
reachable spans. On systems where the native library is in a nonstandard
location, set LIBCAPSTONE_PATH. ndisasm is optional and is used only by the
human-readable disasm command.
Regenerate the catalog only from the pinned files:
python3 scripts/bre-disasm.py analyze \
--directory /path/to/bre --output docs/dev/bre-v0988-disassembly.json
python3 scripts/bre-disasm.py check-catalog
One-time debugger validation¶
Normal mapping and disassembly do not require DOSBox. The debugger remains
useful for validating a loader claim or materializing an unusual indirect
target. This DOSBox build has its heavy debugger enabled, but the SDL window
still needs a display. debugger --run starts DOSBox under a private Xvfb,
sets TERM=xterm (required for the function-key escape sequences), sends
Alt-Pause to the X window, and cleans up Xvfb when DOSBox exits:
python3 scripts/bre-disasm.py debugger --directory /path/to/bre --run
At the debugger prompt, press Enter to enter command mode before typing a command. A minimal overlay trace is:
BPINT 3F
F5
At the trap, F10 enters the handler. MEMDUMPBIN SEGMENT:0000 SIZE writes a
unit image. LOG count writes an instruction trace. Both outputs contain
original program material and belong only in a private temporary directory.
The observed handler is at runtime 10fd:02e6 when the DOS load base is
0x01a2, hence logical 0f5b:02e6. It loads through EMS (INT 67h), patches
the descriptor stubs, and jumps to the requested entry. Calls to Turbo Pascal
integer and Real48 helpers go to resident logical segments in BRE.EXE (notably
0fd0). No separate floating-point library file is opened or loaded; there is
no FP overlay whose segment must be guessed.
The exact operation surface, six-byte representation, BRE-linked constants, Python port, and calculator are documented in bre-real48.md.
Two integer helpers in 0c03 the catalog does not name yet, identified from two
independent call sites each while reading the nuclear strike and the waste
decontamination routines:
| Address | Behaviour |
|---|---|
0c03:129b |
max of two int32 arguments |
0c03:12e1 |
min of two int32 arguments |
The nuclear strike caps its price with 12e1 against 50,000,000, and the
decontamination allowance is min(max(waste / 5, 10), waste) — one use of each,
in the only arrangement that makes both routines sensible.
Empire record fields identified from their whole access list¶
Both were found by scanning each binary for every es:-prefixed [di+disp16]
access and every add di,disp16 at the field's displacement, in the
current-empire form (disp) and the arbitrary-empire form (disp + 0xf093),
then naming the containing block for each hit. The method is the one the
bre-gather skill describes; what makes it conclusive is reading all the
sites, not the two nearest the question.
+0x286 — Score (int32). Fourteen sites. Written by resolve_regular_attack
(two branches, gated on config record +0x3d8), launch_nuclear_attack,
launch_chemical_attack, launch_biological_attack, launch_pirate_raid,
resolve_returning_attack and send_spy; read by show_empire_status,
show_scores, build_recon_record, and format_player_ranking_line. That last
one settles it: the rankings line prints total_regions, then this field, then
the net-worth function — Territory, Score, Net Worth, in the scores table's own
column order.
+0xba — the unallocated-region pool (int32). Credited by a won regular
attack, a pirate raid, a returning interplanetary attack, and waste
decontamination; drained by the picker that prompts [N Regions left] and
How many <Type> regions?, which adds the named count to the chosen type's field
and subtracts it here, looping until the pool is empty.
total_regions (056d:0ec6) sums exactly the nine region counts at +0x96
through +0xb6+2 and does not include this pool, so unallocated land is not
territory until it is placed.
A catalog name corrected¶
allocate_unassigned_regions (BRE.OVR 0x030ebb) was catalogued as
select_regions_to_lose, which the field access list contradicts: it returns
immediately when the pool is empty, and its body adds the chosen count to a
region type while subtracting the same from the pool. Its four callers are the
three that credit the pool — a won regular attack, a pirate raid, waste
decontamination — plus run_player_turn, which re-offers whatever is left. The
key handling is the region-picker's: a type letter, ? to redisplay the list,
* for the advisors, Enter to leave.
Renamed in the catalog with select_regions_to_lose retired to aliases, so
the old name still resolves — the convention calculate_crown_tax already
follows. Every record derived from the name (its 23 branch/loop/join blocks)
was renamed with it, and the "to"/"from" call-graph references were rewritten
to match.
Editing the catalog by hand: it round-trips exactly through
json.dumps(catalog, indent=2) plus a trailing newline, so a structural edit
need not reformat the file — dumping with any other setting produces a
million-line diff. The validation block at the end is a set of counters over
the catalog's own contents and goes stale on any edit; recompute it with
validate_catalog from scripts/bre-disasm.py and confirm with
bre-disasm.py check-catalog. Retiring a name into aliases raises
unique_names by one, because the old name stays resolvable.