ghidra-reconstruct: Turning Decompiler Output Into Compilable C++

By Indie Kings | November 4, 2026

Updated November 4, 2026: ghidra-reconstruct turns a Ghidra decompilation into a clean, namespaced, recompilable C++ source tree. A TypeScript pipeline pulls decompiler output through the ghidra-mcp daemon, parses it into a real C++ abstract syntax tree, runs roughly 140 ordered transform passes to undo decompiler artifacts, and emits per-namespace files, a header and implementation split, a CMake build and source maps back to the original binary addresses. The repository was created on June 9, 2026 and holds 5 stars under MIT.

A chart of the original binary's code split by how closely the reconstructed source reproduces it

Image: The byte-match evidence chart produced by the toolchain this project feeds, from the Redivivus repository. Credit: jaenster/GitHub.

Per the official repository README, ghidra-reconstruct turns a Ghidra decompilation into a clean, namespaced, recompilable C++ source tree. The README describes the problem precisely. Ghidra's decompiler produces correct but ugly pseudo-C: CONCAT44 macros, undefined4 types, reciprocal-multiply division, rotated loops, raw pointer casts with offsets, iVar7 and uVar3 names, vtable calls expressed as indirect jumps, and goto-heavy control flow. That output is readable to a human and almost never compilable as a usable project.

The project is a TypeScript pipeline that pulls that output over the ghidra-mcp daemon, parses it into a real C++ AST, runs about 140 ordered AST transform passes that undo those artifacts, optionally merges a second binary such as a Mac build to attach cross-platform address anchors, and then emits an organized source tree. It was written against a 32-bit MSVC binary, Diablo II's Game.exe, but the README states the engine is binary-agnostic.

The problem with decompiler output

The gap between decompiled pseudo-C and compilable C++ is bigger than it looks from a single function. A decompiler optimizes for faithful reconstruction of behaviour, not for the constructs a compiler would emit. When it cannot prove a value's type it invents one, when it cannot find a loop it emits gotos, and when it sees a division by a constant it shows the multiply and shift rather than the operator. None of that is wrong. All of it has to be reversed before the output can be compiled by the original toolchain and compared against the original bytes.

There is a second problem that the README addresses directly: scale. Reading one decompiled function by hand is a reasonable afternoon. Reading fourteen thousand of them is not. The value of a transform pipeline is that it applies the same judgement everywhere, consistently, and can be measured when it changes. That is why the project's own README treats the transform list as an artifact worth publishing rather than a private implementation detail.

Six phases, one output tree

The README documents six phases in order, and the ordering is deliberate rather than incidental. Connect opens a session to the daemon for the primary program and preflights any additional sources, so a missing secondary binary fails fast instead of silently emitting Windows-only output. Extract lists functions, batch-decompiles them, and pulls data types, globals, strings and namespaces, with a CRT and MSVC library detector flagging compiler-runtime functions so they are skipped rather than reconstructed.

Merge folds in each configured secondary binary, annotating functions present in both with a cross-platform address and tagging functions unique to the secondary with a preprocessor guard. Analyze detects classes, runs scoping analysis, builds the call graph and computes static-global promotion. Transform and rename is where the AST passes run. Codegen and write organizes by namespace, splits headers from implementations, resolves includes, and emits CMake plus source-map files.

PhaseWhat it doesWhy the ordering matters
ConnectOpens the primary program and preflights additional sourcesA missing secondary source fails before the expensive extraction
ExtractFunctions, types, globals, strings, namespaces, machine factsResults are cached so later passes do not re-decompile
MergeFolds a second binary into the extractionDedupes by qualified name so a cross-platform body is not duplicated
AnalyzeClasses, scoping, call graph, static promotionGives the transform passes the facts they need
TransformAbout 140 ordered AST passes plus renamingPass priority is deterministic, so results are reproducible
CodegenNamespaced files, headers, CMake, source mapsEvery emitted function stays traceable to its address

Two details in the extraction phase matter for anyone who wants to reproduce the result. Per-function machine facts, meaning the compare-and-jump behind each comparison, branch and call order, jump tables, zero stores and masks, are read from one decompile per function and cached by Ghidra's modification stamps. That is how the later matching passes get evidence to work from, and stamping the cache means a Ghidra edit invalidates only what actually changed.

Cleanup passes and matching passes are different

The README draws a distinction that is easy to miss. The table of roughly 40 named passes is the original cleanup list: stripping jumptable warnings, normalizing types, turning CONCAT macros into shifts, folding branchless selects into ternaries, reconstructing switches, and so on. Those passes make the code readable. Most passes added since are a different class, which the README calls matching passes.

A matching pass reads a fact from the original instructions and writes the source form that the original compiler turns back into those instructions. They never change a value. The examples are precise. A comparison is written in the form its CMP or TEST and Jcc show. An if and else layout is ordered and nested as the original block addresses and jump edges show. A rotated loop returns to a for loop, except where a two-exit loop or a bound computed once means the rotated form was what the compiler emitted. A signed remainder by a power of two becomes the operator, conditioned on a load scheduled before the jump and a fix-up the decompiler dropped.

The concrete before-and-after examples are the most persuasive part of the README. SBB, the x86 subtract-with-borrow, combined with AND is used as a conditional select, and the pass turns the bit arithmetic back into a ternary. The multiply-and-shift sequences for division by three, five and seven become the division operator. SSA phi merges become ternary initializers. Raw offset dereferences become struct field accesses when the layouts are available. Vtable dispatch through an indirect call becomes a named virtual method call. A linear congruential generator advance becomes a named macro.

The byte match is the acceptance test

The claim that gives the project weight is not read further down, it is implemented. Per the README, a matching pass is accepted on measurement, not on how its output reads. The generated function is compiled with the original compiler, MSVC 8 in the Diablo II case, in the context of its translation unit, and compared instruction by instruction against the original binary. Each function gets a status: identical bytes, same instructions with different operands, matching instruction count, or an instruction-count difference, plus a similarity score.

The change management around that oracle is as interesting as the oracle itself. A change to the generator is measured on exactly the functions whose generated text it changes. Generate before and after from the same snapshot, compare the two per function, and count gains and losses. Losses are examined before a pass lands. The counter lives with the target tree it measures rather than in the reconstruct repository, which keeps the measuring instrument honest about what it is measuring.

Cross-platform anchoring is the feature that shows the design is meant for real decompilation projects rather than a demo. A second binary can be merged to attach cross-platform address anchors, configured as an additional source in the project file. A function present in both binaries is annotated with the secondary address, and codegen emits an anchor comment so the cleaned source stays traceable to both originals. A function unique to the secondary is tagged with a preprocessor guard. That is how a decompilation that exists for two platforms, such as a Windows and a Mac build of the same game, can live in one tree without duplicating bodies.

What it does not promise

The status section is short and unusually clear about gaps. Reconstructed code is a starting point, not a drop-in rebuild. Some struct field layouts must be present in the source Ghidra database for the field-access pass to fully resolve. The bitfield pass needs a bitfield catalogue. Custom and register calling conventions may need manual confirmation. CRT and RTTI internals are excluded and provided by the real runtime. The method-call rewrite is opt-in.

Deployment has its own friction. A Ghidra project is required and is expected to be reachable at a URL like a host and port, and the daemon URL defaults to localhost. Environment variables cover the primary project path, the daemon URL and authentication tokens for OAuth-protected servers. Building the output requires only CMake and a compiler, which is the point of emitting a CMake project rather than a pile of files. The repository also ships a resident daemon that regenerates only the units a Ghidra edit dirtied, which is the difference between a batch tool and one you can keep open while you work.

Our take: this is one of the few tooling projects we have covered where the verification method is more interesting than the feature list. Publishing roughly 140 transform passes is useful. Requiring every matching pass to be accepted by a before-and-after instruction comparison against the original compiler is what turns a code generator into a research instrument. The 5 stars understate it, because the value will show up in the projects that build on it.

FAQ

What does ghidra-reconstruct do?
Per the official repository README, it turns a Ghidra decompilation into a clean, namespaced, recompilable C++ source tree by pulling decompiler output through the ghidra-mcp daemon, parsing it into a C++ AST, running about 140 ordered transform passes and emitting organized files with a CMake build.

Is it specific to Diablo II?
No. Per the README it was written against a 32-bit MSVC binary, Diablo II's Game.exe, but the engine is binary-agnostic. Some passes, such as struct field access and bitfields, need layouts or a catalogue from the source Ghidra database to fully resolve.

How are transform passes verified?
Per the README, a matching pass is accepted on measurement rather than on how its output reads. The generated function is compiled with the original compiler and compared instruction by instruction with the original binary, and losses are examined before a pass lands.

Can I keep both a Windows and a Mac build in one tree?
Yes. Per the README, a second binary can be merged to attach cross-platform address anchors. Functions present in both are annotated with the secondary address and get an anchor comment, and functions unique to the secondary are tagged with a preprocessor guard.

Does it need Ghidra running?
It needs a Ghidra project reachable through the ghidra-mcp daemon, with the project URL and daemon URL configured by environment variables. Authentication tokens are supported for OAuth-protected servers.

Is the output ready to ship?
No. Per the README's status section, reconstructed code is a starting point rather than a drop-in rebuild. Some layouts must be present in the Ghidra database, custom calling conventions may need manual confirmation, and CRT and RTTI internals are excluded and provided by the real runtime.

Bottom Line

ghidra-reconstruct is the missing middle of the decompilation workflow: the part that turns fourteen thousand functions of decompiler pseudo-C into a source tree that actually compiles, and proves each transform against the original compiler's output instead of against how nice the code looks. The pipeline is six documented phases, roughly 140 ordered passes, and source maps that keep every function traceable to its address.

Five stars and a TypeScript codebase under MIT make this a project to read rather than a service to adopt, and the gaps the README lists are real. But if you have ever tried to hand-clean Ghidra output at scale, the byte-match oracle alone is worth the read.

Related: REA: The MCP Server That Reverse Engineers Anything, REA Tools: A Website for Agent-Driven Reverse Engineering

Sources: per the official ghidra-reconstruct repository README at github.com/jaenster/ghidra-reconstruct, fetched in full on November 4, 2026, including the pipeline diagram, transform pass tables, byte-match oracle and status sections; repository metadata (5 stars, 2 forks, no open issues, MIT, created June 9, 2026, language TypeScript, last pushed November 4, 2026) re-verified via the GitHub API on November 4, 2026.

Share