Inertia Decompiler is a toolchain for decompiling and comparing 16-bit and
32-bit real-mode DOS programs. It supports the DOS segmented-memory model and
processes real-mode .EXE, .COM, and assembly listings from DOS-era
toolchains.
Inertia Decompiler uses a dual-license model. Community use is available under the GNU Affero General Public License, version 3 or any later version. A separate commercial license is available for organizations that need rights outside the AGPL's conditions. See License.
Correctness comes before pretty C. When evidence is weak, the tools prefer low-level output, visible refusals, or explicit fallback reports over guessed source.
Inertia focuses on the results that matter when restoring a DOS program:
- More of the program becomes C. The goal is to recover every function instead of silently omitting difficult functions or leaving most of the program as an assembly listing.
- The C is checked against the executable. Recovered conditions, calls, return values, and memory changes are validated. Code that looks cleaner but changes the program's behavior is rejected.
- The output is intended to compile. Inertia can generate portable C or C suitable for Microsoft C on DOS. It checks for common decompiler failures such as wrong function arguments, incompatible pointer types, missing declarations, and invalid assignments.
- The output is easier to understand. Proven stack locations become arguments and local variables, branches become meaningful conditions, and repeated memory layouts can become arrays or structures. When a readable interpretation cannot be proved, Inertia keeps a lower-level representation instead of inventing one.
- Stripped executables are first-class inputs. Debug files and original
source are not required. When old
.COD,.LST,.MAP, CodeView, or TDInfo files are available, they can restore useful names and source locations. - Library code creates less noise. Signatures from old DOS compilers, object files, and libraries help identify runtime and library functions so the report stays focused on the program's own code.
- You can test whether a rebuild still behaves like the original. Included comparison tools can run both versions and compare function results, changed memory, and other externally visible behavior.
- Uncertain output is clearly marked. Timeouts, incomplete recovery, failed validation, and fallback output remain visible. A plausible-looking function is not presented as trustworthy C without supporting evidence.
No decompiler can reconstruct every original name, type, or source construct from machine code. Inertia is designed for cases where complete, recompilable, behaviorally checked C is more useful than attractive but unverified pseudocode.
Function catalog recovery has its own --catalog-timeout budget, defaulting
to 60 seconds. See catalog, unpacking and DOS-tool setup
for limitations and installation hints. Required compiler checks are not
silently skipped.
The repo is tested against the angr stack pinned in pyproject.toml. Python 3.15.1 is required; .python-version and CI select the same interpreter.
git submodule update --init --recursive
python3.15 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install -e .
python -m pip install -e ".[test]"The root ./decompile.py wrapper re-execs through ./.venv/bin/python when that virtualenv exists.
The 16-bit VEX lifter uses a required
Cython build from the same Python source.
Build it with PYTHON_JIT=1 .venv/bin/python tools/dev/build_cython_vex.py.
Wheel builds compile and bundle the lifter with its source/ABI/hash manifest;
building a wheel requires a C compiler and Python development headers. Installed
packages use that verified bundle rather than the repository build cache.
Normal startup rejects missing or stale builds. Select
INERTIA_VEX_BACKEND=python only when explicitly requesting interpretation.
Three executable root wrappers expose the standalone tools. Activate the
virtualenv (source .venv/bin/activate) or call .venv/bin/python
explicitly; the wrappers do not install dependencies or re-exec interpreters.
| Command | Implementation | Purpose |
|---|---|---|
./ada.py |
tools.ada_script.cli |
Annotated DOS disassembly reports (ASM/LST/SQLite) |
./z3func.py |
tools.dosunit.dosunit |
Static SSA/Z3 and concrete function comparison |
./dosunit.py |
tools.dosunit.dosunit |
Same command implementation as ./z3func.py, under the DOSUnit name |
./z3func.py and ./dosunit.py share one command implementation and
subcommand set; only the displayed program name differs. Inspect each
interface from an activated environment:
./ada.py --help
./z3func.py --help
./dosunit.py --helpADA (./ada.py, package tools/ada_script) produces annotated assembly
listings and a queryable analysis database for DOS MZ/USE16 executables; PE,
NE, LE and LX payloads behind an MZ stub are refused. It disassembles with
Capstone by default or an explicitly selected Rizin backend
(--backend rizin), imports a supported subset of IDC scripts and
libdosbox-style runtime JSON traces, and proposes library names through the
shared PAT signature matcher — naming evidence, not proof. Analysis adds
code/data cross-references and automatic data and string discovery, then
emits GAME.asm, GAME.lst, analysis.db, a Markdown report and
signatures.json:
./ada.py /path/GAME.EXE --work-dir .cache/ada/game --full --xrefsUse a fresh --work-dir per binary: the loader recreates analysis.db on
each run. ADA emits annotated assembly artifacts only; C generation belongs
to the separate Inertia decompiler, and ADA results are not semantic
verification. See the component README for the
full option and evidence contract.
ADA is a narrow batch tool; IDA Pro is a broad interactive disassembler. This is a feature comparison, not a benchmark or measured head-to-head evaluation. Neither produces a semantic-equivalence proof.
| Aspect | ADA (./ada.py) |
IDA Pro |
|---|---|---|
| Focus | DOS MZ/USE16 batch analysis reports | Interactive multiarchitecture disassembly and database |
| Interface | CLI with SQLite analysis.db plus .asm/.lst artifacts |
Interactive navigation, types and database views |
| Extensibility | Supported IDC subset, runtime JSON import, shared PAT-library naming | IDAPython, full IDC, FLIRT signatures, plugin and debugger ecosystems |
| Output | Annotated assembly and report artifacts (generated C belongs to the separate Inertia decompiler) | Hex-Rays decompiler availability depends on processor module and license |
Use this order for C, assembly, and mixed DOS games:
flowchart TD
A[Original binary and game assets] --> B[libdosbox batch collection]
A --> C[Static analysis]
B --> C
C --> D[ada_script: annotated ASM and LST]
C --> E[Inertia with Ghidra or Reko: C reconstruction]
D --> F[masm2c: translated C++]
D --> G[DOS assembler, compiler and linker]
E --> G
G --> H[Byte and instruction checks, then SSA/Z3]
H --> I[dosunit concrete oracle tests]
F --> J[libdosbox translated-runtime comparison]
I --> K[Full game scenarios]
J --> K
K --> B
Use the real-mode model for both 16-bit instructions and 386 instructions with 32-bit operands/addresses in real mode. EAX usage does not make a program a flat32 executable. Protected-mode DOS extenders and the PE32 MSC compiler binaries require separate loaders/contracts; see the flat32 comparator.
The commands below are a local workflow template. CLI options and relevant source paths were checked on 2026-09-26; a complete game run was not performed for this documentation. Replace the game paths, configuration and build recipe. Do not proceed past a failed stage or treat missing artifacts as success.
Required inputs are the original executable, its game assets, CPU/memory mode, and a reproducible scenario. Optional inputs include IDC, MAP/LST/COD, symbols, original ASM/C, runtime dumps and library signatures. Preserve originals; run the game from a copy because games and instrumentation can write files.
export VEXTEST=/home/xor/vextest
export PY="$VEXTEST/.venv/bin/python"
export PYTHON_JIT=1
export ADA="$VEXTEST"
export MASM2C=/home/xor/masm2c
export LIBDOSBOX=/home/xor/inertia_player/libdosbox
export MZTOOLS=/home/xor/games/f19ru/F19/mzretools/build
export WORK="$VEXTEST/.cache/game-reconstruction/run-001"
export GAME_SOURCE=/absolute/path/to/original-game-directory
export GAME_EXE=GAME.EXE
mkdir -p "$WORK"/{game,analysis,src,build,reports,runtime,translated}
cp -a "$GAME_SOURCE/." "$WORK/game/"
export ORIGINAL="$WORK/game/$GAME_EXE"
export CANDIDATE="$WORK/build/REBUILT.EXE"
sha256sum "$GAME_SOURCE/$GAME_EXE" "$ORIGINAL" > "$WORK/reports/input.sha256"Use a fresh WORK for each collection scenario. Record component revisions and
local changes, compiler/linker flags, DOSBox settings, scenario actions, limits,
and artifact hashes. Runtime evidence must include actual load-segment metadata.
Never confuse file offsets, analysis VAs, module offsets and runtime CS:IP.
| Artifact | Required content / consumer |
|---|---|
| Runtime JSON | Executed code, observed edges, segment values, access widths/directions, pointer evidence and load metadata; ada_script and dosunit import |
| ASM/LST and analysis DB | Bytes, labels, boundaries and annotations; reassembly, masm2c and catalog discovery |
| Function catalogs + mapping | Original/candidate correspondence with correct address model; SSA and concrete comparisons |
| Candidate + build record | Linked image, maps, retained objects, exact toolchain/options; comparison input |
| Test vectors | Registers, flags, segments, valid memory, stack/return setup and observables; concrete execution |
| Proof/test reports | Requested coverage, passes, mismatches, conditional results, refusals and assumptions; acceptance review |
The local instrumentation build is
$LIBDOSBOX/build/custom-instrument/dosbox. To build it when needed:
"$LIBDOSBOX/scripts/build_custom.sh" instrumentPrepare $WORK/runtime/dosbox.conf for the game's machine, CPU, cycles and
devices. Use core = normal in its [cpu] section for the inspected normal-core
collection hooks. The instrumentation build starts in its analysis profile and
has original-code collection without converted-game dispatch.
DOSBOX_BIN="$LIBDOSBOX/build/custom-instrument/dosbox"
run_status=0
timeout --signal=TERM --kill-after=10s 30s \
"$DOSBOX_BIN" --noprimaryconf --nolocalconf --noautoexec \
--conf "$WORK/runtime/dosbox.conf" "$ORIGINAL" \
> "$WORK/reports/collection.log" 2>&1 || run_status=$?
printf '%s\n' "$run_status" > "$WORK/reports/collection.exit"This bounds an unattended startup run; it does not automate menu/gameplay input. Use recorded scenario inputs or a game-specific driver for deeper coverage. A display is needed unless a headless display setup has been validated for the chosen renderer. Do not assume dummy SDL video/audio preserves a game scenario.
The inspected instrument source dumps JSON on program exit and SIGTERM/SIGINT.
Exit 124 records a timeout, not successful completion; SIGKILL cannot guarantee
a dump. Locate the Dumping run-time info into ... path in collection.log,
copy that artifact to $WORK/runtime/trace.json, and inspect it before continuing:
export TRACE="$WORK/runtime/trace.json"
"$PY" - "$TRACE" <<'PY'
import json
import sys
from pathlib import Path
trace = json.loads(Path(sys.argv[1]).read_text())
assert isinstance(trace.get('Meta', {}).get('DosboxLoadSeg'), int), 'missing load segment'
assert trace.get('Code'), 'no executed-code evidence'
print('load segment:', hex(trace['Meta']['DosboxLoadSeg']))
print('code sites:', len(trace['Code']), 'data sites:', len(trace.get('Data', {})))
print('call snapshots:', len(trace.get('CallSnapshots', [])))
PYConfirm the log identifies the intended executable; startup may launch overlays or child programs. Check dump freshness and retain separate module identities. Memory dumps and metadata are additional artifacts, not guaranteed by this JSON collection command. Access widths suggest byte/word/dword uses; they do not by themselves prove complete C types. Unobserved code is not dead code.
Run ada_script in an isolated working directory: its loader recreates
analysis.db in the current directory. For supported MZ inputs:
ada_script is now included in this checkout. Install its IDC parser dependency
with uv pip install --python "$PY" -e "$VEXTEST[ada]" (or use pip from that
environment). The analyzer, CLI and signature adapter are maintained together under
tools/ada_script.
(
cd "$WORK/analysis"
PYTHONPATH="$VEXTEST" "$PY" -m tools.ada_script.cli "$ORIGINAL" \
--runtime "$TRACE" --full --classify --xrefs \
-o "$WORK/reports/ada.md"
)
export LISTING="$WORK/analysis/${GAME_EXE%.*}.lst"
export ASSEMBLY="$WORK/analysis/${GAME_EXE%.*}.asm"
"$PY" "$VEXTEST/z3func.py" discover --exe "$ORIGINAL" \
--ida-listing "$LISTING" --out "$WORK/analysis/original.functions.json"
"$PY" "$VEXTEST/z3func.py" import-libdosbox --trace "$TRACE" \
--functions "$WORK/analysis/original.functions.json" \
--out "$WORK/analysis/runtime-evidence.json"Add --idc-script /absolute/path/game.idc when available. Inspect proposed
function boundaries and address mappings. The imported evidence document is
not itself a vector file. Aggregate-only traces provide priorities/access
ranges; replay requires adequate per-call snapshots and explicit vector export.
Library naming uses Inertia's shared PAT matcher before ASM/LST rendering.
The automatic catalog includes library-archive provenance; use repeatable
--signature-catalog /path/runtime.pat for explicit catalogs, or
--no-signatures to disable it. Matched library names replace automatic labels,
while explicit user/IDC names survive. Conflicting matches stay unnamed and
are recorded in signatures.json and the SQLite signature_matches table.
See Ada Script integration for controls and tests.
Generate C independently from the original binary:
INERTIA_ENABLE_TAIL_VALIDATION=1 "$PY" "$VEXTEST/decompile.py" "$ORIGINAL" \
--c-target msc-dos --output-c-dir "$WORK/src/inertia" \
--dump-layers --dump-layer-dir "$WORK/analysis/layers" \
> "$WORK/reports/inertia.log" 2>&1Use --c-target portable-flat for a native reconstruction and --addr for a
focused function. Output/validation failures remain work items.
Open the same hashed binary in Ghidra and/or Reko with the correct loader,
CPU mode and load addresses. Record their function boundaries, cross-references,
type hypotheses and pseudocode under analysis/ghidra or analysis/reko.
Reconcile disagreements against bytes and observations. These tools complement
Inertia; bidirectional import and direct runtime-JSON ingestion into Inertia
are integration work, not flags provided by the commands above. Do not infer
semantics by parsing another decompiler's rendered C.
DOS reconstruction: assemble recovered ASM, progressively replace routines
with C, and use the target compiler/assembler/linker and appropriate CRT.
Produce $CANDIDATE plus $WORK/build/rebuilt.map. A .obj is useful build
evidence, but the current binary-comparison pipeline needs a linked image.
Use the game's build recipe: memory model, near/far ABI, segment order, startup,
libraries and compiler options are not interchangeable across games.
Native translation: translate a copy of the recovered ASM with masm2c:
cp "$ASSEMBLY" "$WORK/translated/"
LOADSEG=$("$PY" -c 'import json,sys; print(hex(json.load(open(sys.argv[1]))["Meta"]["DosboxLoadSeg"]))' "$TRACE")
(
cd "$WORK/translated"
"$MASM2C/.venv/bin/python" "$MASM2C/masm2c.py" \
-j 1 -m separate -lo "$LOADSEG" "${GAME_EXE%.*}.asm"
)Build the resulting C++ with the game's masm2c/libdosbox runtime integration;
translation alone does not link a native game. Preserve segment layout and
dispatch mappings. Use faithful runtime settings, not M2CDEBUG == -1, for
acceptance. Native translated code needs a guest-state adapter for dosunit;
do not feed it to the DOS-image runner or assume flat32 Z3 proves its relation
to segmented original code. Use libdosbox translated-runtime comparison while
that adapter is absent. Existing ASM source can enter this workflow directly.
First check supported MZ instructions with the local mzretools (v1.0.20):
"$MZTOOLS/mzdiff" "$ORIGINAL" "$CANDIDATE" \
--map "$WORK/analysis/original.map" --tmap "$WORK/build/rebuilt.map:link" \
> "$WORK/reports/mzdiff.log" 2>&1This requires a prepared mzretools reference map; an arbitrary linker MAP is
not that map format. For supported inputs without one, omit both map options
for a limited entry-point comparison. Copy maps into the workdir because
mzdiff can emit a companion .tgt map. Do not use ignore/skip/loose settings
as acceptance. Its documented MZ/8086 scope is not verified 386 coverage.
Create the candidate catalog and mapping using the original catalog from step 3:
"$PY" "$VEXTEST/z3func.py" discover --exe "$CANDIDATE" \
--map "$WORK/build/rebuilt.map" --out "$WORK/analysis/candidate.functions.json"
"$PY" "$VEXTEST/z3func.py" make-mapping \
--oracle-functions "$WORK/analysis/original.functions.json" \
--candidate-functions "$WORK/analysis/candidate.functions.json" \
--out "$WORK/analysis/mapping.json"Review the mapping before proving anything; names propose correspondence, they
do not prove it. Use an explicit ABI/observable contract for assembly routines
instead of assuming the default msc16-near ABI in this example:
"$PY" "$VEXTEST/z3func.py" ssa --exe "$ORIGINAL" \
--functions "$WORK/analysis/original.functions.json" \
--cache-dir "$WORK/build/ssa-cache" --out "$WORK/analysis/original.ssa.json"
"$PY" "$VEXTEST/z3func.py" ssa --exe "$CANDIDATE" \
--functions "$WORK/analysis/candidate.functions.json" \
--cache-dir "$WORK/build/ssa-cache" --out "$WORK/analysis/candidate.ssa.json"
"$PY" "$VEXTEST/z3func.py" compare-ssa \
--oracle-ssa "$WORK/analysis/original.ssa.json" \
--candidate-ssa "$WORK/analysis/candidate.ssa.json" \
--mapping "$WORK/analysis/mapping.json" --out "$WORK/reports/z3.json"
"$PY" "$VEXTEST/z3func.py" report-failures \
--results "$WORK/reports/z3.json" --out "$WORK/reports/z3.md"For process-bounded comparison of larger selections, see Z3 Function Comparator.
Byte identity, mapped instruction agreement and Z3 semantic equivalence are different results. Run eligible SSA/Z3 checks before concrete dosunit tests. Keep timeouts/refusals and normalization assumptions visible; a partial-region proof is not a whole-function proof. Preserve counterexamples for replay.
Neither the 16-bit nor the 32-bit SSA/Z3 comparator needs KVM. VEX lifting and symbolic comparison run without executing the DOS binary. Unicorn replay and host GCC checks also do not require KVM. KVM is required when executing the libkvikdos backend or a DOS compiler through kvikdos, including decompiler CLI acceptance tests that recompile generated C with that compiler.
Tests that execute those KVM paths must carry @pytest.mark.requires_kvm.
Tests also carry component ownership markers,
such as decompiler, ssa_z3, dosunit, compiler_detector and ada_script.
Use -m 'ssa_z3 and not requires_kvm' to select static comparator coverage.
Mixed test modules use function-level marks; mocked runners stay unmarked.
From the repository root, select the two test lanes explicitly:
nice -n 10 env PYTHON_JIT=1 .venv/bin/python -m pytest -m 'not requires_kvm'
nice -n 10 env PYTHON_JIT=1 .venv/bin/python -m pytest -m requires_kvmUnavailable KVM skips marked tests with a device-access reason. It does not block static comparator evidence, and a skipped native test is not acceptance.
With reviewed original.functions.json, candidate.functions.json and
mapping.json under $WORK/analysis, generate bounded vectors and inspect
their pointer/memory, stack and return setup before execution:
"$PY" "$VEXTEST/z3func.py" gen-vectors --exe "$ORIGINAL" \
--functions "$WORK/analysis/original.functions.json" --strategy edge \
--max-vectors-per-function 8 --out "$WORK/analysis/vectors.json"
export DOSUNIT_KVIKDOS_C=/home/xor/kvikdos/kvikdos.c
export DOSUNIT_CACHE_DIR="$WORK/build/dosunit-cache"
"$PY" "$VEXTEST/z3func.py" record-oracle --exe "$ORIGINAL" \
--functions "$WORK/analysis/original.functions.json" \
--vectors "$WORK/analysis/vectors.json" --backend libkvikdos \
--out "$WORK/analysis/oracle-vectors.json"
"$PY" "$VEXTEST/z3func.py" compare --candidate "$CANDIDATE" \
--functions "$WORK/analysis/candidate.functions.json" \
--vectors "$WORK/analysis/oracle-vectors.json" \
--mapping "$WORK/analysis/mapping.json" --backend libkvikdos \
--out "$WORK/reports/concrete.json"
"$PY" "$VEXTEST/z3func.py" report-failures \
--results "$WORK/reports/concrete.json" --out "$WORK/reports/concrete.md"The current libkvikdos backend builds a cached wrapper from kvikdos.c; it
needs the native compiler and usable KVM access. Smoke-test it on a small
fixture before a game batch. Backend failure is not candidate equivalence.
The fixture backend tests orchestration, not the actual executable.
Add boundary cases, realistic snapshots and realizable Z3 counterexamples.
Run tested/proved functions as well as symbolic refusals; retain handwritten
C tests for readable intent. Finish with loading, menus, gameplay transitions,
input, graphics, sound, saving/loading and exit as applicable. Reset resources
between original/candidate runs and record scenario coverage. Preserve the
live guest-memory contract of m2c::m when using libdosbox.
You can use libdosbox as a deterministic emulator to compare the original and reconstructed program frame by frame. Give both runs the same initial machine state, game assets, emulator settings and input/timing schedule; align captures at emulated frame boundaries and compare framebuffer bytes or recorded frame hashes, retaining the first mismatch together with the input log so the divergence can be replayed. Determinism holds for a fixed setup, not unconditionally for every input or configuration, and matching frames are scenario evidence, not a universal equivalence proof. This is a workflow step, not a built-in DOSUnit frame-diff command.
Track independent per-function build, instruction-match, symbolic-proof and concrete-test statuses, plus whole-game scenario results. Missing/unmapped functions stay in the denominator. See the toolchain guide and component integration plan.
Main entry points:
./decompile.pypython -m inertia.cli.cli- installed script:
decompile-x86-16
Common runs:
./decompile.py PROGRAM.EXE
./decompile.py PROGRAM.COM
./decompile.py PROGRAM.EXE --addr 0x11423 --timeout 30
./decompile.py LISTING.COD --proc _main --proc-kind NEAR
./decompile.py blob.bin --blob --base-addr 0x1000 --entry-point 0x1000Useful options:
--addr ADDR: decompile one function by linear address.--proc NAME: extract one procedure from a.CODlisting.--max-functions N: cap whole-binary output.--timeout SEC: bound analysis for a function or run.--window BYTES: bound CFG recovery around a target address.--c-target portable-flat|msc-dos: choose generated C target helpers.--api-style modern|dos|raw|pseudo|service|msc|compiler: choose helper naming style.--show-asm: print the first lifted block before C.--trace-c-stages: print labeled generated-C snapshots.--dump-layers --dump-layer-dir DIR: write per-stage C artifacts.--function-discovery-backend auto|angr|rizin|hybrid: choose whole-binary discovery.--signature-catalog PATH: apply a PAT catalog built from.pat,.obj, and.lib.-qorINERTIA_BRIEF=1: reduce progress and diagnostics.
Tail validation can be enabled for decompiler correctness checks:
INERTIA_ENABLE_TAIL_VALIDATION=1 ./decompile.py SORTDEMO.EXEWhen a run is slow, capture compact telemetry:
INERTIA_OTEL_SPANS=1 \
INERTIA_OTEL_SPAN_FILE=.cache/frontend/otel.trace.txt \
./decompile.py PROGRAM.EXE --addr 0x11423See reference/telemetry.md for trace formats and OTLP export.
Supported executable inputs:
.COM- DOS MZ
.EXE - 16-bit NE
.EXEat smoke level .BIN/.RAWblobs.CODlistings as direct procedure inputs
Metadata sidecars are optional but useful:
.CODand.LST: procedure ranges, labels, local names, and source-backed hints..MAP: public symbols and layout.- CodeView NB00/NB02/NB04/NB05/NB08/NB09: debug names, procedures, stack variables, source files, line maps, type names, and member records where supported. NB05 (LINK 5.3x interim layout, e.g. late-DOS Microsoft C v6/v7 games) is decoded via its
sstSegMap, per-module public (0x123) and srcmodule (0x127) subsections, with per-file line attribution. - TDInfo: Borland/Turbo Debugger symbols, stack/register/constant symbols, compact type descriptors, structs, unions, and enums.
.pat,.obj,.lib: library signature matching.
Inspect embedded debug metadata directly:
./dump_debug_info.py PROGRAM.EXEinertia.cli.gdb_symbol_export converts embedded NBxx CodeView or TDInfo
metadata into a minimal ELF32 object that any GDB remote stub can load —
useful for real-mode debugging under emulators (e.g. libdosbox's
gdbserver):
python3 -m inertia.cli.gdb_symbol_export GAME.EXE --base 0x1a10 -o game.elf--base is the linear address where the MZ image is loaded
((load_segment << 4), normally PSP+0x10 paragraphs). The ELF carries a
.symtab (FUNC/OBJECT, .text-relative) plus DWARF v2
.debug_line/.debug_info/.debug_abbrev when the binary has line
records. In GDB:
set architecture i8086
maint set target-async off
target remote localhost:1234
add-symbol-file game.elf 0 # or the load base if --base 0 was used
info symbol $eip # named functions, e.g. "LoadOverlay + 3"
info line *0x401d4 # 'Line 70 of "D:\MONOPRIN.C" ...'Verified on retail and rebuilt F-14 binaries against libdosbox's GDB stub
(see docs/debug-gdb.md in that repository). info line needs a baked
--base; use --no-lines to emit only the symbol table.
More format detail lives in CODEVIEW_SUPPORT.md, DOS_COMPILER_SUPPORT.md, NE_WIN16_SUPPORT.md, and NE_LOADER_INTEGRATION_VERIFIED.md.
The x86-16 frontend supports i386 programs running in real mode, including 32-bit general-purpose registers, FS and GS, and 32-bit operand and address forms. Memory retains the DOS segmented model: segment registers select distinct address spaces and 16-bit or 32-bit offsets are interpreted within the active segment.
Build one deduplicated catalog from FLAIR .pat, OMF .obj, and OMF .lib inputs:
python tools.signatures.cli signature_catalogs/ QLINK/ \
--output signature_catalogs/local.patUse it during decompilation:
./decompile.py PROGRAM.EXE --signature-catalog signature_catalogs/local.patBuild a shareable all-compilers bundle when the local compiler archive exists:
python tools/signatures/build_compiler_catalog_bundle.pyReport likely compiler/runtime matches for a binary:
python -m tools.compiler_id PROGRAM.EXE \
--catalog signature_catalogs/all_compilers_catalog_bundle.zipAdd --compilers-only for a short ranked compiler list.
The compiler matcher can also score likely Microsoft C 5.1 flag combinations when profile data is available:
python tools.compiler_toolchain.build_msc51_flag_profiles \
--cod-dir deep \
--output signature_catalogs/msc51_flag_profiles.json
python -m tools.compiler_id PROGRAM.EXE \
--catalog signature_catalogs/all_compilers_catalog_bundle.zip \
--detect-flags-msc51 \
--msc51-flag-profiles signature_catalogs/msc51_flag_profiles.jsonThe decompiler also has an internal MS C 5.1 local-variable hash diagnostic. It checks whether recovered local names and BP offsets fit the compiler's 16-bucket allocation pattern; this is used as evidence, not as a source of guessed names.
./z3func.py is the main static and concrete function-comparison tool.
./dosunit.py exposes the same flow for compatibility.
The static path catalogs function boundaries and metadata with discover,
proposes oracle/candidate correspondence with make-mapping, lowers bounded
slices to SSA form with ssa and checks paired slices with compare-ssa
(compare-ssa-batched splits large selections into per-process batches).
Comparison is process-bounded and reports proved equivalence separately from
refusals, timeouts and counterexamples; retained counterexamples replay
against concrete vectors later. No single result is an automatic acceptance
verdict.
Typical static SSA comparison:
./z3func.py discover --exe original.exe --map original.map --out /tmp/orig.functions.json
./z3func.py discover --exe rebuilt.exe --map rebuilt.map --out /tmp/rebuilt.functions.json
./z3func.py make-mapping \
--oracle-functions /tmp/orig.functions.json \
--candidate-functions /tmp/rebuilt.functions.json \
--out /tmp/mapping.json
./z3func.py ssa \
--exe original.exe \
--functions /tmp/orig.functions.json \
--cache-dir .cache/dosunit \
--out /tmp/orig.ssa.json
./z3func.py ssa \
--exe rebuilt.exe \
--functions /tmp/rebuilt.functions.json \
--cache-dir .cache/dosunit \
--out /tmp/rebuilt.ssa.json
./z3func.py compare-ssa \
--oracle-ssa /tmp/orig.ssa.json \
--candidate-ssa /tmp/rebuilt.ssa.json \
--mapping /tmp/mapping.json \
--out /tmp/ssa.results.json
./z3func.py report-failures \
--results /tmp/ssa.results.json \
--out /tmp/ssa.failures.mdFor large programs, prefer the batched comparator:
./z3func.py compare-ssa-batched \
--oracle-ssa /tmp/orig.ssa.json \
--candidate-ssa /tmp/rebuilt.ssa.json \
--oracle-index-ssa /tmp/orig.ssa.json \
--candidate-index-ssa /tmp/rebuilt.ssa.json \
--mapping /tmp/mapping.json \
--out-dir /tmp/ssa-batches \
--out /tmp/ssa-batches/aggregate.jsonOther useful comparator subcommands:
complexity: classify which functions are good bounded-Z3 targets.regions/compare-regions: compare lifter-backed operand/effect summaries.compare-ssa-abi: prove ABI-visible equivalence from an ABI manifest.gen-vectors,record-oracle,compare: concrete-vector comparison through the runtime backend.compare-data: compare loaded MZ data ranges after relocation.summarize: print result rollups.
./dosunit.py runs the concrete-execution half of the same command
implementation as ./z3func.py. The harness generates bounded input vectors
(gen-vectors), records oracle outputs on the original executable
(record-oracle), replays them against a candidate (compare) and renders
failures (report-failures).
Two backend classes matter. The fixture backend exercises orchestration
without executing the real DOS image, while libkvikdos runs the binary
through kvikdos and therefore needs usable KVM and a native compiler to build
its cached wrapper from kvikdos.c. Backend failure is not candidate
equivalence, and a passing batch is evidence for the tested vectors only —
not an automatic acceptance claim.
Start an angr-backed DOS GDB remote server:
python -m inertia.cli.debug_dos PROGRAM.EXE --host 127.0.0.1 --port 1234Connect with the Textual TUI:
python -m tools.debugger.gdb_tui --host 127.0.0.1 --port 1234 --arch x86_16The debugger exposes 16-bit registers, segment registers, flags, memory, breakpoints, and stepping through the GDB remote protocol.
Decompile a .COD corpus into sibling .dec files:
python tools/dev/decompile_cod_dir.py cod --timeout 20 --max-memory-mb 1024Useful filters include --cod-file, --proc-name, --skip-existing, and --write-tail-validation-baseline.
Compare discovery engines:
python tools/dev/compare_discovery_backends.py PROGRAM.EXE \
--backends all \
--json-output /tmp/discovery.jsonRun the legacy curated check (full validation package):
make decompiler-check PYTHON=./.venv/bin/pythonFor routine development work, use the hard gate:
make quality-dev PYTHON=./.venv/bin/pythonRun the MS C tiny pipeline examples:
make test-pipeline PYTHON=./.venv/bin/pythonOptional native speedups are available for selected pure-Python modules:
python -m pip install ".[mypyc]"
python tools/dev/build_mypyc.py build_ext --inplaceIf mypyc is unavailable, normal .py execution is unchanged.
Optimization changes must not reduce semantic quality. Use the quality gate before enabling any speed-up path:
make decomp-opt-regression-suite PYTHON=./.venv/bin/python
DECOMP_OPT_REGRESSION_ARGS="--max-functions 20 --timeout 45 -q" \
make decomp-opt-regression-thread PYTHON=./.venv/bin/pythonThe gate compares pure-Python decompilation to the optimized path and fails unless:
- both runs validate (
validation=passed) - no functions are lost
- no quality metrics regress
The core pipeline is:
IR -> Alias -> Widening -> Types -> Structuring -> Rewrite
The frontend loads DOS real-mode programs through the in-tree x86-16 angr platform, loader, lifter, SimOS, and sidecar parsers. Recovery tries to prove storage identity first, then widen split byte/word values, recover typed memory and stack objects, structure control flow, and finally perform cleanup-only C rewrites.
Generated C targets either portable flat helpers or MS C DOS helpers. Segmented memory is represented through helpers such as SEG_U8, SEG_U16, SEG_PTR, and MK_FP; raw (seg << 4) + off arithmetic is an internal execution detail, not normal output.
Tail validation compares semantic effects after late-stage cleanup. Validation failures are reported as failures rather than silently converted into prettier C.
z3func.py ssa lifts bounded VEX or AIL regions into compact SSA. The comparator observes ABI-selected registers, stack effects, memory stores, direct control-flow successors, direct calls, and optional composed acyclic regions.
compare-ssa first applies cheap equivalence checks such as byte-identical code, compact-SSA equality, mapped direct-call targets, and proven region facts. It then asks Z3 for a counterexample where observable outputs differ. Unsupported helpers, hard guarded memory, oversized slices, symbolic loop paths beyond the bound, and unknown call effects become structured refusals.
compare-ssa-batched runs the same proof in child processes with full SSA indexes available for call-target lookup, keeping whole-program comparisons memory-bounded.
Function signature evidence comes from callsite materialization, stack-push recovery, sidecar/debug metadata, known helper/runtime models, library signature matches, and ABI manifests. The decompiler uses that evidence to distinguish near/far calls, stack arguments, register returns, preserved/clobbered registers, and pointer-vs-value argument classes.
When exact signatures are not proven, output should keep an honest fallback prototype instead of filling guessed arguments or rewriting call bodies late.
The signature pipeline accepts existing FLAIR .pat files and Microsoft OMF .obj / .lib inputs.
For OMF inputs, the parser extracts module blobs, public names, fixup references, segment bytes, module lengths, tail bytes, source path, and compiler provenance. Each module is converted to PAT-style pattern data, then tools/signatures/signature_catalog.py deduplicates modules by pattern bytes, length, public names, referenced names, and tail bytes.
At match time, the loaded binary image is scanned with either the portable Python regex backend or Hyperscan when available. Matches become code labels, code ranges, library-function classifications, and probable compiler names. Unsupported archive formats are detected defensively and skipped instead of crashing.
Contributor and agent-specific rules are intentionally not in this README. See AGENTS.md, reference/agent-rules.md, reference/decompiler-fix-plan.md, and the other files under reference/.
Original Inertia Decompiler code and documentation are available under either:
- the GNU Affero General Public License v3.0 or later; or
- a separate written commercial license.
The AGPL permits commercial use. The separate commercial license is for users who need additional proprietary-use rights or separately agreed support terms.
Third-party components, dependencies, test binaries, corpora, research material, and other separately licensed content are not automatically covered by either Inertia license. Review THIRD_PARTY_NOTICES.md before redistribution.
Contributions require the Inertia Decompiler Contributor License Agreement so the project can preserve both licensing options.