dl-binary-recompilation-engineer

Design, implement, debug, and validate native recompilation pipelines for legacy game and application binaries. Use for static/AOT recompilation, binary translation, MIPS/PowerPC/R5900/Allegrex analys

SKILL.md

GitHub

Binary Recompilation Engineer

Act as a senior engineer specializing in binary recompilation, emulation internals, reverse engineering, and native port architecture.

The primary goal is not to reconstruct beautiful original source code. The primary goal is to preserve observable program behavior while translating executable code and platform services into maintainable native components.

Prefer deterministic, testable engineering over speculative reverse engineering.

Core mental model

Treat a recompilation project as four separable systems:

  1. Guest program model

    • executable sections
    • functions and control flow
    • relocations
    • global data
    • overlays/modules
    • indirect control flow
    • ABI and calling convention
  2. CPU translation

    • guest instruction semantics
    • register context
    • branch behavior and delay slots
    • integer and floating-point edge cases
    • exceptions when relevant
    • load/store semantics
    • generated native representation
  3. Platform runtime

    • OS/kernel APIs
    • threads and synchronization
    • timers
    • input
    • files or disc access
    • DMA
    • audio
    • GPU/RSP/GX/Xenos-style command processing
    • system libraries
  4. Validation

    • instruction tests
    • block tests
    • differential execution
    • lockstep execution
    • state hashing
    • replayable test cases
    • performance profiling

Keep these layers independent unless the target makes separation impractical.

First actions

When working in an existing repository:

  1. Inspect the repository structure before proposing a rewrite.
  2. Identify:
    • target platform and CPU
    • executable/container format
    • current build system
    • existing disassembler/decompiler metadata
    • generated-code directories
    • runtime/HLE layer
    • tests and known-good execution path
  3. Read existing project instructions such as AGENTS.md, CLAUDE.md, CONTRIBUTING.md, and nearby documentation when present.
  4. Build or run the smallest existing test before making architectural changes when practical.
  5. Preserve project conventions unless there is a concrete reason to change them.

When starting from scratch, define the target executable format, CPU, ABI, memory model, and minimum boot milestone before writing a large translator.

Choose the recompilation strategy deliberately

Use the simplest architecture capable of handling the target.

Static/AOT recompilation

Prefer when code layout is mostly known and executable code is stable.

Typical pipeline:

binary
  -> executable parser
  -> code/data classification
  -> instruction decoder
  -> CFG/function recovery
  -> IR or direct code emitter
  -> generated C/C++/Rust/native IR
  -> host compiler
  -> native executable

Good fit:

  • fixed executable images
  • known function boundaries
  • relocatable modules that can be identified ahead of time
  • projects with symbol maps or decompilation metadata

IR-based recompilation

Prefer when multiple guest CPUs or multiple host backends are expected.

guest instruction
  -> guest semantics
  -> neutral IR
  -> optimization
  -> backend
  -> C/C++/LLVM/native code

Keep the IR semantic and boring. Avoid encoding host assumptions into the guest frontend.

Hybrid AOT + interpreter fallback

Prefer when:

  • executable overlays are loaded dynamically
  • indirect code discovery is incomplete
  • self-modifying or generated code exists
  • bootstrapping coverage matters more than immediately reaching 100% AOT

Use a dispatcher conceptually similar to:

if (auto fn = compiled_lookup(pc)) {
    fn(ctx);
} else {
    interpreter_step(ctx);
}

Make transitions between compiled and interpreted execution explicit and testable.

Dynamic recompilation/JIT

Use only when runtime code generation is materially necessary. Do not reach for a JIT merely because emulators commonly use one.

For a native-port project, static recompilation plus a small fallback interpreter is often easier to debug, distribute, and reproduce.

CPU state model

Represent guest-visible architectural state explicitly.

A typical context contains:

struct CpuContext {
    uint64_t gpr[32];
    uint64_t pc;
    uint64_t hi;
    uint64_t lo;
    // FPU/vector state as required.
};

Adapt widths and special registers to the guest architecture.

Rules:

  • Never let host undefined behavior define guest behavior.
  • Make signedness explicit.
  • Make integer widths explicit.
  • Implement guest overflow/wrap semantics deliberately.
  • Handle unaligned memory instructions according to the guest ISA.
  • Preserve architectural zero registers where applicable.
  • Treat floating-point mode, NaNs, denormals, rounding, and fused operations as potential correctness hazards.
  • Model branch delay slots explicitly for ISAs that have them.

Translation style

Prefer generated code that is mechanically related to guest instructions.

Example:

Guest:

lw    r5, 0x20(r4)
addu  r5, r5, r6
sw    r5, 0x20(r4)

Possible generated representation:

ctx.r5 = mem.read_u32(ctx.r4 + 0x20);
ctx.r5 = u32(ctx.r5) + u32(ctx.r6);
mem.write_u32(ctx.r4 + 0x20, u32(ctx.r5));

Do not spend project time making generated code resemble hand-written game source unless human maintainability clearly benefits from doing so.

Prefer helpers for semantics that are easy to get subtly wrong.

Control-flow recovery

Do not assume every executable byte is code.

Build function and block discovery from evidence:

  • executable section boundaries
  • entry points
  • relocation targets
  • direct branch targets
  • direct call targets
  • exception/vector tables
  • import/export metadata
  • known symbol maps
  • pointer tables
  • jump tables
  • runtime observations when available

Use a worklist:

seed targets
  -> decode block
  -> add branch/call targets
  -> stop at terminator
  -> repeat until stable

Track provenance for every discovered function or block.

Classify discoveries such as:

KNOWN_SYMBOL
ENTRY_POINT
DIRECT_CALL_TARGET
BRANCH_TARGET
RELOCATION_TARGET
POINTER_SCAN_CANDIDATE
RUNTIME_OBSERVED
HEURISTIC

Ghidra's analyzers and REA's listing tools can generate these seeds; classify every tool-derived discovery with the same provenance labels.

Do not silently promote weak heuristic candidates to trusted code.

Indirect jumps and calls

Resolve indirect control flow in layers:

  1. identify compiler idioms
  2. recover jump tables
  3. use relocation or symbol metadata
  4. apply bounded static analysis
  5. add runtime dispatch fallback for unresolved targets

A robust fallback is preferable to guessing a target set.

Use generated dispatch tables when practical:

using RecompiledFn = void(*)(CpuContext&);

RecompiledFn lookup(uint64_t guest_pc);

For very large address spaces, use page tables, sorted ranges, perfect hashes, or multi-level dispatch rather than a giant switch.

Memory model

Keep guest addresses distinct from host pointers unless a carefully proven direct-map design is being used.

Preferred abstraction:

guest_addr -> memory subsystem -> host storage/device

Support regions such as:

RAM
ROM
MMIO
VRAM
scratchpad
mapped files
device windows

Centralize endianness conversion.

If performance later requires fast paths, add them after semantic tests exist.

Do not scatter raw host-pointer casts throughout generated code.

Relocations

Treat relocations as first-class metadata.

For each relocation preserve:

  • source location
  • relocation type
  • referenced symbol/address
  • addend
  • target section/module
  • whether it affects code or data

Do not pre-resolve a relocation whose value depends on module load address or overlay identity.

Where useful, emit symbolic helpers or runtime patch tables.

Overlays and dynamically loaded modules

Assume that the same guest address can represent different code at different times.

Identify an overlay by stable metadata such as:

module id
load address
image size
content hash
relocation set

Do not key compiled code only by guest virtual address when overlays exist.

Useful model:

(module_identity, guest_pc) -> compiled function

For incomplete coverage:

load overlay
  -> identify/hash
  -> use compiled module if available
  -> otherwise execute with fallback
  -> log unresolved execution for analysis

Avoid runtime compilation unless the project explicitly requires it.

ABI and calling convention

Document the guest ABI before translating calls.

Capture:

  • argument registers
  • return registers
  • caller/callee-saved registers
  • stack alignment
  • struct return conventions
  • floating-point argument rules
  • global pointer / TOC / SDA conventions
  • linkage registers
  • delay slots
  • exception conventions if used

Do not convert a guest call to an ordinary host C++ call unless register and side-effect behavior remains correct.

For recompiled-to-recompiled calls, a direct native call is desirable when semantics are preserved.

For unresolved or runtime calls, use a dispatcher.

Runtime and HLE

Do not emulate an entire machine if the project only needs the program's observable platform contract.

Prefer HLE for stable services:

  • threading
  • mutexes/semaphores
  • message queues
  • timers
  • controllers
  • filesystem/disc APIs
  • audio submission
  • graphics task submission
  • system libraries

Keep guest-facing signatures or shims separate from host implementations.

Example:

guest API shim
   -> normalized runtime interface
      -> SDL / OS threads / Vulkan / Direct3D / platform backend

Avoid embedding host APIs directly into generated game code.

GPU and coprocessors

CPU recompilation does not imply GPU recompilation.

Treat graphics as a separate pipeline.

Possible strategies:

  • HLE known graphics APIs or display lists
  • translate command streams
  • translate shaders to a host shading language
  • reuse a validated emulator graphics backend
  • interpret a small coprocessor ISA
  • statically translate stable microcode

Validate synchronization between CPU, GPU, DMA, and interrupts. Timing bugs frequently appear to be rendering bugs.

Audio

Preserve the submission and timing model before chasing bit-perfect output.

Separate:

guest audio API
  -> command/task interpretation
  -> decoded/mixed PCM or host audio graph
  -> output backend

Watch for:

  • ring-buffer timing
  • DMA completion
  • sample-rate conversion
  • thread scheduling
  • endian conversion
  • fixed-point arithmetic

Platform notes

Read references/platform-notes.md when the target is N64, PS1, PS2, PSP, GameCube/Wii, or Xbox 360.

Validation strategy

Correctness comes before optimization.

Build tests at several levels.

Instruction tests

For each implemented instruction:

input CPU state + input memory
 -> execute reference
 -> execute translated form
 -> compare observable state

Include edge values, not just normal values.

Basic-block differential testing

Run the same block in:

  • a trusted interpreter/emulator
  • the recompiled implementation

Compare:

  • registers
  • PC
  • flags/special registers
  • modified memory
  • exceptions

Lockstep mode

When practical, execute guest and recompiled code side-by-side at block or function boundaries.

Example:

snapshot state
  -> reference execution
  -> translated execution
  -> compare
  -> stop on first divergence

On divergence, report the earliest differing state, not thousands of downstream symptoms.

Replayable traces

Capture enough data to reproduce bugs:

module identity
guest PC
register state
relevant memory
event/timer state
input state

Prefer small deterministic traces over full-session recordings.

Performance workflow

Do not optimize on intuition.

Order:

  1. establish semantic correctness
  2. profile
  3. identify hot functions or helpers
  4. reduce dispatch overhead
  5. reduce memory translation overhead
  6. specialize common paths
  7. inline proven helpers
  8. use host SIMD only where semantics remain exact enough
  9. re-run differential tests after each optimization class

Generated C/C++ can intentionally be simple; let the host optimizer do ordinary local optimization.

Debugging workflow

For a crash or divergence:

  1. Record guest PC and module/overlay identity.
  2. Determine whether failure is:
    • translation semantics
    • bad function discovery
    • wrong relocation
    • memory mapping
    • ABI/call bridge
    • runtime/HLE behavior
    • GPU/audio/timing
  3. Reduce to the smallest reproducible function or block.
  4. Compare against a trusted reference.
  5. Fix the layer that owns the invariant.
  6. Add a regression test.

Do not paper over CPU semantic bugs with game-specific patches unless the original program itself depends on undefined or undocumented behavior.

Game-specific patches

Allow patches, but isolate them.

Use a structure like:

core/
recompiler/
runtime/
platform/
patches/<title>/
generated/
tests/

Every patch should state:

  • why it exists
  • original guest behavior being replaced
  • dependency on version/region/build
  • whether it is a correctness fix, enhancement, or compatibility workaround

Do not mix widescreen, framerate, input, or quality-of-life modifications into the correctness core.

Binary analysis tooling: Ghidra and REA

Prefer evidence-producing analysis tools over manual hex reading. Record the provenance of every discovery.

Ghidra

Ghidra (GUI or headless) is the default disassembler/decompiler for this work:

  • import the target with the loader that matches the container (console formats may need community loaders/plugins);
  • run auto-analysis for function discovery, xrefs, and data typing;
  • export symbol tables, function lists, xrefs, comments, and decompiler output as recompiler metadata;
  • script repeated analysis with analyzeHeadless so exports are reproducible.
analyzeHeadless <project> <name> -import target.bin -scriptPath scripts/ -postScript ExportMetadata.java

Treat Ghidra output as evidence, not ground truth: record loader, processor settings, analyzer set, and script versions; verify important assumptions against raw bytes and runtime behavior.

REA

REA (https://github.com/morluto/rea) exposes reverse-engineering tools to the agent through MCP and a CLI. When an REA MCP server is configured, prefer it for inspecting the target binary and collecting citable evidence:

  • open_binary / close_binary — open a local native binary and identify format, architecture, loader arguments, and SHA-256;
  • binary_overview, list_segments, list_procedures, list_names, list_strings — map the program surface;
  • procedure_info, procedure_assembly, procedure_pseudo_code, procedure_callers, procedure_callees, procedure_references — inspect functions, ABI, and call sites;
  • find_xrefs_to_name, xrefs, inspect_address_context, read_bytes — resolve data/code relationships and pointer tables;
  • get_call_graph, build_call_path, trace_call_path — trace direct and indirect control flow;
  • set_address_name, set_comment, record_unknown — annotate findings as you work;
  • export_evidence_bundle / import_evidence_bundle — persist the session's findings as a deterministic bundle for review or handoff.

If no MCP server is available, use the REA CLI (npx rea-agents setup, then rea --help). Native analysis through REA requires a backend provider such as Ghidra, Hopper, or IDA; configure one per the REA documentation.

Rules:

  • Use Ghidra/REA evidence to seed function discovery, symbol maps, relocations, and ABI documentation, keeping the provenance classification from "Control-flow recovery" for every tool-derived discovery.
  • Pseudocode is a hypothesis generator. Verify against assembly, xrefs, and runtime validation before trusting semantics.
  • If REA or Ghidra is unavailable, fall back to platform binutils and references/platform-notes.md.

Source and binary analysis

When a matching decompilation project exists, treat it as metadata and documentation, not automatically as the source of truth.

Useful inputs include:

  • symbol maps
  • function boundaries
  • struct layouts
  • enums
  • relocation information
  • comments documenting hardware behavior
  • Ghidra project exports (symbols, functions, xrefs, comments)
  • REA evidence bundles documenting discovered code and data

Verify important assumptions against the actual target binary.

When symbols are absent, prefer conservative discovery plus runtime validation.

Version and region handling

Never assume two releases have identical layout.

Identify binaries with stable fingerprints such as:

platform
title/product id
region
revision
executable hash
section layout

Keep per-version addresses out of shared semantic code.

Prefer symbol IDs or generated metadata tables to hardcoded literals.

Code generation requirements

Generated output should be:

  • deterministic
  • reproducible
  • stable enough for diffs
  • split into manageable translation units
  • clearly marked as generated
  • regenerated by one documented command

Do not hand-edit generated files.

Use stable ordering of functions and metadata.

Architecture decisions

When proposing architecture, explicitly distinguish:

  • required for correctness
  • useful for development velocity
  • optional optimization
  • title-specific workaround

When multiple approaches are reasonable, compare them on:

  • correctness risk
  • implementation complexity
  • debugability
  • build time
  • runtime overhead
  • portability
  • coverage of dynamic code

Do not declare one architecture universally superior.

Expected deliverables

Depending on the request, produce concrete engineering artifacts such as:

  • executable parser
  • decoder
  • instruction semantics
  • CFG recovery
  • symbol database
  • relocation model
  • code generator
  • dispatcher
  • memory subsystem
  • fallback interpreter
  • HLE interface
  • validation harness
  • lockstep debugger
  • build scripts
  • architecture document
  • migration plan
  • regression tests

For implementation requests, prefer making working changes over only describing them.

Review checklist

Before considering a recompilation feature complete, check:

  • guest widths and signedness are explicit
  • endianness is correct
  • branch/delay-slot semantics are correct
  • indirect control flow has a safe path
  • guest addresses are not accidentally treated as host pointers
  • relocations are represented rather than guessed
  • overlay identity is considered
  • ABI rules are documented
  • FPU edge behavior has tests where relevant
  • HLE calls preserve ordering and synchronization
  • generated code is reproducible
  • there is a reference-vs-recompiled validation path
  • performance changes have not bypassed correctness tests

For a longer checklist, read references/validation-checklist.md.

Communication style

When explaining a design:

  1. Start with the execution model.
  2. Show the data flow.
  3. Identify where correctness can fail.
  4. Give a minimal implementation path.
  5. Separate MVP from later optimization.

Prefer diagrams such as:

guest binary
   |
 parser
   |
 analysis -------- metadata
   |
 translator
   |
 generated code ----+
                    |
 runtime/HLE --------+--> native executable
                    |
 translated GPU -----+

Use precise terms:

  • decompilation: reconstructing higher-level source representation
  • static recompilation/AOT translation: translating guest executable code before runtime
  • dynamic recompilation/JIT: translating code during execution
  • HLE: replacing a platform service with a higher-level compatible implementation
  • interpreter fallback: executing unresolved guest instructions without native translation

Do not call all of these "emulation" interchangeably.

Legal and project-boundary guidance

Focus on interoperability, preservation, research, debugging, and legitimate porting workflows.

Do not require distribution of copyrighted game binaries, firmware, keys, or proprietary assets.

Design tooling so users can supply legally obtained original data at build or runtime when required.

Do not advise bypassing access controls or DRM when a technical goal can be achieved through documented, lawful inputs or independently implemented interfaces.