0 of 13 steps0%
Back to guides
Tech & ComputersMedium

How to Use Ghidra to Decompile a File into Readable Code

A step-by-step walkthrough of Ghidra, the free NSA-built decompiler. Learn to take a compiled .exe or .dll, open it in the CodeBrowser, and produce pseudo-C that shows exactly what the program does. Covers both interactive (click-around) and headless (one-command) modes, plus the honest limits — Ghidra is a decompiler, not a decryptor, and what that distinction actually means for your review.

admin
25 min
13 steps
20 views

What Ghidra is — and what it is not

Step 1 of 13

The first thing to be clear about: Ghidra is a decompiler, not a decryptor. This distinction is the difference between useful and useless results. A decompiler takes compiled machine code (x86, ARM, whatever the binary was built for) and reconstructs a readable C-like representation. The logic is real, the API calls are real, the math is real. The variable names, function names, and comments are gone forever — the compiler removed them before producing the binary. A decryptor reverses encryption. That requires the key. Ghidra cannot crack AES, RSA, ChaCha20, or any other cryptographic scheme. If your file is genuinely encrypted, no tool can recover the plaintext without the key — that is a math fact, not a Ghidra limitation. What Ghidra can recover from a compiled binary: every function, every API call, every string, every control flow path, every arithmetic operation. For a reverse engineer or malware researcher, that is the whole program — reconstructed in a form you can read. What Ghidra cannot recover: the original C/C++ source code (names, comments, types), the original variable types without a PDB file, the contents of any data that was encrypted before being placed in the binary, or any cryptographic key that is not stored in plaintext. Keep this distinction in mind. It saves a lot of frustration later.

Install Ghidra and a JDK

Step 2 of 13

Ghidra is a Java application, so it needs a JDK before it can run. The supported versions are JDK 17 and 21 LTS. Linux (Debian/Ubuntu): sudo apt install openjdk-17-jdk java -version # should report 17.x macOS: brew install openjdk@17 Windows: Download the Temurin 17 MSI from `adoptium.net` and run it. Java does not need to be on PATH for Ghidra to find it, but it helps. Then install Ghidra itself. The official site is `ghidra-sre.org`. Download the latest stable release (currently 11.3.x). It is a ~500 MB zip. Unzip it anywhere — there is no installer, just a folder. On Linux/macOS, run `ghidraRun` from the unzipped folder. On Windows, run `ghidraRun.bat`. First launch is slow (30–60 seconds) because Ghidra builds its review caches. After that, it is fast. Some Linux distributions also package Ghidra. On Debian, `apt install ghidra` works but the version tends to lag the official release by a year or two. The official zip is usually more up to date.

Create a project and import your binary

Step 3 of 13

When Ghidra opens, you see the project window. It is empty. The first thing to do is create a project to hold your work. Go to File → New Project. Pick Non-Shared (unless you have a team setup with a Ghidra server). Pick a folder. Ghidra stores all imported binaries, review results, and your naming edits there. Treat the project folder like a workspace — one per investigation. Then File → Import File and select your binary. Ghidra auto-detects the format: PE (Windows .exe/.dll), ELF (Linux), Mach-O (macOS), or raw firmware. The import dialog shows what it found. The defaults are fine for almost everything — just click OK. After import, the binary appears in the project window. Double-click it. Ghidra asks: Would you like to examine now? Click Yes. This is the slow part — 10 to 60 seconds depending on binary size, during which Ghidra identifies functions, strings, cross-references, and builds its database. For a small sample (under 1 MB), review takes a few seconds. For a 4 MB binary, expect 15–30 seconds. For a 40 MB binary with thousands of functions, give it 5–10 minutes. When review finishes, the CodeBrowser opens. That is where the work happens.

Understand the three-panel CodeBrowser layout

Step 4 of 13

Understand the three-panel CodeBrowser layout

The CodeBrowser is the main Ghidra work surface. It has three connected panels. Clicking anything in one updates the others — the panels show the same program from three different angles. Left — Symbol Tree: The navigation panel. Functions, imports, exports, labels, strings. Use the search box at the top (Ctrl+F) to find a function by name. If your binary has removed symbols (most malware does), functions appear as `FUN_00401dab` — that address-based naming is Ghidra's placeholder. Middle — Listing: The disassembly. Pure x86 (or ARM, or whatever architecture) instructions at the current address. This is what a debugger shows you, but read-only. The highlighted line is the current selection. Click any instruction and the decompile panel updates. Right — Decompile: The pseudo-C output. This is Ghidra's reconstruction of what the machine code does in C-like form. Variable names are removed-PDB placeholders: `param_1`, `local_2c`, `iVar3`. The control flow, the math, the API calls — those are real. The three panels are connected. Click a function in the Symbol Tree and the Listing scrolls to its first instruction; the Decompile panel shows the pseudo-C. Click any line in the Decompile panel and the Listing jumps to the corresponding machine code. This three-way linkage is what makes Ghidra fast to use — you can navigate by what the program is doing rather than by address. If you only ever use one panel, make it the Decompile panel. That is where the readable code is.

Find and decompile a function (press F5)

Step 5 of 13

The most common action in Ghidra: find a function, decompile it, read the result. Pick a function. Start with one of the auto-named `FUN_004xxNN` entries in the Symbol Tree. The address-prefixed names are unhelpful at first, but as you explore you will recognize patterns. Functions that call `CreateFileW` are doing file I/O. Functions that call `RegCreateKeyExW` are touching the registry. Functions that call `CryptAcquireContext` are setting up encryption. Click the function to select it. The Listing and Decompile panels update. Now press F5 (or go to Window → Decompile). The Decompile panel rebuilds. For small functions this is instant. For large functions (hundreds of lines of pseudo-C), it takes a second or two. What you see is pseudo-C that looks like real C. It is not. The names are made up. But the structure is faithful — if the machine code does `if (x > 0) { y = 1; } else { y = 2; }`, Ghidra writes exactly that. If the machine code calls `CreateFileW` with five arguments, Ghidra shows `CreateFileW(arg1, arg2, arg3, arg4, arg5)` with the argument values filled in from the stack at the call site. Three things to do with the output: 1. Read it. Most of the program logic is right there. 2. Rename variables. Right-click `param_1` → Rename Variable → `hFile`. This is annotation, not review — but it makes the next person (or future you) much faster. 3. Add a comment. Press ';' at any line and type a note. Comments save to the Ghidra project, not the binary, so they are private to you. That is the basic loop: click a function, press F5, read the result, annotate. Repeat.

Read a real Ghidra decompile from start to finish

Step 6 of 13

Read a real Ghidra decompile from start to finish

This is what actual decompiled output looks like. The image below shows Ghidra's CodeBrowser pointed at a function from Pikachu.exe (a 1999 Visual Basic worm, 32 KB, x86). The decompile panel on the right is reconstructed C from 47 bytes of x86. What Ghidra recovered: 4 real function calls including the Windows API CryptAcquireContextA; 2 control flow branches; 1 string literal; 1 reference to an internal helper. What Ghidra could NOT recover: the fact that the kill_switch string is actually a registry key (you would need cross-references to see that); the original variable names like hCryptKey or hKey; the order of operations matters to the threat intent but not to Ghidra. The lesson: in 47 bytes of compiled code, Ghidra gave us a complete, readable description of what the function does. The variable names are placeholders, but the logic, the API calls, the control flow — all real. This is why Ghidra is enough for most reverse engineering work without needing the original source. The image is the decompiled.c output that the headless Ghidra script produces: one .c file with every function separated by a header line. On Pikachu.exe the whole binary is just 9 functions, 800 lines. A real piece of malware (3-4 MB) produces 6,000-8,000 lines. The fun part is reading it.

The full decompiled.c output (headless mode)

Step 7 of 13

The full decompiled.c output (headless mode)

Below is what the headless decompile script produces for a small binary. Each function is preceded by a header like `=== FUN_00403c80 @ 00403c80 | size=111 | entry-point dispatcher ===`. The header tells you the function's name (FUN_00403c80), its address in memory (00403c80), its size in bytes (111), and — if you renamed it — a human label like 'entry-point dispatcher'. The body is the reconstructed C. Notice FUN_00403c80 is the entry point: it gets called by the OS loader first. FUN_00404b10 is the C runtime init wrapper — it sets up the C runtime then calls main. FUN_00405a08 is the actual main: it just calls FUN_00403cf0 with a single argument. And FUN_0040b148 is the main loop — it does nothing but call a function pointer in a busy loop, which is the classic 'wait for something to happen' pattern. From these 4 functions you can already tell what the program does. Working on real malware is the same pattern: start with the entry point, follow the call tree, rename as you go, and within 30 minutes you understand what the program is trying to do.

If the file is a script (PowerShell, VBS, JS) you get this

Step 8 of 13

If the file is a script (PowerShell, VBS, JS) you get this

Not every sample is a compiled binary. Many start as a PowerShell one-liner or a VBScript dropper. Ghidra is irrelevant for those — they are already source. But they are usually obfuscated: base64-encoded payloads, XOR'd strings, nested Invoke-Expressions. The multi-stage layer (which runs before Ghidra) removes the encoding. Below is what a fully-decoded PowerShell reverse shell looks like once the layers are removed. Four stages are visible at a glance: Stage 1 turns off Windows Defender realtime monitoring. Stage 2 writes a base64-decoded .exe to the user's temp folder. Stage 3 registers a scheduled task to run the dropped .exe on every logon. Stage 4 opens a TCP socket to a hardcoded IP and pipes PowerShell commands back and forth — a fully interactive reverse shell. You can see the IP, the port, the persistence mechanism, the AV bypass, all in one screen of code. No Ghidra needed — but the multi-stage pipeline that produced this clean view is the same one that feeds Ghidra when the underlying payload IS a binary. The pipeline handles both cases.

Annotate what each piece of code does

Step 9 of 13

Annotate what each piece of code does

Reading decompiled code or even decoded scripts is slow if you do not know what each API call does. The companion tool explainer.py (also in the decoder pipeline repository) annotates any file in three layers: a one-sentence file summary (built from MITRE pattern combos), a per-function summary (heuristic from body content), and per-line annotations (token + what it is + why it is there + MITRE tag). Below is a real output for a PowerShell reverse connection — the same one shown in the previous step. The first section is the file summary: multi-stage reverse connection that disables AV, drops and persists a payload, then opens a TCP socket for live command execution. The MITRE tags detected tell you the techniques in play: T1059.001 (PowerShell execution, 4 hits), T1562.001 (Defender tampering, 2 hits), T1027 (obfuscation, 2 hits), T1071.001 (C2 channel, 1 hit). The line annotations below show what each line is doing — L3 disables Defender, L7 decodes a base64 blob, L13 establishes persistence via scheduled task, L17 opens the TCP C2 socket, L22 runs the remote command via Invoke-Expression. To run it on your own file: `python3 explainer.py malware.ps1` for the plain-text view, `--format json` for machine-readable, `--format markdown` for paste-into-docs, the --decode flag to remove encoding layers first. The companion tool decoder.py produces the readable code; explainer.py produces the explanation of what the code is doing.

Read decompiler output skeptically

Step 10 of 13

Ghidra is right about 95% of the time. The other 5% is where you need to be careful. What is reliable: Control flow (if/else/while/for reconstruction is solid), arithmetic, function calls with their arguments, string references, structure layout when the binary has type info. What is best-effort: Variable types (without PDB, Ghidra guesses). If you see `undefined4 local_2c = 0;`, that is a 4-byte value of unknown type. It might be an int, a pointer, a flag — Ghidra does not know without more context. What is often wrong: Array bounds, struct field names, complex pointer arithmetic. Ghidra will show you `*(int *)(param_1 + 0x1c)` and you have to figure out from the calling code what struct that offset belongs to. Apply struct types manually using the Data Type Manager (Window → Data Type Manager). How to verify: Switch to the Listing panel. The highlighted line in Decompile corresponds to a highlighted line in the machine code. If the pseudo-C says `if (eax == 0)`, look at the machine code — is it really a comparison, or is the compiler doing something subtle? The Decompile panel summarizes; the Listing shows the truth. The naming tells you a lot. A function called `FUN_00401dab` that imports from `ADVAPI32.dll` and references `CryptAcquireContextA` is almost certainly initializing Windows crypto. Ghidra will not name it for you (no PDB), but you can. Right-click the function name → Rename Function → `init_windows_crypto`. Now the Symbol Tree, the Decompile panel header, and the cross-reference window all use your name. Decompiler output is a starting point, not a conclusion. The good news is that for most reverse engineering tasks, it is more than enough to understand what the program does.

Run Ghidra headless for batch work

Step 11 of 13

Run Ghidra headless for batch work

Interactive mode is for deep diving on one binary. For scanning many samples or feeding results into a pipeline, use headless mode. Headless mode runs Ghidra in a terminal with no GUI. The entry point is `examineHeadless`, in the `support/` folder of your Ghidra install. The basic command structure is: examineHeadless /tmp/work my-project \ -import malware.exe \ -scriptPath /path/to/my/scripts \ -postScript decompile_all.py /tmp/out.c 9999 \ -deleteProject What each part does: • `/tmp/work my-project` — workspace folder and project name. The project is auto-created. • `-import malware.exe` — the binary to examine. • `-scriptPath /path/to/my/scripts` — folder containing Jython scripts. Ghidra scans this folder for available scripts at startup. • `-postScript decompile_all.py /tmp/out.c 9999` — after review, run this Jython script. Pass it the output path and a max-function cap. • `-deleteProject` — clean up the temp project when done. Without this, you will accumulate hundreds of project folders. The output is one `.c` file with all functions separated by `=== name @ address | size=N ===` headers. On a 3.4 MB ransomware sample, this takes 13 seconds and produces 6,500 lines of pseudo-C across 134 functions. The scriptPath gotcha: Ghidra's `-postScript` needs the script in a Ghidra-discoverable directory. If you pass a full path to a `.py` file, Ghidra throws `ClassNotFoundException: Failed to find source bundle containing script`. Put the script in a folder and pass that folder with `-scriptPath`. Headless mode is what automated tools (and the `the decoder` pipeline) use to scan many binaries in sequence.

The honest limits — when Ghidra cannot help

Step 12 of 13

Ghidra is a powerful tool. It also has limits that are easy to run into. Knowing them ahead of time saves hours of confused debugging. Packed binaries. If the file is packed with UPX, MPRESS, ASPack, or similar, the actual code is compressed and the decompiler will show you the unpacker stub, not the real program. Unpack first: `upx -d packed.exe -o unpacked.exe`, then examine the unpacked version. Ghidra has packer detection (look for sections named `UPX0`, `UPX1`, `.aspack`, `.MPRESS1`) but cannot unpack for you. Encrypted payloads. If the binary decrypts its real payload at runtime using `CryptEncrypt` or `CryptDecrypt` with a key fetched from a server, the encrypted bytes in the file are random noise. Ghidra can show you the decryption call site, but it cannot read past it. To get the real payload, you need to run the malware in a sandbox and dump memory after the decryption happens. That is dynamic review, which is a different guide. Anti-reverse tricks. Some binaries detect debuggers and refuse to run, or detect Ghidra's review and behave differently. Look for `IsDebuggerPresent`, `CheckRemoteDebuggerPresent`, timing checks, and exception-based obfuscation. Bypassing these is a deep topic on its own. The original source code is gone. This is the most important one. Ghidra gives you pseudo-C that runs the same way the original did, with the same logic, the same API calls, the same behavior. But the variable names you would have written, the comments you would have left, the struct definitions from the original header files — those are not in the binary. They never were. The compiler discarded them. A decompiler reconstructs behavior, not authorship. Accept that limit and Ghidra is a remarkably capable tool. Fight it and you will be frustrated.

Next steps and references

Step 13 of 13

Where to go after this guide. Practice on easy targets. Compile a small C program yourself (a hello world, a file copier, a calculator) and decompile it. Compare Ghidra's output to the original source. You will quickly learn what decompilation preserves and what it loses. Try real samples. Public malware repositories have labeled samples for learning: MalwareBazaar, VirusBay, theZoo, and academic datasets from Endgame or Sophos. Use a virtual machine. Never run unknown binaries on your host. Learn the keyboard shortcuts. F5 (decompile), G (go to address), L (label), ; (comment), Ctrl+Shift+F (search), Ctrl+click on a call (jump to callee). The first hour with Ghidra is slow; the second hour is fast. Pair Ghidra with a debugger. Ghidra is static review (no execution). For seeing what a program actually does at runtime, use x64dbg (Windows) or gdb (Linux). The Ghidra decompile view tells you what could happen; the debugger tells you what did happen. They are complementary. Useful resources: • The Ghidra official docs at `ghidra-sre.org` — heavy but complete. • The Ghidra Users mailing list and Slack — active community. • The Ghidra Book by Chris Eagle — the canonical reference. • CTF challenge archives (picoCTF, reversing.krypto.rocks) for hands-on practice. Reverse engineering is a learnable skill. Ghidra lowers the barrier — most of the work it does for you used to take an expert weeks of staring at machine code. Take advantage of that, and spend your time on the parts that actually require human judgment: understanding what the program is trying to do.