A small universal bytecode decompiler experiment.
For the complete read-only GUI, CLI, simulator, symbolic executor, and all bundled frontend formats, install the meta-package:
python -m pip install --upgrade unidecompiler-all
unidecompiler-guiTo use only the command-line host with selected formats:
python -m pip install --upgrade unidecompiler-cli \
unidecompiler-plugin-python-pyc \
unidecompiler-plugin-jvm-class
unidecompiler --helpYou do not need to clone this repository or build from source for normal use. The packages require Python 3.11 or newer. Clone the repository only for development, testing, or creating a custom VM frontend.
The project is built around one hard architectural rule: VM frontends are thin submitters, and the core owns recovery.
unidecompiler is designed for authorized analysis of VM-SDK and bytecode virtualization protection. Its goal is to provide one frontend-neutral recovery pipeline for proprietary or custom virtual machines, including SDK-protected applications where bytecode is the only practical analysis surface.
The Python, JVM, Lua, .NET CLI, and WebAssembly frontends in this repository are reference implementations and regression coverage. They demonstrate the thin-IR contract; they are not the boundary of the project. A new VM frontend should decode its format and submit neutral bytecode facts while the core owns control-flow recovery, AST construction, diagnostics, and rendering.
Use the project only for software you own or are authorized to analyze.
For normal use, install released packages with pip. You do not need to clone
this repository or build from source. Clone the repository only when developing,
testing, or contributing to unidecompiler.
For development rules and agent instructions, see AGENTS.md.
At a high level, the pipeline is a semantics-preserving fixed point in core:
VM bytecode -> thin IR -> generic IR / low-level CFG
|
v
CFG structuring (if/while/branch)
|
v
structured FunctionIR refinement
|
+-------------+-------------+
| |
changed stable
| |
+--> CFG analysis/structuring ↺
|
v
final AST -> pseudocodeThe refinement loop is owned entirely by core. Each accepted rewrite is
validated against the original control-flow and safety invariants before core
re-enters CFG analysis and structuring. If equivalence cannot be proved, the
low-level CFG/goto form is retained. The final FunctionDecl AST is produced
only after this loop reaches a stable result; backends only render that result
and never perform CFG recovery.
CFG recovery is edge-aware. Core keeps concrete incoming and outgoing edges, including parallel edges, with deterministic identities; a reducer must never deduplicate an edge merely because its source, target, or kind matches another edge. Shared CFG analysis snapshots provide dominators, postdominators, frontiers, loop information, and irreducible-entry facts for VM-neutral structuring passes. Phi cleanup and fallthrough-jump removal are accepted only when the exact predecessor edges, exception state, and data-flow values prove the rewrite safe. Otherwise the preservation-floor CFG/goto representation is kept unchanged.
Generic value recovery also preserves observable evaluation order. Stack copies, duplicates, unpacking, calls, and stores retain the value that existed at that bytecode point even when a later local, global, captured value, member, or item is mutated. Store and delete operations are represented explicitly in generic IR instead of being approximated as stack pops. Phi assignments execute as parallel copies, and an ambiguous duplicate predecessor is rejected rather than silently overwritten.
Numeric semantics travel with each generic operation. Frontends may submit
canonical operators or common VM-neutral aliases such as shl, shr, rol,
and ror, together with signed/unsigned/float domain, bit width, and wrapping
or trapping overflow policy. Core rewrites preserve that metadata, and the
optional simulator applies it without branching on a frontend or source
language. Container kind is likewise retained, so tuple and list literals do
not collapse into one representation.
Core also accepts descriptive call-effect summaries. They record reads, writes, return arity, and possible raise/suspend/mutation behavior without executing a callee. Known summaries enable narrowly proven stack-value preservation; unknown calls form conservative barriers for deferred values. Fixed-point pass scheduling and CFG rewrite evidence remain core-owned and diagnosable, including the instruction context available when recovery stops.
Every accepted region rewrite records a proof entry in FunctionIR.metadata
with its rule, concrete block/edge identities, CFG snapshot key, and raw bytecode
context. Rejected candidates record the rule and rejection diagnostics. The
shared CFG validator also checks edge targets, contiguous parallel-edge
ordinals, exception-edge provenance, and entry consistency before a rewrite is
admitted. These records are analysis evidence, not frontend control-flow
instructions; they let a remaining goto be traced to a specific proof
boundary without weakening the preservation fallback.
Hosts may observe decompilation through the optional progress argument on
DecompilerEngine. It emits immutable, frontend-neutral events with separate
current-artifact and batch coordinates. Existing frontends remain compatible;
frontends that can prove finer-grained work units may additionally implement
decode_with_progress() or lift_with_progress(). Progress is observational
only, is never stored in IR or metadata, and observer failures are isolated from
decompilation. The CLI keeps progress disabled by default; bare --progress
enables TTY auto mode and --progress always forces it. The single-line bar is
written only to stderr, so pseudocode and AST JSON on stdout remain
machine-readable. Events may also carry an optional VM-neutral item_label
for the currently processed, frontend-proven function, method, code object, or
other stable work item. Hosts prefer that label and fall back to the event
message when it is absent. A frontend must omit it when no stable name or
proven offset exists; when an offset is proven, a neutral fallback such as
function@<offset> is appropriate. This label is display-only and must never
enter IR, metadata, CFG, AST, or recovery decisions.
File and project exports are host functionality provided by the separate
unidecompiler-export package. It writes recovered pseudocode documents and
generates frontend or GUI-plugin starter projects without adding file-system
I/O or template-generation responsibilities to the core engine.
CLI exports mirror the GUI host behavior: -o/--output writes one result,
--output-dir writes one collision-safe file per successful artifact, and the
template/export-template command generates a VM frontend or GUI-plugin
starter project. Template destinations are created atomically and are never
overwritten. Frontend templates can explicitly enable the optional data-only
simulator adapter and AI development kit; both are off by default. AI input
files are validated for regular-file status, size, and likely credentials
before they are copied into a generated project.
VS Code navigation metadata is opt-in: pair --output with
--vscode-metadata path.unidec.json to write a sidecar after the pseudocode.
It contains a UTF-8 text hash, UTF-16 pseudocode ranges, and minimal instruction
facts only. It does not duplicate pseudocode or export source paths, AST, IR,
CFG, diagnostics, recovery data, or frontend-private objects. Ordinary CLI and
GUI pseudocode exports do not produce sidecars.
unidecompiler template --interactive (short form -i) provides a guided
version of the same template export. It only collects host-side template
settings and does not affect the decompiler engine or recovery pipeline.
unidecompiler is an embeddable core library. It has no command-line entry
point, no bytecode format parser, and no dependency on a concrete frontend.
The repository is a Python package workspace: each distributable component
lives under packages/ and can be installed independently.
-
unidecompiler: generic IR, lifting, analysis, structuring, and backends. -
unidecompiler-cli: optional command-line host. -
unidecompiler-gui: read-only PySide6 workbench. -
unidecompiler-gui-sdk: stable, Qt-neutral API for trusted GUI plugins. -
unidecompiler-export: host-side pseudocode and starter-project exporters. -
unidecompiler-simulator: optional bounded executor for recovered generic IR. -
unidecompiler-symbolic: bounded symbolic executor for recovered generic IR. -
unidecompiler-simulation-host-python: trusted Python runtime host for applications that provide unresolved functions. -
unidecompiler-plugin-*: independently installable frontend adapters. -
unidecompiler-all: complete-installation meta-package.
The CLI and other hosts discover installed adapters through the
unidecompiler.frontends Python entry-point group. An embedding application
can instead create a FrontendRegistry from an explicit plugin collection.
The decompiler is split into three layers:
- External VM frontend plugins parse bytecode formats and submit neutral thin IR.
- Core lifts thin VM steps, effects, hints, regions, CFG-like control flow, and recoverable structures.
- Backends render the recovered generic IR into pseudocode.
The current frontend pipeline is:
- Decode the VM bytecode with the frontend's format decoder.
- Convert each decoded instruction into a
VMBytecodeStep. - Attach neutral operands, opcode classes, hints, and effect-table results.
- Submit the complete step stream through
lift_vm_step_function. - Let core produce full, partial, or unsupported generic IR.
After lifting, core may run a fixed-point recovery refinement pass. It can
conservatively simplify structured FunctionIR expressions and statements,
then re-run generic CFG analysis and structuring whenever a rewrite changes the
recoverable shape. This repeats until no safe rewrite remains. This is a core
phase, not a frontend or backend extension, and it never replaces an unproven
recovery with guessed source structure.
Simulation is an optional consumer of recovered generic IR. It is deliberately decoupled from both core recovery and frontend bytecode execution:
frontend -> core generic IR <- simulator <- CLI / GUI / embedding hostThe core does not depend on the simulator, and the simulator does not execute frontend bytecode, VM opcodes, effect tables, or frontend-private decoded models. A frontend may optionally provide a data-only simulation adapter for function lookup and runtime facts. Language-specific lookup, such as Lua names or JVM class/method names, remains owned by that frontend.
The simulator returns structured results for completion, return values,
exceptions, unsupported operations, limits, cancellation, and execution trace.
See packages/unidecompiler-simulator/README.md for the public library API and
frontend query formats.
The optional symbolic executor follows the same ownership boundary. It receives
only recovered generic IR and opaque frontend target queries through the host;
it never interprets frontend bytecode. The CLI exposes it as unidecompiler symbolic, with JSON symbolic-input declarations and concrete parameter values:
unidecompiler symbolic sample.pyc --function choose \
--symbolic '{"value":{"sort":"int"}}' --concrete '{}'The Workbench's Symbolic tab uses the same opaque target listing and presents bounded path constraints, models, return or raise outcomes, and concrete CFG edge IDs. Unsupported IR, solver uncertainty, limits, and cancellation remain visible structured results.
Supported frontend families follow this model:
- Python
.pyc - JVM
.class - Lua chunks
- .NET CLI assemblies
- WebAssembly modules
-
packages/unidecompiler/: embeddable core package, using a standardsrc/layout. -
packages/unidecompiler-cli/: optional CLI host package. -
packages/unidecompiler-gui/: read-only desktop workbench package. -
packages/unidecompiler-gui-sdk/: versioned data contracts for GUI plugins. -
packages/unidecompiler-export/: host-side pseudocode and starter-project exporters. -
packages/unidecompiler-simulator/: bounded generic IR execution library. -
packages/unidecompiler-symbolic/: bounded symbolic execution over generic IR. -
packages/unidecompiler-simulation-host-python/: trusted Python runtime host shared by applications. -
packages/unidecompiler-plugin-*/: independently installable frontend packages. -
packages/unidecompiler-all/: complete-installation meta-package. -
unidecompiler-emojivm-frontend-case/: complete custom-VM frontend analysis and simulator example. -
unidecompiler-gui-test-plugin/: Qt-free GUI SDK plugin example. -
opcode_projects/source/<project>: source stress projects. -
opcode_projects/generate/<project>: generated stress project outputs. -
simulator_projects/: source fixtures and expected results for generic-IR simulation. -
docs/: supporting design notes.
The stress corpora are local working data and are scanned by path rather than
imported as Python test packages. opcode_projects validates decompiler
recovery; simulator_projects validates execution of recovered IR and the
frontend adapter boundary.
The unidecompiler-all meta-package provides the complete CLI, GUI, GUI plugin
SDK, simulator, symbolic executor, and all frontend plugins with one command:
python -m pip install unidecompiler-allInstall a published command-line setup with the formats you need:
python -m pip install unidecompiler-cli \
unidecompiler-plugin-python-pyc \
unidecompiler-plugin-jvm-classRun unidecompiler --help to see CLI options. Plugins are discovered through
Python entry points, so installing another frontend adds its input formats
without changing the core or host application.
The CLI keeps stdout suitable for pipelines. Use -o/--output for one result
or --output-dir for a collision-safe batch export; destinations and errors
are reported on stderr. Progress is disabled by default, --progress enables
TTY auto mode, and --progress always forces the single-line bar on stderr.
Install unidecompiler-export with the CLI when using host-side exports and
starter projects:
python -m pip install unidecompiler-cli unidecompiler-export
unidecompiler template frontend MyVM -o ./my-vm \
--author "Your Name" --description "A VM frontend" \
--requirements "Decode and lift the VM" --suffix .vm --version 1Add --simulation to include the optional data-only simulator adapter, or
--ai-guidance with explicit interpreter/sample files and entry facts to add
the static-analysis kit. Both options are disabled by default; generated
directories are created atomically and are never overwritten.
The optional simulator command executes a selected function from the recovered generic IR. The frontend chooses how the function query is resolved:
unidecompiler simulate sample.pyc \
--function bubble_sort \
--args '[[5, 1, 4, 2, 8]]'Calls made by the selected function that are not present in the lifted module can be handled by an explicit runtime file:
unidecompiler simulate sample.pyc \
--function bubble_sort \
--args '[[5, 1, 4, 2, 8]]' \
--environment runtime.py \
--show-host-outputThe runtime file is trusted host Python code selected by the user. It is not a sandbox and is loaded by the application-host package, not by core, the simulator, or a frontend. The simulator itself receives only data-only call requests and validated runtime values.
The symbolic command explores multiple feasible paths through a recovered
function. Install unidecompiler-all (or unidecompiler-cli together with
unidecompiler-symbolic) and declare symbolic parameters as a JSON object:
unidecompiler symbolic sample.pyc \
--frontend python-pyc \
--function choose \
--symbolic '{"value":{"sort":"int"}}'--concrete supplies JSON values for parameters that should remain concrete,
while --max-paths, --max-steps, --max-loop-unroll,
--max-call-depth, and --solver-timeout-ms bound exploration resources.
Inputs support bool, int, real, and fixed-width bitvec sorts (the latter
also requires bit_width). Results are JSON by default; add --format text
for a concise report. Each path includes its constraints, model, return or
raise outcome, and concrete CFG block/edge trace. A top-level status of
completed means exploration finished; unsupported, solver_timeout,
*_limit, cancelled, and invalid_request are explicit non-success
outcomes.
For the desktop workbench, install the GUI and all bundled frontend packages:
python -m pip install 'unidecompiler-gui[all-formats]'
unidecompiler-guiThe GUI is read-only. Its decompiler workflow uses the public
DecompilerEngine facade, while its optional Simulation tab uses the separate
public simulator API. Select a recovered artifact to discover targets from the
registered frontend, enter a JSON argument array, optionally choose a trusted
runtime.py, and press Run to inspect the result and execution trace. The GUI
does not implement language-specific target lookup or simulation semantics.
The Symbolic tab uses the same frontend-owned target list. Select a target,
review the discovered parameter sorts, optionally enter a concrete JSON object,
adjust path, step, loop, and solver limits, and press Explore. The result
table shows one row per feasible path; selecting a row reveals constraints, a
model, returns or raises, and the CFG edges taken. The GUI only renders the
public SymbolicResult; it does not access Z3 state, simulator frames,
frontend decoders, or private IR objects.
The GUI also provides a read-only Structure / Hex view. When a frontend can
prove an instruction's exact absolute byte range in the opened artifact, the
core exposes that neutral ByteRange provenance and the GUI highlights the
corresponding bytes. Logical VM offsets are kept separate from artifact byte
offsets; when a range cannot be proven, the GUI deliberately does not guess.
This view never edits, re-encodes, or executes the original bytes.
UniDecompiler supports the UniDecompiler Pseudocode VS Code
extension.
Install it from the Visual Studio Code Marketplace, then export pseudocode
with its optional navigation sidecar. Keep the .pse file and the adjacent
.unidec.json file together so the extension can resolve pseudocode locations
back to proven bytecode instruction facts:
unidecompiler sample.pyc \
--output sample.pse \
--vscode-metadata sample.pse.unidec.jsonThe same paired export is available from the GUI's Export pseudocode with VS Code metadata... action. Ordinary CLI and GUI pseudocode exports remain
unchanged and do not create a sidecar unless this option is explicitly chosen.
Pseudocode can be exported from the File menu. Export pseudocode writes
the currently selected result to one text file. Export all pseudocode writes
every open result that has pseudocode to a directory, using sanitized source
basenames and numeric suffixes for collisions. Results without pseudocode are
reported as skipped; source paths are never copied into output filenames.
Export pseudocode with VS Code metadata... and Export all pseudocode with VS Code metadata... are separate opt-in actions. They write the same
pseudocode files plus adjacent .unidec.json sidecars.
The GUI plugin SDK is installed automatically with unidecompiler-gui. Plugin
authors can install it directly when developing against the public, Qt-neutral
API:
python -m pip install unidecompiler-gui-sdkSee docs/GUI_PLUGIN_DEVELOPMENT.md for the plugin manifest and API contract.
GUI SDK plugins are application-layer extensions, separate from VM frontend
plugins. They run as trusted in-process Python code and use only immutable
snapshots plus host requests. They cannot access Qt widgets, ModuleIR,
FunctionIR, decoded bytecode, frontend adapters, simulator frames, or stacks.
Create a plugin with a root plugin.toml:
[plugin]
id = "example.workspace-inspector"
name = "Workspace Inspector"
version = "1.0.0"
api = "1"
entry = "workspace_inspector:register"
[python]
requires = []Install the SDK for development and install the plugin directory from the GUI:
python -m pip install unidecompiler-gui-sdkUse context.commands and context.panels to register declarative commands
and read-only panels. Installation, update, enable/disable, and removal take
effect after restarting the GUI. Plugin dependencies are checked but never
installed automatically. The complete API and lifecycle are documented in
docs/GUI_PLUGIN_DEVELOPMENT.md.
The repository includes unidecompiler-gui-test-plugin/, a complete trusted
GUI plugin that uses only unidecompiler_gui_sdk. Install that directory from
Plugins -> Manage plugins -> Install local folder, then restart the GUI. It
adds a read-only workspace panel and commands for refreshing document data and
discovering simulation targets. The example does not import Qt, core internals,
frontend decoders, or simulator implementation classes.
unidecompiler-emojivm-frontend-case/ demonstrates how to add a custom VM without changing
the core. It includes a VM format note, a sample artifact, a reference runner,
a frontend plugin, and a trusted runtime environment:
unidecompiler-emojivm-frontend-case/
├── chal.evm
├── emojivm
├── runtime.py
└── unidecompiler-plugin-emojivm/
The frontend can be registered through the public registry API:
from pathlib import Path
from unidecompiler import DecompilerEngine
case = Path("unidecompiler-emojivm-frontend-case")
engine = DecompilerEngine.discover()
engine.register_frontend_directory(case / "unidecompiler-plugin-emojivm")
result = engine.decompile_bytes(
(case / "chal.evm").read_bytes(),
filename="chal.evm",
frontend_id="emojivm",
)
print(result.status)
print(result.pseudocode.text if result.pseudocode else "<no pseudocode>")For generic-IR simulation, use the case's runtime.py through
PythonFileEnvironment. The runtime is trusted host code and is not a
sandbox. The case README contains the full registration, simulation, and
reference-runner workflow. Its test files are local development material and
are excluded from delivery.
Repository cloning is required only for development. Create and activate a Python 3.11+ virtual environment, then install the workspace packages in editable mode:
.venv/bin/python -m pip install build -e packages/unidecompiler \
-e packages/unidecompiler-gui-sdk \
-e packages/unidecompiler-simulator \
-e packages/unidecompiler-symbolic \
-e packages/unidecompiler-simulation-host-python \
-e packages/unidecompiler-cli \
-e packages/unidecompiler-gui \
-e packages/unidecompiler-plugin-lua \
-e packages/unidecompiler-plugin-python-pyc \
-e packages/unidecompiler-plugin-jvm-class \
-e packages/unidecompiler-plugin-dotnet-cli \
-e packages/unidecompiler-plugin-wasmBuild the core package independently:
.venv/bin/python -m build packages/unidecompiler