What a Decompiler Does
You have heard that compiling is a one-way street. It mostly is, yet decompilers reconstruct readable source from compiled code every day. Both statements are true at once, and understanding why is the key to knowing what a decompiler can and cannot hand an attacker.
What survives compilation
Compilation throws away things humans need and keeps things the machine needs. The machine needs the operations, the control flow, and the constants, so those survive completely. It does not need your variable names, your comments, or your formatting, so those are discarded. A decompiler can perfectly recover the first group and can only guess at, or invent placeholders for, the second.
Think of translating a novel into another language and back. The plot, the events, and the dialogue survive the round trip intact, because they carry meaning. The author's exact word choice, the rhythm of the sentences, and the chapter breaks do not come back the same, because the translation never needed to keep them. A decompiler is that round trip: the logic returns, the style does not.
More precisely, a decompiler reads the bytecode, recognizes the patterns the compiler emits for loops, conditionals, and function calls, and rebuilds the equivalent high-level structures. Because the operations and control flow are fully present in the bytecode, this reconstruction is faithful in behaviour. What it cannot reconstruct is the information the compiler had no reason to record: the original names become generic labels like v1 and v2, comments are simply gone, and the formatting is whatever the decompiler's printer chooses.
Close, but never the original
The output of a decompiler is a program that behaves like the source and reads like a stripped-down version of it. Below is what a small routine looks like going in as source and coming back out of a decompiler: the shape and logic return faithfully, but every name is a placeholder and every comment is gone.
// original source, as written
function discountedTotal(price, isMember) {
// members get 20 percent off
let rate = isMember ? 0.8 : 1.0
let total = price * rate
return total
}// decompiler output: same behaviour, no names or comments
function f1(a1, a2) {
var v1 = a2 ? 0.8 : 1.0
var v2 = a1 * v1
return v2
}For an attacker this is enormously useful and genuinely limited at the same time. The logic is legible, so they can understand and modify behaviour. The intent is muddied, so they still have to figure out what f1 and v1 mean, which on a large program is real work. Decompilation recovers the how; it does not recover the why.
Why this matters for obfuscation
Two facts follow directly, and they explain a lot about how protection is designed. First, since names and comments never survive compilation, obfuscating them is almost free of downside, because you are only scrambling information the machine already discards. Second, and more importantly, since the operations and control flow survive perfectly, they are exactly what a decompiler hands the attacker, so serious protection has to attack that surviving structure, not just the names.
- Renaming defends the names, which the decompiler had already reduced to placeholders anyway, so its marginal benefit against a decompiler is small.
- Control flow flattening attacks the structure the decompiler reconstructs, forcing it to emit a tangle instead of clean loops and branches.
- Virtualization goes furthest: it compiles your logic for a custom machine, so a normal decompiler recovers only the interpreter, not your program, and the attacker must reverse the machine first.
The honest limit runs the other way too. A decompiler cannot recover what was never there, but it also cannot be stopped from recovering what is there. If your logic must ship as ordinary bytecode for a standard runtime, a decompiler will reconstruct that logic's behaviour, full stop. This is why the strongest static defense is to not ship ordinary bytecode at all, but a custom encoding a stock decompiler does not understand. Even then, the run-and-dump ceiling from later in this level still applies: a custom machine can be observed executing.
A decompiler recovers everything the machine needed and nothing the human added. Logic and control flow come back faithfully; names, comments, and intent do not. That split is why obfuscating names is cheap but only partial, and why real static strength means transforming the surviving structure itself, up to shipping a form a stock decompiler cannot read.
Frequently asked questions
If a decompiler recovers the logic, is compiled code pointless to protect?
No, but it reframes the goal. A decompiler recovers behaviour and control flow faithfully while losing names, comments, and intent, so protection that only scrambles names offers limited resistance to it. Strong static protection targets the surviving structure, for example by flattening control flow or, most effectively, by compiling for a custom machine a stock decompiler cannot read.
Does decompilation give back the exact original source?
No. It gives back a program that behaves like the original and reads like a stripped version of it, with generic placeholder names, no comments, and the decompiler's own formatting. The how comes back; the why, carried by names and comments, does not, which on a large program is a meaningful barrier to understanding.
Can obfuscation stop a decompiler completely?
It cannot stop a decompiler from recovering logic that ships as ordinary bytecode for a standard runtime. It can change what the decompiler sees, most strongly through virtualization, so the tool recovers only an interpreter. Even then the dynamic run-and-dump attack still applies, because a running machine can be observed, which is the recurring ceiling of this level.