ghidra-decomp
Bulk-decompile binaries into browsable source trees using Ghidra.
Takes a binary, runs Ghidra's decompiler on every function, and produces a directory of .c files and JSON indexes — ready for grep, code review, or AI-assisted analysis.
Why
Reverse engineering through Ghidra's GUI (or MCP) means looking at one function at a time. This tool dumps everything upfront so you can treat the binary like a normal codebase: grep for patterns, read call graphs, search strings — all without waiting for decompilation round-trips.
The analyzed Ghidra project is retained and reused, so iterating on the same binary does not pay for analysis again. --resume rebuilds only the functions whose artifacts no longer match their recorded hashes.
Install
Requires Ghidra 12.0+ and PyGhidra 3.0+.
pip install -e .
Set GHIDRA_INSTALL_DIR to your Ghidra installation, or pass --ghidra-path.
Usage
ghidra-decomp ./firmware.bin -o ./firmware_decomp
Options:
| Flag | Description | Default |
|---|---|---|
-o, --output |
Output directory | <binary>_decomp/ |
--timeout |
Per-function decompilation timeout (seconds) | 60 |
--combined |
Also emit all_functions.c |
off |
--ghidra-path |
Path to Ghidra install | $GHIDRA_INSTALL_DIR |
--base-addr |
Rebase binary to this address before analysis (e.g. 0x80000000) |
none |
--entry |
Mark this address as the entry point and disassemble from it before analysis (e.g. 0x31000). Useful for raw binaries. |
none |
--language |
Force Ghidra language ID (e.g. ARM:LE:32:v7). Use when auto-detect fails. |
auto |
--compiler |
Force compiler spec ID (e.g. gcc, default, windows). Requires --language. |
auto |
--list-languages |
List all available language IDs and compiler specs, then exit. | |
--workers |
Decompiler threads. JVM memory, not CPU count, is usually the limit. | min(4, CPU count) |
--no-cache |
Do not retain or reuse a Ghidra project. | off |
--cache-dir |
Where retained Ghidra projects live. | $XDG_CACHE_HOME/ghidra-decomp |
--resume |
Reuse per-function artifacts from an existing output whose hashes still match. | off |
--force-analysis |
Ignore any retained project and analyze again. | off |
--force-export |
Reuse the retained project but rebuild every artifact. | off |
--include / --exclude |
Name regexes selecting which functions to export. Repeatable. | all |
--function |
Export the function at this entry address regardless of name filters. Repeatable. | |
--gdt |
Apply a Ghidra data type archive before analysis. Repeatable. | none |
--gzf |
Also export program.gzf for reopening in the Ghidra GUI. |
off |
--semgrep-config |
Scan the exported pseudocode with semgrep and write semgrep.sarif. |
off |
Caching and resume
Analysis is the expensive part, so the analyzed Ghidra project is retained under
$XDG_CACHE_HOME/ghidra-decomp/<fingerprint>/ and reused when the input bytes,
base address, entry point, language, compiler, data type archives, Ghidra
version, PyGhidra version, exporter version, and schema version all match. Any
mismatch re-analyzes rather than failing, and manifest.json records the
decision under cache:
{"enabled": true, "hit": false, "path": "...", "reason": "cached analysis was built with a different ghidra_version"}
--resume additionally reuses per-function artifacts from an existing output.
A function is reused only when its sidecar records a successful decompilation
and its pseudocode, assembly, and sidecar all still hash to what the previous
run recorded; anything else is decompiled again.
ghidra-decomp ./firmware.bin -o ./firmware_decomp # cold: analyze and export
ghidra-decomp ./firmware.bin -o ./firmware_decomp --resume # warm: reuse both
For raw binaries or unknown formats, Ghidra will refuse to auto-load. Discover the right spec then pass it:
ghidra-decomp --list-languages | grep -i arm
ghidra-decomp ./firmware.bin --language ARM:LE:32:v7 --compiler default
Raw binaries typically also need a known image base and an entry point for auto-analysis to reach every function. Both are applied before analysis runs, so function boundaries, xrefs, and switch-table recovery all happen at the real addresses:
ghidra-decomp ./dal_ivm.mod \
--language x86:LE:32:default \
--compiler gcc \
--base-addr 0x00031000 \
--entry 0x00031000
Output
firmware_decomp/
├── manifest.json # fingerprint, versions, cache decision, counts, artifact hashes
├── functions/
│ ├── 00010000_<digest>_main.c
│ ├── 00010000_<digest>_main.json
│ ├── 00010234_<digest>_parse_config.c
│ └── ...
├── assembly/
│ ├── 00010000_<digest>_main.s
│ └── ...
├── xrefs.jsonl # call-site-aware code and data references
├── all_functions.c # optional; enabled with --combined
├── program.gzf # optional; enabled with --gzf
├── semgrep.sarif # optional; enabled with --semgrep-config
├── types.json # structs, enums, unions, typedefs + provenance and users
├── functions.json # function index with address ranges + signatures
├── callgraph.json # who calls who
├── strings.json # strings + xrefs to functions
├── imports.json # external library functions
├── exports.json # exported entry points
├── symbols.json # globals, labels, data
├── sections.json # memory map with r/w/x permissions
└── metadata.json # binary info + stats
Each .c file includes a metadata header. Its adjacent JSON sidecar records body
ranges, signature and variable storage, calls, references, related strings and
globals, decompilation status, artifact paths, and hashes. Assembly files contain
instruction addresses, raw bytes, mnemonics, and operands.
Local variables come from the decompiler's own model, so the uVar1 you read in the
pseudocode carries its type and storage. A thunk records the name and address of what
it forwards to, and the decompiler's own warnings — "Subroutine does not return",
"Unknown calling convention" — are structured under decompilation.warnings instead of
being left as prose inside the C:
"is_thunk": true,
"thunk": {"name": "getenv", "address": "EXTERNAL:00000001"},
"decompilation": {
"status": "success",
"warnings": [{"kind": "warning", "text": "Subroutine does not return"}]
}
Every type record says where it came from, how it is laid out, and who uses it, so a
consumer can tell a layout the compiler described from one Ghidra reconstructed off the
bytes before trusting an offset. source_archive.archive_type is PROGRAM for a type
recovered from this binary and FILE or PROJECT for one imported from an archive; on
/usr/bin/true, 11 of 28 types come from generic_clib_64 and the other 17 from the
binary itself. used_by reaches back to the data and functions that use the type,
including through pointers and arrays, so types.json is not a dead end:
{
"kind": "struct",
"name": "Elf32_Sym",
"path": "/ELF/Elf32_Sym",
"category_path": "/ELF",
"alignment": {"value": 1, "type": "DEFAULT", "minimum": null},
"packing": {"type": "DISABLED", "value": null},
"source_archive": {"name": "binary", "archive_type": "PROGRAM"},
"usage_count": 1,
"usage_truncated": false,
"used_by": [
{
"kind": "data",
"address": ".symtab::00000000",
"name": "Elf32_Sym_ARRAY_.symtab__00000000",
"ordinal": null,
"via": "Elf32_Sym[10]"
}
]
}
packing and alignment.type are null for a kind with no members to pack. via is the
declaration at the use site, so a pointer or array use stays distinguishable from a
direct one. usage_count is always the true total; used_by is capped at 200 entries
and sets usage_truncated when it is cut, so a bounded list never reads as a small one.
Schema 5 artifact filenames include a stable digest and bounded ASCII components so
segmented addresses, long symbols, and platform-reserved names remain safe. Consumers
must follow the artifact paths in functions.json rather than constructing filenames.
// Function: parse_config
// Address: 00010234
// Size: 284 bytes
// Calling: __stdcall
// Params: 3
void parse_config(char *param_1, int param_2, int param_3) {
...
}
manifest.json is the corpus completion marker. It fingerprints the snapshotted input
bytes and analysis configuration, records Ghidra/PyGhidra/tool versions, summarizes
function statuses, and hashes every generated index. Exports are built in a hidden
sibling staging directory whose .run-state.json marks that staging corpus incomplete.
The existing completed output remains untouched if analysis fails. Publication is
serialized by a destination lock and occurs only after the staged manifest, artifacts,
and indexes are durable.
Each function receives at most the configured --timeout, never multiplied by worker
count or retries. A function that the decompiler declines is recorded as fallback; a
timeout or exception is recorded as failed, which --resume retries on a later run.
Either way the C artifact contains the error and the normal assembly artifact remains
available for analysis.
successful, fallback, and failed partition the discovered functions. skipped
counts the reused subset and overlaps them: it answers how much work a run avoided,
not how a function ended.
Intended workflow
- Dump the binary with
ghidra-decomp - Analyze the output like source code — grep, glob, read
- Re-run with
--resume, or narrow to one function with--include/--function, as the objective sharpens. The retained project means this is cheap. - Write back renames/annotations to Ghidra via MCP (separate tool, not yet built)
Development
Fast checks:
ruff check src/ tests/
ruff format --check src/ tests/
mypy src/ghidra_decomp/ --ignore-missing-imports
pytest tests/test_artifacts.py tests/test_cache.py \
tests/test_decompiler_helpers.py tests/test_filters.py tests/test_parallel.py
pytest tests/test_multiarch_e2e.py::test_checked_in_fixture_hashes_and_provenance
CI installs the project and pytest before running these fast unit/CLI tests and the checked-in fixture checksum/provenance validation. It does not start embedded Ghidra.
The complete suite starts embedded Ghidra and decompiles the original repository
fixture, the host's true executable, and six stored ELF fixtures covering x86-32,
ARMv7, AArch64, MIPS32LE, PowerPC32BE, and RISC-V64:
GHIDRA_INSTALL_DIR=/path/to/ghidra pytest tests/
The system-binary integration test verifies manifest and artifact hashes, sidecars,
addressed assembly bytes, call-site-aware xrefs, storage metadata, stack frames, and
combined output. It skips when the host does not provide true or a Ghidra install.
See tests/fixtures/README.md for the pinned LLVM 21 regeneration and reproducibility
procedure for the multi-architecture binaries.
License
MIT
Release files for ghidra-decomp 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ghidra_decomp-0.5.0.tar.gz | 91.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ghidra_decomp-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 128.0 kB
Release files / ghidra_decomp-0.5.0.tar.gz
| Download URL | ghidra_decomp-0.5.0.tar.gz |
|---|---|
| Size | 91.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
60a4dc4ab3b7b46b10c30493c8291f89d8957361821c97255e27df9445911c7a
|
|
BLAKE2b-256 checksum How to use checksums |
50c2b32829182338b3f7ae38c4bdc5bf7e78223ed08f877cd024f178faaf01d5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / ghidra_decomp-0.5.0-py3-none-any.whl
| Download URL | ghidra_decomp-0.5.0-py3-none-any.whl |
|---|---|
| Size | 36.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0da6c5133974d17b1a97e7f7cfbfd5c4c63b0a73363e012dadc5ee8d10663ae8
|
|
BLAKE2b-256 checksum How to use checksums |
b680ccd2614b1f1a6b8f545373c913a4bb6a3d178eb94557afad85edc925d110
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|