What the decompiler recovers¶
Level: 201 · for anyone who can build a C program and has just opened one they did not write
One line: Ghidra's decompiler gives back C that computes what the machine code computes — (a * 3 + b) / 2 reassembled from a lea, an add and a four-instruction signed shift — but not the C that was written: the file never held the parameter names, the local names or the struct, so p->y comes back as param_1[1] until you tell Ghidra about point, and a loop the optimizer replaced with a formula comes back as the formula.
The program¶
Four functions, each there to show one thing, and a main that calls them — demo/shapes.c:
/* Four small functions and a main that calls them -- the program this page
builds, runs, and then hands to Ghidra's decompiler. */
#include <stdio.h>
#include <string.h>
struct point {
int x;
int y;
};
/* An expression the optimizer takes apart: a multiply, an add, and a signed
division by two that becomes a shift with a sign fix-up. */
int weighted(int a, int b)
{
return (a * 3 + b) / 2;
}
/* A loop with two locals. */
int sum_to(int n)
{
int total = 0;
for (int i = 1; i <= n; i++) {
total += i;
}
return total;
}
/* A struct reached through a pointer: the file keeps offsets, not names. */
int norm2(const struct point *p)
{
return p->x * p->x + p->y * p->y;
}
/* A string the library functions know the type of. */
void shout(const char *word)
{
size_t n = strlen(word);
printf("%s has %zu letters\n", word, n);
}
int main(void)
{
struct point p = { 3, 4 };
printf("weighted(4, 6) = %d\n", weighted(4, 6));
printf("sum_to(10) = %d\n", sum_to(10));
printf("norm2({3, 4}) = %d\n", norm2(&p));
shout("decompiler");
return 0;
}
| Function | What it is there to show |
|---|---|
weighted |
an expression the compiler splits into six instructions, none of them a division |
sum_to |
a loop with two locals, which an optimizer is free not to keep |
norm2 |
a struct reached through a pointer: the file keeps offsets, the source kept names |
shout |
a parameter whose type the library functions give away |
What the file still knows¶
Build it as this page decompiles it — -O2, no -g — run it, and ask the executable what it kept. The script's names lists the exported function names with nm, minus the addresses and the leading underscore that make Linux and macOS disagree:
Verified output of decompiler_shapes_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
$ cc -std=c17 -Wall -Wextra -O2 -o shapes shapes.c
$ ./shapes
weighted(4, 6) = 9
sum_to(10) = 55
norm2({3, 4}) = 25
decompiler has 10 letters
$ names shapes
main norm2 shout sum_to weighted
$ grep -c -a 'has %zu letters' shapes
1
$ grep -c -a -w -E 'point|word|total' shapes
0
$ strip shapes
$ names shapes
$ ./shapes
weighted(4, 6) = 9
sum_to(10) = 55
norm2({3, 4}) = 25
decompiler has 10 letters
The five function names are in the file, and so is the format string: the program needs one at run time, and the others are there because nothing removed them. point, word and total are not in the file at all — they were the compiler's, and the compiler is finished. After strip, the function names are gone too and the program runs exactly as before. That is the edge of what any decompiler can do. It reads the file, and a name that is not in the file has to come from somewhere else: a debug-info file, a library whose prototypes Ghidra ships, or you.
What Ghidra gives back¶
demo/decompile.sh builds the program and hands it to Ghidra's headless analyzer with two scripts: DumpDecompiled.java writes the decompiled C of the functions it is named, and ApplyPointType.java is the next section's. The analyzer runs the same auto-analysis the Code Browser runs on import, and -deleteProject throws the project away afterwards:
#!/usr/bin/env bash
# Build shapes.c, hand the program to Ghidra's headless analyzer, and print what
# the decompiler gives back -- first as the file arrived, then after
# ApplyPointType.java tells Ghidra about struct point.
#
# Needs a Ghidra install. Homebrew's is found through `brew --prefix ghidra`;
# any other one through GHIDRA_INSTALL_DIR, the folder holding support/.
set -eu
here=$(cd "$(dirname "$0")" && pwd)
ghidra=${GHIDRA_INSTALL_DIR:-$(brew --prefix ghidra)/libexec}
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
cc -std=c17 -Wall -Wextra -O2 -o "$work/shapes" "$here/shapes.c"
"$ghidra/support/analyzeHeadless" "$work" shapes -import "$work/shapes" \
-scriptPath "$here" \
-postScript DumpDecompiled.java "$work/before.txt" weighted sum_to norm2 shout \
-postScript ApplyPointType.java "$work/after.txt" \
-deleteProject > "$work/ghidra.log" 2>&1
cat "$work/before.txt" "$work/after.txt"
Everything Ghidra printed on this page was produced on 2026-09-13 by Ghidra 12.1.3 from Homebrew on OpenJDK 25, on an x86-64 Mac running macOS 26 with Apple clang 21. CI does not run it — the runners have no Ghidra — so each block is a Real output fence that says so, and the program the fences describe is the one the verified block above built and ran.
// _weighted
int _weighted(int param_1,int param_2)
{
return (param_1 * 3 + param_2) / 2;
}
// _sum_to
int _sum_to(int param_1)
{
if (0 < param_1) {
return (int)((ulong)(param_1 - 2) * (ulong)(param_1 - 1) >> 1) + param_1 * 2 + -1;
}
return 0;
}
// _norm2
int _norm2(int *param_1)
{
return param_1[1] * param_1[1] + *param_1 * *param_1;
}
// _shout
void _shout(char *param_1)
{
size_t sVar1;
sVar1 = _strlen(param_1);
_printf("%s has %zu letters\n",param_1,sVar1);
return;
}
The leading underscores are Mach-O's: a C function weighted is the symbol _weighted on macOS and plain weighted on Linux, and Ghidra shows the symbol — the GCC build further down has none. Read against the list of capabilities in Ghidra's own introduction to the decompiler ↗:
| Ghidra says it recovers | Where it shows above |
|---|---|
| Expressions — operations the compiler split up and interleaved with others, put back into one line | (param_1 * 3 + param_2) / 2: the machine code below has no division in it |
| Function parameters — from the calling convention | two ints in weighted, one pointer each in norm2 and shout: the registers the code reads before it writes them |
| Data types, by propagation — from anything that already has one | shout's parameter is char * and its local is size_t because Ghidra knows strlen's prototype, and the type flowed backwards from the call to the parameter |
| Names and signatures you applied | none yet — every parameter is param_N and the struct is an int *; the next section applies one |
| Scoped variables — one source variable followed through stack and registers | sVar1 in shout; the two locals of sum_to are missing, and The loop that is not there says why |
| Structure definitions | param_1[1]: the offset is recovered, the field name cannot be |
weighted is the one to look at closely. This is what the decompiler started from:
0000000100000470 <_weighted>:
100000470: pushq %rbp
100000471: movq %rsp, %rbp
100000474: leal (%rdi,%rdi,2), %ecx
100000477: addl %esi, %ecx
100000479: movl %ecx, %eax
10000047b: shrl $0x1f, %eax
10000047e: addl %ecx, %eax
100000480: sarl %eax
100000482: popq %rbp
100000483: retq
leal (%rdi,%rdi,2) is a * 3 — an address-calculation instruction used as a multiplier — addl %esi is + b, and the four after it are a signed division by two: copy the value, pull its sign bit out with shrl $0x1f, add that back, then shift right arithmetically, so that -3 / 2 is -1 as C requires and not -2. The decompiler's data-flow analysis sees one value threaded through all of them and writes it as one expression. Nothing in the file says the author wrote a division; nothing else is what those instructions compute.
Tell it what the file never held¶
norm2 came back as int *param_1 with param_1[1] for p->y, because the file holds an offset of four and nothing about a struct. ApplyPointType.java does through the API what the Decompiler window does by hand — a point structure in the Data Type Manager with int x at 0 and int y at 4, then Retype Variable on the parameter to point * and Rename Variable to p — and decompiles the function again:
// _norm2, told about struct point
int _norm2(point *p)
{
return p->y * p->y + p->x * p->x;
}
That is the loop Ghidra's introduction describes: the window redecompiles on every annotation, and a type applied to one variable reaches every expression it touches. Look at the order, though: p->y first. The source said p->x * p->x + p->y * p->y; the compiler loaded y second and added in the order it liked, and the decompiler writes what the code does, not what the text said. Nothing computes differently, and nothing will tell you which order the author typed. Program annotations affecting the decompiler ↗ lists what else can be applied — function prototypes, types on globals, comments — and each of them redecompiles the function it touches.
The loop that is not there¶
sum_to is a loop over two locals in the source, and the decompiler gave back an if and a formula: (n - 2)(n - 1) / 2 + 2n - 1, which for n ≥ 1 is n(n + 1) / 2 — LLVM replaced the loop with its closed form, so there is no loop, no i and no total in the file for anyone to recover. The same source built without optimization keeps them:
// _sum_to
int _sum_to(int param_1)
{
undefined4 local_14;
undefined4 local_10;
local_10 = 0;
for (local_14 = 1; local_14 <= param_1; local_14 = local_14 + 1) {
local_10 = local_14 + local_10;
}
return local_10;
}
Two stack locals, a for with the source's shape, and names Ghidra made from the slots' offsets in the frame, because that is all a stack slot has. undefined4 is Ghidra's four bytes, type not established: nothing in the function forces them to be int rather than unsigned, and the decompiler does not guess where it has no evidence.
Build the same source with GCC and the loop is a third shape:
// sum_to
int sum_to(uint param_1)
{
int iVar1;
int iVar2;
if ((int)param_1 < 1) {
return 0;
}
iVar2 = 0;
iVar1 = 1;
if ((param_1 & 1) != 0) {
iVar1 = 2;
iVar2 = 1;
if (param_1 + 1 == 2) {
return 1;
}
}
do {
iVar2 = iVar2 + 1 + iVar1 * 2;
iVar1 = iVar1 + 2;
} while (iVar1 != param_1 + 1);
return iVar2;
}
GCC unrolled the loop by two — iVar2 + 1 + iVar1 * 2 adds i and i + 1 in one step — and peeled the first iteration off when n is odd. Three compilations, three functions, and the same output from each. CI checks that for the builds it can make — the same source at -O0 and -O2, with GCC on Linux and clang on macOS — as two files that differ and print the same lines:
Verified output of decompiler_levels_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
$ cc -std=c17 -Wall -Wextra -O0 -o shapes0 shapes.c
$ cc -std=c17 -Wall -Wextra -O2 -o shapes2 shapes.c
$ cmp -s shapes0 shapes2
exit 1
$ ./shapes0
weighted(4, 6) = 9
sum_to(10) = 55
norm2({3, 4}) = 25
decompiler has 10 letters
$ ./shapes2
weighted(4, 6) = 9
sum_to(10) = 55
norm2({3, 4}) = 25
decompiler has 10 letters
The decompiler recovered each compiler's program, not the author's. The Rust library's What the optimizer does ↗ shows the same LLVM turning a ten-element sum into movl $55, %eax.
Stripped¶
The function names came from the symbol table. After strip, Ghidra has nothing to name a function with but its address:
// FUN_100000470
int FUN_100000470(int param_1,int param_2)
{
return (param_1 * 3 + param_2) / 2;
}
// entry
undefined8 entry(void)
{
_printf("weighted(4, 6) = %d\n",9);
_printf("sum_to(10) = %d\n",0x37);
_printf("norm2({3, 4}) = %d\n",0x19);
_printf("%s has %zu letters\n","decompiler",10);
return 0;
}
FUN_100000470 is the function's address in the file; entry is where the load command says execution starts, since the _main symbol is gone; and _printf kept its name because an import has to be named for the dynamic linker to find it. The bodies are the same text as before — the decompiler never needed the names. And main is the optimizer's work once more: every call had constant arguments, so all four were computed at compile time, and the function that reached the file prints 9, 0x37 and 0x19. That is the FUN_00401040 and param_1 at the top of Ghidra's introduction: not a failure of analysis, but a file with nothing better in it.
If you are coming from another language¶
Rust. The same LLVM behind rustc, so a summing loop folds the same way — the Rust library's What the optimizer does ↗ watches a ten-element sum become a constant. Its assembly listing also shows what a Rust binary keeps that a C binary does not: a mangled name that encodes the module path and the generic arguments, so an unstripped Rust function arrives in Ghidra with more of its name. Past the name, it is the same problem.
Python. There is nothing to recover, because nothing was thrown away: a function's parameter and local names ride along in its code object, and the bytecode refers to them by name.
Verified output of decompiler_names_kept_py.py — regenerated by tools/run_examples.py, never hand-typed.
ABAP. (Not machine-checked — CI cannot run ABAP.) Nothing to decompile either, for the opposite reason: the source is the object of record, stored in the repository beside the generated load, and the kernel regenerates the load from the source whenever it is stale. The decompiler's problem — machine code and no source — is a state an ABAP system is not supposed to reach.
See also¶
- Ghidra's help at the installed tag: the introduction ↗, concepts ↗ and annotations ↗ — the pages this lesson checks against; the Help menu of a running Ghidra shows them rendered
DecompInterface↗ — the class both scripts use — and the headless analyzer's README ↗, which ships assupport/analyzeHeadlessREADME.htmlinside the install, for the-import,-scriptPathand-postScriptoptions- ELF ↗ and Mach-O ↗ in the encodings library — the two containers the builds on this page came in, and where each keeps the symbol table that
stripempties - Makefiles — where
-O2comes from in a real build, and whatCFLAGSreaches