Skip to main content

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

  1. Dump the binary with ghidra-decomp
  2. Analyze the output like source code — grep, glob, read
  3. Re-run with --resume, or narrow to one function with --include/--function, as the objective sharpens. The retained project means this is cheap.
  4. 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)

Source distribution for ghidra-decomp 0.5.0
File Size Uploaded
ghidra_decomp-0.5.0.tar.gz 91.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ghidra-decomp 0.5.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page