A fork of sp00nznet/pcrecomp that extends the toolbox from 16/32-bit PC software to 64-bit Windows: PE32+/AMD64 programs recompiled to native Linux executables, with the Win32 API taken from Wine's own DLLs through winelib rather than reimplemented — and without running the guest program on Wine's loader or emulating a single instruction.
Start here: README-LINUX64.md, then docs/linux64/RECON.md for the program spec, docs/linux64/WINE.md for the Win32 layer, and docs/linux64/ROADMAP.md for what is done and what is next.
The rest of this file documents the upstream toolbox this fork builds on; the 16-bit and 32-bit paths are unchanged.
____ ______ ____ ________________ __ ___ ____
/ __ \/ ____/ / __ \/ ____/ ____/ __ \/ |/ // __ \
/ /_/ / / / /_/ / __/ / / / / / / /|_/ // /_/ /
/ ____/ /___ / _, _/ /___/ /___/ /_/ / / / // ____/
/_/ \____/ /_/ |_/_____/\____/\____/_/ /_//_/
"everything old is new again"
The unified toolbox for tearing apart old PC software and putting it back together, better.
This repo collects every tool, runtime, and hard-won trick from our PC static recompilation projects into one place. Next time we want to crack open a dusty .exe from 1995 and make it run on Windows 11, we start here.
pcrecomp/
tools/ Reusable analysis & transformation tools
pe/ PE analysis (imports, exports, sections, hashes, delay-imports,
protection/DRM detection, recursive binary catalog that
also names the non-PE binaries (NE/LE/MZ) instead of
calling them broken,
stdcall_argc.py derives each import's stack purge from the SDK)
ne/ NE (16-bit New Executable) parse / disasm / call-graph,
Win16 import resolution (ordinal -> API name + the PASCAL
stack-purge table) and C shim generation
disasm/ Disassemblers (32-bit recursive descent, 16-bit table-driven,
x87 FPU decoder, direct call-graph scanner, large-model
far-call + code/data-boundary call-graph completion,
score_recovery.py scores a catalog against a reference and
splits false positives into "split" vs "invented")
lift/ Code lifters (x86-32 and x86-16 to readable C; lift32_cpu.py
is the reentrant CPU-struct model needed for hybrid builds;
recover.py finds alternate entry points a catalog missed;
difftest.py / difftest16.py run the lifted C against Unicorn
and name every register, flag and byte the two disagree on)
classify/ Function classifiers (SDK vs custom, multi-signal, string refs)
ghidra/ Ghidra headless scripts (decompile, export, stats, xrefs,
range disasm, function bounds)
ida/ IDA headless scripts (code-map export, segment probe)
drm/ DRM analysis (SafeDisc memory dumping, DLL injection)
assets/ Asset extraction (InstallShield, Wise, PK3/ZIP, BIN/ISO, CAB)
cpp/ C++ RE helpers (MSVC/MWerks name mangling, vtable parsing)
formats/ Format decoders (FIF fractal images, M20/MVB, SPAM, DAT,
string tables)
runtime/ Drop-in runtime support for recompiled code
recomp32/ 32-bit x86 runtime (global registers, memory model, dispatch,
image loader, crash reporter)
recomp32_cpu/ 32-bit x86 runtime, explicit CPU struct (reentrant; pairs
with lift32_cpu.py, required for hybrid builds)
recomp16/ 16-bit DOS runtime (CPU state, INT handlers, HAL, SDL2)
compat/ Win32 API compatibility layers (Win32 -> SDL2 mapping)
hybrid/ The lifted <-> real boundary: import trampoline, real->lifted
__thiscall trampoline, vtable routing. Lets a framework
(MFC, the CRT) stay real while the app body runs recompiled
templates/ Starter files for new projects (CMake, .gitignore)
docs/ Deep dives and philosophy
Every tool here was forged in the fires of an actual recompilation project. These are the PC games and apps we've taken apart so far. Statuses are what the project's own README claims; the numbers are function counts from its last pipeline run.
| Project | What | Era | Engine/Tech | Status |
|---|---|---|---|---|
| civ | Civilization | 1991 | 16-bit DOS / MSC 5.x | Runs! 672 functions, interactive boot/menu, 164K lines |
| operationneptune | Operation Neptune | 1991 / Win32 1998 | Borland PE32, ships its own linker map | Plays! CRT -> WinMain -> opening -> in the submarine |
| skifree | SkiFree | 1991 | Win16/Win32 (ski32.exe) |
Playable rebuild from decompiled C, cross-platform + extras |
| dinopark | DinoPark Tycoon | 1993 | 16-bit DOS / Borland large model | Boots! Whole game lifted (~90K lines), renders .PIC screens + .ACT dinosaurs in colour |
| elfish | El-Fish | 1993 | 16-bit NE + TSXLIB extender | Lifted & links - 2,236 functions, 121 segments, startup executes |
| hellbender | Hellbender | 1996 | Terminal Reality voxel engine (Win32/MSVC) | Bring-up - lifts clean (5,262 functions, 0 errors), 507 import bridges; same toolchain as Fury³ |
| fury3 | Fury³ | 1995 | Terminal Reality voxel engine (Win32/MSVC) | Playable! Flies the canyon - 1,945 functions, SDL2+imgui frontend, real joystick |
| catz | Catz | 1996 | 16-bit NE engine DLL (PF Magic) | Runs! Win32 window, original frame loop, toys and saving work |
| encarta | Encarta 97 Encyclopedia | 1996 | MFC 4.0 + proprietary | Runs! Whole app lifted (7,326 fns); hybrid boundary puts the app body in recompiled code - 10,242 real MFC virtual dispatches land lifted per session |
| gta | Grand Theft Auto | 1997 | DMA "Race'n'Chase" | Builds & runs - 4,094 functions, 444K lines, runtime bringup |
| pod | POD Gold | 1997 | Ubi Soft MMX software rasteriser | Compiles & links - 3,405 functions, 0 lift errors, 422K lines |
| ejay | Dance eJay 1 + 2 | 1997 | PXD Musicsoft audio engine behind a VB front end | Plays and draws! Decodes its intro, streams to a real sound card at 22,050 Hz and animates its cursor - out of recompiled 16-bit code |
| fallout1-re | Fallout | 1997 | Custom (Interplay) | Fork - native + HTML5 web port, multiplayer |
| fallout2-re | Fallout 2 | 1998 | Custom (Interplay) | Fork - decompilation ~complete (alexbatalov upstream) |
| trespasser | Jurassic Park: Trespasser | 1998 | DreamWorks Interactive rigid-body engine (MSVC 6.0) | P0 - reconnaissance. Ships a linker map, which makes it the calibration target for function recovery |
| nocturne | Nocturne | 1999 | Terminal Reality, Watcom C/C++32 | Phase 7 - 6,027 functions lift with 0 errors; real window, 42 MB image mapped, IAT dispatch, 95 of 171 imports live |
| xwa | X-Wing Alliance | 1999 | Custom (LucasArts) | Active - D3D11 port, concourse UI runs, 2,702 functions |
| sof | Soldier of Fortune | 2000 | Quake II + GHOUL | Active - SDL2 port, 8 subsystems, full maps render |
| gunman | Gunman Chronicles | 2000 | GoldSrc (Half-Life) | Phase 2 - 3,990 functions, weapons/entities rebuilt |
| heavymetal | Heavy Metal: FAKK2 | 2000 | id Tech 3 + UberTools | Foundation - 57 source files, core systems scaffolded |
| crimsonskies | Crimson Skies | 2000 | Zipper GOS engine | Compiles & links - 6,232 functions, 826K lines, runtime bringup |
| bw | Black & White | 2001 | Lionhead custom | Active - all 569 types done, 10 Hz game loop runs |
| rol | Rise of Nations: Rise of Legends | 2006 | Big Huge Games rts2 (MSVC 7.1) | Phase 3 - the largest binary this toolchain has faced: 13.25 MB, 25,513 vtable-only functions, no RTTI |
Every row above links to a repo that is actually there. See docs/PROJECTS.md for what each one taught the toolbox, including the three fixes One Must Fall: Battlegrounds forced before its disc would even open.
Also not public -- same toolbox, repos still private, listed because the
tools here carry their scars: bolo (Bolo Adventures III, 1993 -- shipped
tools/unpklite.py, a byte-exact static PKLITE 1.15 decompressor),
coaster (Roller Coaster Construction Set, 1993 -- ~560 functions, boots),
bob (Microsoft Bob, 1995 -- Win16 NE + Jet/WinG, InitInstance runs),
mw3 (MechWarrior 3, 1999 -- 2,805 functions), recoil (1999 -- 3,490
functions) and xvt (X-Wing vs TIE Fighter, 1997).
The first three are held back by their own generated code rather than by
progress: lifted C is a derivative work of the binary it came from, and every
public repo here tracks none of it. See
docs/PUBLISHING.md and tools/audit_repo.py.
Still at P0, so still private: missileattack (Missile Attack!, 1992), tim (The Even More Incredible Machine, 1993), msbus (Magic School Bus: Human Body, 1994), tv (Terminal Velocity, 1995), mtm (Monster Truck Madness 1+2, 1996 / 1998), forcecommander (Star Wars: Force Commander, 2000), omfbg (One Must Fall: Battlegrounds, 2003) and bw2 (Black & White 2, 2005). Each is in docs/PROJECTS.md with what it cost the toolbox.
Same idea, different instruction set or platform. They are separate repos, not submodules:
- xboxrecomp -- original Xbox (XBE), and therefore also x86-32. It is the closest relative this repo has, and the two have been converging: see docs/CONSOLIDATION.md for what has already been ported across and what is queued next.
- macrecomp -- 68k Macintosh, A-trap dispatch and a QuickDraw/Toolbox HAL.
# What are we dealing with? (prints a summary; --json names the output file)
python tools/pe/pe_analyze.py mystery.exe --json analysis.json
# What DLLs does it import? (including delay-loaded ones). Point
# extract_imports at the whole install folder to get the shared-API view
# across every module at once -- those are the shims to write first.
python tools/pe/extract_imports.py mystery.exe
python tools/pe/extract_imports.py /path/to/install
python tools/pe/delay_imports.py mystery.exe
# Is it packed or copy-protected? (SafeDisc/SecuROM/UPX/...)
python tools/pe/analyze_sections.py mystery.exe
# Catalog every binary in the install folder at once
python tools/pe/catalog.py /path/to/install --json > catalog.json
# Got Ghidra? Decompile everything in one shot
# (run in Ghidra's headless analyzer)
analyzeHeadless /path/to/project MyProject -import mystery.exe \
-postScript tools/ghidra/DecompileAll.java output.c# Full automated pipeline: analyze -> disassemble -> lift -> compile
python -m tools game.exe --all --output src/recomp/gen/
# Or step by step:
python tools/pe/pe_analyze.py game.exe --json config/pe_analysis.json
python tools/disasm/disasm32.py game.exe --output functions.json --pe-json config/pe_analysis.json
# `python -m tools` is tools/lift/translator.py, and that is the only lifter
# CLI. lift32.py itself is a library: `from lift32 import Lifter`, one
# `Lifter` per function. Every project past the default pipeline drives it
# from its own run_lift.py -- that is where per-project relocation handling,
# import bridging and file splitting belong, not in a flag.# Disassemble: the whole resident image, one overlay, or a byte range
python tools/disasm/decode16.py GAME.EXE --resident
python tools/disasm/decode16.py GAME.EXE --overlay 3
python tools/disasm/decode16.py GAME.EXE 0x1200 0x400
# Find function boundaries (MSC 5.x patterns) and write the symbol table
python tools/disasm/analyze.py GAME.EXE -symbols work/symbols.toml
# Large model (Borland/MSC): `largemodel16` is a library that extends the
# analyzer in place -- detect_code_end() clips the DGROUP data blob off the
# code, build_call_graph() resolves the far calls analyze.py leaves dangling.
# from largemodel16 import detect_code_end, build_call_graph
#
# Lifting is a library too, same as the 32-bit side: `from lift16 import Lifter`.
# Copy a project's driver to start -- dinopark/tools/lift_full.py (DOS MZ,
# large model) or elfish/tools/ne_lift.py (NE, segmented).# Run the lifted C and a real x86 (Unicorn) over the same bytes and compare
# every register, flag and byte of memory. Needs unicorn + a C compiler.
python tools/lift/difftest.py -v
# The 16-bit CPU model's hand-written flag logic (BCD, ADC/SBB carry-in),
# against values taken from hardware
cc -Iruntime/recomp16 runtime/recomp16/cpu_selftest.c -o selftest && ./selftest# Score a recovered catalog against a reference -- a linker map, a PDB export,
# or another tool's analysis. Reports precision/recall on function starts.
python tools/disasm/score_recovery.py --reference ida_funcs.json \
--candidate functions.json
# The reference can come from IDA, headless, in about a minute:
py -3.11 tools/ida/ida_funcs.py GAME.EXE ida_funcs.jsonA false positive is two different defects wearing one name, so when the reference carries function ranges (IDA, a PDB) the score separates them: split means the address landed inside a known function, so one function got entered twice -- it duplicates code in a lift. invented means it landed outside every known function, so data was probably decoded as code -- that lifts to garbage. They need opposite fixes.
A reference is only ground truth if it came from symbols. Otherwise it is a second opinion, and a disagreement means one of the two is wrong -- go look at which before quoting the number.
# Structure: segments, relocations, imports, entry points
python tools/ne/ne_parse.py GAME.EXE
# NE-aware disassembly (resolves cross-segment far calls + imports)
python tools/ne/ne_decode.py GAME.EXE --summary
python tools/ne/ne_decode.py GAME.EXE --seg 3
# Segment call graph / clusters / import usage
python tools/ne/ne_xref.py GAME.EXE --clusters
python tools/ne/ne_xref.py GAME.EXE --imports
# The import surface as C: prototypes + a correctly-purging stub for every
# import with no hand-written shim yet
python tools/ne/gen_win16_stubs.py GAME.DLL --api runtime/runtime_api.h --stubs runtime/win16/win16_stubs.c --shims runtime/win16 --guard MYGAMEWin16 is PASCAL, and that is the thing that bites. The callee pops the
arguments, so a shim that pops the wrong number does not fail at the call - it
shifts the caller's frame, and the caller's epilogue then restores DS (or BP,
or a return address) from the wrong slot. The crash lands somewhere else
entirely, in code that is fine. tools/ne/win16.py carries the accumulated
purge table keyed by (MODULE, API); gen_win16_stubs.py fails rather than
generating a stub for an import that has no entry in it.
Names come from IDA, which ships the Win16 ordinal maps. Export them once with
tools/ida/ida_export.py-style extraction into work/win16_imports.json and
win16.py finds it by walking up from the project root; without it, imports
resolve to MODULE_OrdN and the purge lookups all miss.
# Confirm it statically first (entry point inside a high-entropy section?)
python tools/pe/analyze_sections.py game.exe
# Dump decrypted code from a running process (Steam/CD version)
python tools/drm/safedisc_dump.py game.exe decrypted.exe
python tools/drm/safedisc_dump.py --pid 1234 game.exe decrypted.exe# Find the overlay, inflate the install script, list embedded files
python tools/assets/extract_wise.py setup.exe out_dir/# Direct callers (no Ghidra/IDA needed) + most-referenced functions
python tools/disasm/callgraph.py game.exe --callers 0x401D10
python tools/disasm/callgraph.py game.exe --hot 25
# Function-level graph + leaf detection (pair with DumpBounds.java output)
analyzeHeadless proj P -process game.exe -postScript tools/ghidra/DumpBounds.java bounds.csv
python tools/disasm/callgraph.py game.exe --bounds bounds.csv --leaves# Export IDA's verified code map (functions + instruction heads), then feed it in
py -3 tools/ida/ida_export.py GAME.EXE code_map.json --key ne
python tools/ne/ne_decode.py GAME.EXE --ida-json code_map.jsonPython 3.10+ with:
capstone- disassembly engine. Required.pefile- PE parsing. Optional; there is a pure-struct fallback.lief- advanced binary analysis. Optional.unicorn- the reference x86 thatlift/difftest.pychecks the lifted C against. Only needed to run the differential tests.
For Ghidra scripts: Ghidra 11.0+
For IDA scripts: IDA 7.4+ with its bundled Python (ida_funcs.py and
ida_export.py run headless under idat -A -S).
For format tools: C compiler (MSVC or GCC)
For runtime: CMake 3.20+, Visual Studio 2022 or compatible
pip install capstone pefile lief unicorn- Copy
templates/CMakeLists.txt.templateandtemplates/.gitignore.template - Run
pe_analyze.pyon your target binary - Pick your pipeline:
- 32-bit PE:
python -m tools game.exe --all(disasm32->lift32, driven bylift/translator.py). Write your ownrun_lift.pywhen the defaults stop fitting. - 16-bit DOS:
decode16->analyze->lift16(with DOS compat runtime) - 16-bit Windows/OS-2 (NE):
ne/ne_parse->ne/ne_decode->lift16(seetools/ne/README.md) - GoldSrc/SDK game:
DecompileAll.java->combined_classify.py(SDK separation) - C++ heavy:
GhidraStats.java+msvc_mangler.py+parse_vtables.js
- 32-bit PE:
- Score the recovery before you lift it.
disasm/score_recovery.pyagainst a linker map, a PDB or IDA. Lifting a catalog you have not scored means finding its gaps at runtime, 30,000 calls deep, instead of now. - Drop in the appropriate
runtime/files - Build with CMake, fix, repeat
| Doc | What it covers |
|---|---|
| docs/PHILOSOPHY.md | Why static recompilation, and the approach end to end |
| docs/PIPELINE.md | Each phase, which tool, what it emits |
| docs/HYBRID.md | Running lifted code inside a real program - the lifted/real boundary, its three non-obvious correctness rules, and how to bisect a hybrid build when it breaks 30,000 calls deep |
| docs/PROJECTS.md | Which project contributed which tool, and why it exists |
| docs/CONSOLIDATION.md | What pcrecomp and xboxrecomp should share, what has been ported, what is queued |
Any PC application ever compiled can be systematically deconstructed and rebuilt for modern hardware. It's not magic, it's just work -- and with the right tools, it's less work every time.
We've proven this across DOS, Win16, Win32, MFC, Quake-family engines, GoldSrc, id Tech 3, and completely custom engines. The pattern is always the same: Analyze -> Disassemble -> Classify -> Lift -> Shim -> Build -> Debug -> Ship.
Read the full philosophy in docs/PHILOSOPHY.md.
Built on other people's tools, and on published reverse-engineering work:
- capstone (BSD-3-Clause) — the disassembly engine under every lifter here.
- pefile (MIT) — PE parsing,
including the
.relocwalk the 32-bit lifters rely on. - Ghidra (Apache-2.0) and IDA Pro — hosts for
the headless scripts in
tools/ghidra/andtools/ida/; function boundaries and decompilation. - Kostya Shishkov and Alyssa Milburn — their published work on the
FVF/IFS fractal codec family underpins
tools/formats/ftcdecode/.
All of the above are used as libraries or hosts under their own licences. None of them are vendored here.
MIT — see LICENSE. Use these tools to bring back whatever software you love.
One thing the licence cannot give you: rights to the software you point these tools at. Lifting a binary produces a derivative work of that binary, so the output carries whatever licence the original does. Own what you take apart.
Built with stubbornness and too much coffee by sp00nznet