17 min read

What Is Reverse Engineering Actually Reversing? — For Programmers Who've Never Touched Assembly

What Is Reverse Engineering Actually Reversing? — For Programmers Who've Never Touched Assembly

This is the concepts installment of my reverse engineering series — concepts only, almost no assembly. The next one is the methods installment. Assumed reader: you can program (variables, functions and loops are enough), but you have never touched a disassembler, Ghidra, or assembly language. By the end you will know what reversing actually is and why all those tools exist, so that the methods post doesn't scare you off with assembly.


Let's answer the question nobody bothered to explain

You've probably read plenty of reverse engineering articles full of hex, registers, and mov ldr cmp. They look like hieroglyphics, and there's a question in the back of your mind nobody ever answers:

What are these people actually doing? What are they "reversing"?

Here's the one-sentence answer up front; the rest of this post is just unpacking it:

The source code you wrote gets "flattened" when it's compiled, and a lot of stuff is thrown away in the process. Reverse engineering is taking that flattened product and putting back as much of what was thrown away as you can.


Hold on — what is this skill actually good for?

Before the theory, here's a reason to keep reading. Reversing isn't just something they do in hacker movies; it solves one particular class of problem: you have a program, you don't have its source code, and you need to know what it does.

Real-world scenarios:

  • Security research / vulnerability hunting — the vendor won't hand you the source, but you still have to find where they got it wrong
  • Malware analysis — you caught a suspicious file; what is it stealing, and where is it phoning home?
  • Software compatibility and maintenance — the company has a fifteen-year-old system, the author left, the docs are gone, the source is gone, and the thing is still running
  • Checking what you actually bought — is that app quietly shipping your data somewhere in the background?
  • CTFs and security competitions — reversing is a permanent fixture as a challenge category
  • Pure curiosity — how did they pull that feature off?

The common thread is the "black box". Reversing is the technique for prying it open.


Something you do every day without noticing

You write code like this:

int check_password(char *user_input) {
    char *correct = "hunter2";
    if (strcmp(user_input, correct) == 0) {
        return 1;
    }
    return 0;
}

(There's a reason I'm using C: C compiles down to machine code the CPU executes directly, which demonstrates this post's point best. Languages like Python and JavaScript work rather differently — they have their own arrangements, and we'll leave them aside for now.)

You hit compile, and the computer hands you an executable.

Question: is the name check_password still in that executable? Is the variable name correct?

The answer: it depends — and the two names don't even share the same fate.

A function-local variable name like correct usually isn't there in an ordinary release build without debug information. check_password, on the other hand, might still be around (sitting in the symbol table), or it might have been removed later in the build.

But the important part isn't "is it there or not" — it's this: when the CPU executes the program, it doesn't rely on those names at all. They're entirely optional as far as execution is concerned, and that is precisely why they can be thrown away.

They were never for the computer. They were for you.


In the computer's world there are no "names"

This is the bedrock of the whole subject, so let me take it slowly.

When you write code you use names: user_input, check_password, correct. Those names let you understand what the program is doing.

But the CPU doesn't need names. The CPU only recognises three kinds of thing:

  1. Locations — "go fetch whatever is in slot number 5000"
  2. Registers — the handful of small drawers the CPU keeps at arm's length for numbers it's working on
  3. Actions — "add these two numbers", "if they're equal, jump to slot 3000"

Notice there are no names in that list. As far as the CPU is concerned, check_password isn't a name; it's "that run of instructions starting at slot 4384".

The variable correct is in a slightly more interesting position. correct itself is a signpost (it records where the text lives), while "hunter2"the text itself — sits somewhere else entirely. After compilation the signpost might live in one of the CPU's little drawers, might live in some scratch area, or the compiler might notice "we don't actually need this signpost at all" and drop it. The text, meanwhile, typically ends up in an area dedicated to storing fixed data.

That distinction may look like a fine detail right now, but it matters — later, when we talk about "why tools can dredge strings out of a binary", that area is exactly what they're dredging.

So what compilation does is fundamentally translation:

What you wrote (for humans)       After compiling (for the CPU)
──────────────────────────────────────────────────────────────
check_password               →    the instructions at 0x1120
user_input                   →    a value in a register
correct (the signpost)       →    a value in a register or a
                                  scratch slot — or dropped
                                  entirely if the compiler
                                  decides it isn't needed
"hunter2" (the text itself)  →    usually a read-only data area
if a == b                    →    "compare, then jump"

Once the translation is done, those names are useless. The CPU runs perfectly well on locations and registers; the names can be dumped wholesale.

Let me clear up a common misconception here: compiling and "removing names" are not the same thing.

After compilation the CPU no longer needs the names — but the names don't necessarily vanish immediately. Plenty of build pipelines keep symbols around (for the linker, for the debugger), so the executable you end up with may still carry some of them.

strip is one common way of removing that information (the name means exactly what it sounds like) — it clears out symbols and debug information that aren't needed at runtime, making the file smaller. But it isn't the only reason names disappear: depending on build settings, the compiler and linker may never have preserved certain names in the first place.

So a name can meet several fates: it may never have been written into the file at all, it may survive in the symbol table or debug information, or it may be removed at link time or by strip.

"Does it have debug info?", "has it been stripped?", "is this a release or a debug build?" — these are related but distinct questions. Don't collapse them into one. The habit worth building is: when you get a file, look at what's actually left in it, rather than assuming what should be left in it.


An analogy to nail this down

Picture a recipe.

Compiling = tearing off the labels, keeping the exact locations Same recipe: the human version has meaning, the machine version has only coordinates Source code (for humans) Take the “main ingredient” (chicken breast), 200 g ← the label tells you what it is Add “marinade A” (soy sauce, sugar, rice wine) ← the parentheses are for humans Rest for 30 minutes You understand what it does compile rip off labels Executable (for the CPU) Take 200 g from cabinet 3, slot 2 Exact location, unknown contents Add jar 5 (???) No way to see what's in the jar Rest for 30 minutes You can follow it without understanding it Key: with the labels gone, the information isn't fuzzy — it's exact but meaningless Reversing = taking the right-hand version and inferring every “what is it” from the actions and their order
Figure 1: Compiling is like tearing every label off a recipe — the locations remain, the meaning is gone.

What's the difference between the original (the human-readable version) and the version with the labels torn off? Let's go step by step.

The source-code version of the recipe reads like this:

"Take 200 g of the 'main ingredient' (chicken breast), add 'marinade A' (soy sauce, sugar, rice wine), and let it rest for 30 minutes."

The parenthetical bits, plus labels like "main ingredient" and "marinade A", exist so the reader can understand what's going on.

The compiled recipe has had every label torn off, and now reads:

"Take 200 g from cabinet 3, slot 2, add jar 5, and let it rest for 30 minutes."

You can still follow it (the locations, quantities and order are all precisely there), but you have no idea what's in cabinet 3 slot 2, or what's been mixed into jar 5.

Note the point of the analogy: after the labels come off, the remaining information hasn't become vague — it has become precise but meaningless. The machine needs "where to go get it"; the human needs "what it is". Compilation throws away exactly the latter.

That is what a compiled program is: every action is present, it runs fine, and every label that would help you understand it has been ripped off.

And reverse engineering is taking that label-less recipe and working out, from the actions and their order, what each "that thing" actually is.

  • See "let it rest for 30 minutes" → guess this step is marinating
  • See "the colour turns brown after adding jar 5" → guess jar 5 is soy sauce
  • Step by step, stick the torn-off labels back on

That's all reversing is. Everything else is detail.


So what "labels" does compilation actually throw away?

Not just names. The "flattening" that compilation performs discards things at four levels, from shallow to deep:

The four layers compilation throws away The deeper you go, the harder to rebuild; the bottom layer can never be uniquely recovered ① Names userPassword → a slot on the stack checkPassword → an address rename them yourself ② Types Is this run of bytes an integer? Text? An address? Machine code doesn't say infer from usage ③ Structure if / while / for / switch → mostly become “compare + conditional jump” rebuild from the jumps ④ Intent Comments, naming taste, the reasons behind “why design it this way” never uniquely recoverable rebuild difficulty Reversing = rebuild ①②③, then use them to infer ④ One piece of machine code can map to several original sources and motives; the product alone can't tell them apart
Figure 2: The four layers compilation throws away. Tools mainly help with ①②③; ④ is forever a human's job.

Let me map each layer onto your programming experience, one at a time:

Layer 1: names (the easiest to grasp)

check_password → gone, now an address. user_input → gone, now a value in a register.

What you do when reversing: give every nameless thing a name of your own. In your tool you'll rename "that chunk at 0x1120" to check_password, because you've worked out what it does.

Layer 2: types (the most counter-intuitive layer)

You write int age = 25; and you know it's an integer. You write float price = 25.0; and you know it's a float. The compiler knows too, because you told it. But after compilation, that information isn't written into the executable.

Why not? Because the CPU doesn't need it. The CPU only needs to know whether to "do integer addition on these 4 bytes" or "do floating-point addition" — and that information lives in the instructions, not in the data.

Let's get concrete. Say you find these 4 bytes in an executable:

68 65 20 66

What is it? At least four possibilities:

  • An integer
  • A floating-point number
  • An "address that points somewhere else"
  • Four characters (h, e, space, f)
One run of bytes, four possible identities Machine code carries no type information — you infer it from how the bytes get used 68 65 20 66 4 bytes, that's all An integer if it's used in addition → supports the guess A float if it's used in FP math → supports the guess An address if used to “fetch from there” → supports the guess Four characters h e ␣ f easiest to spot by eye Why does “go look for strings” work so often? Because text is the one identity you can recognise at a glance A run of consecutive printable bytes jumps out when a human skims it — that's exactly how strings works Types aren't read off, they're inferred — and inference “supports”, it doesn't “prove”
Figure 3: The multiple identities of one run of bytes. Types are discarded at compile time and can only be inferred from usage.

From those 4 bytes alone, you can't tell. They're just four numbers.

So what do you do? You look at how they're used.

If the program feeds those 4 bytes into an "addition" → that supports the hypothesis that it's a number.
If the program uses it to "compare character by character against another chunk" → that supports "it's text".
If the program uses it "as an address and fetches something from there" → that supports "it's an address".

Note the word: "supports", not "proves". The same chunk of data being used in several places, or treated as several different things, genuinely happens. Almost every judgement in reversing is "the most plausible explanation so far", not hard proof.

This is one of the core moves in reversing: types aren't read off, they're inferred.

First, the good news: text (strings) is the easiest identity to recognise of the lot, because a run of consecutive bytes that all happen to fall in the "printable characters" range simply looks like a sentence — a human glancing at Please enter your password knows instantly it isn't random numbers.

That's why there are tools dedicated to finding exactly those fragments (strings, which we'll get to), and why "go look for strings" is so often the opening move in reversing.

While we're here, let me be clear about what that tool actually does, so you don't imagine it's magic: it scans the file and prints out any fragment where several consecutive bytes happen to fall in the printable range. It doesn't understand file formats, and it doesn't care whether those bytes were originally a string — so it will miss text that's been split up or encoded, and it will dredge up piles of noise that merely happens to look like text. It's a very crude but very cheap sieve.

The hard part is the other types: are these 4 bytes an integer, a float, or an address? Is this pile of bytes an array, or a struct with several fields? No tool can tell you with certainty; you can only infer from "how it gets used".

Layer 3: structure (your if and while are gone)

The if, while, for and switch you wrote all turn into the same thing after compilation: "compare, then jump".

What you wrote            After compiling
──────────────────────────────────────────────────────────────
if (x > 5) {...}     →    compare x with 5; if not greater, skip below
while (x < 10) {...} →    compare x with 10; if less, jump back up

See it? In the common, not-heavily-optimised form, if and while both frequently become "compare + conditional jump" — the difference is which way the jump goes.

But jump direction on its own is not a reliable rule — compilers do loop rotation, move the test to the bottom, and reorder blocks, so the direction of a jump may not match the structure your intuition expects. It's a clue, not a law.

And even that is only the most common form, not the only one. If a switch has many branches the compiler may switch to a "jump table" (look up where to jump), which looks nothing like a chain of comparisons; a very short if may be replaced by a "don't jump, just pick one of two values" instruction. The harder you crank up optimisation, the more variation you'll see.

What you do when reversing: rebuild what the original control flow looked like from the actual jump relationships.

Layer 4: intent (this one never comes back)

Your comments, the good names you chose, the reasons in your head for "why design it this way" — after compilation these cannot be uniquely recovered. The same machine code can correspond to several different original implementations and motives, and you can't tell them apart from the product alone.

This layer is where reversing gets genuinely hard, and where machines can't help you. Tools can get you seventy or eighty percent of the way on the first three layers, but "why is this code written this way, what was the author thinking" comes down to your own experience and reasoning.


So here's the definition of reverse engineering

Putting all of the above together:

Compilation is a "lossy compression" process — it squashes the source code you wrote, full of names and structure, into a pile of machine code that only has locations and actions, throwing away names, types, structure and intent along the way.

Reverse engineering is rebuilding as much of "what was thrown away" as possible from "what's still there", in the absence of the source code.

"Lossy compression" is a very precise term. Just like JPEG shrinks a photo by discarding some detail, and blowing it back up never restores the original quality — compilation is the same.

What you reverse out will never be identical to the original source (you chose the names, you rebuilt the structure). And the more honest way to put it is: what you build is a model that "adequately explains the behaviour you've observed", not a copy of the source. That model may fail under conditions you haven't observed yet — which is why a reversing conclusion should always come with the caveat "as far as I've seen so far".


So why is there anything "left behind" to work with?

You might ask: if compilation throws away all the names, isn't reversing hopeless from the start?

Here's the key — not everything can be thrown away.

There's an iron rule:

Anything that "has to be supplied from outside when the program runs" leaves clues that are hard to scrub away — because if you scrubbed them, nobody would know where to go get it.

A few examples:

If the program was packaged the usual "dynamically linked" way, the names of the functions it asks external libraries for often stick around.

For instance: your program wants to "compare two strings", so it uses an off-the-shelf function called strcmp. That function's code isn't in your executable — it's in a shared system library — so when the program runs, something has to find strcmp for it. The component responsible (the loader) needs the name in order to find it, so that name usually survives.

That makes "which capabilities the program asks the outside world for" a very handy checklist, and it leaks a huge amount of information:

  • Asks for strcmp / strncmp (string comparison) → first suspect: there's a comparison here; it may be validating something
  • Asks for crypto-related functionality → first suspect: there's encryption, and a secret may be hidden
  • Asks for network-related functionality → first suspect: it goes online; go see where it connects
  • Asks for anti-debugging functionality → first suspect: it's defending itself against analysis

From that list alone, without reading a single line of code, you can already guess roughly what the program does — the highest return-on-investment move in all of reversing.

But mind its limits — the list only covers capabilities the program "asks the outside for at runtime". Some things never show up on it:

  • The program bakes the functionality straight into itself (this is called static linking)
  • The compiler inlines small functions directly into the code
  • The program reimplements it itself rather than borrowing from the system
  • The program only borrows it on the fly at runtime

So the correct framing is: this list is "a starting point for forming hypotheses", not "a complete inventory of capabilities". See strcmp and boldly assume there's string comparison; but not seeing any crypto functions doesn't let you conclude there's no encryption. That sense of proportion matters, and later posts will keep coming back to it.


Why does "looking for strings" work so often?

If you've dabbled in reversing, or watched someone else do it, you'll have noticed a common first step: run a tool called strings to find runs of printable bytes within whatever it scans.

Why does this work so often?

Because plenty of secrets are just plaintext strings, and strings (unlike names) often do stay in the program, because the program needs them at runtime (displaying messages, comparing passwords, connection URLs).

So "run strings and look for text" isn't laziness — it's the highest-ROI first step, and you should try it first.

But it fails in three situations, and understanding those three failures is exactly what shows you the difficulty ladder of reversing:

strings is a very crude, but very cheap, sieve It only finds “runs of printable bytes”; it doesn't understand file formats What's in the file Plaintext strings Chopped-up strings Encrypted data Raw numbers / code strings consecutive printable bytes? no format awareness ignores original type Caught ✓ Plaintext strings, plus noise that looks like text Missed ✗ Chopped up (you get half a sentence) Encrypted / encoded (a pile of garbage) Secrets that were never strings Every kind of “miss” is telling you to switch viewpoint: Half a sentence → read the code, pick up the tail A pile of garbage → read how it decrypts Nothing at all → the secret may be the algorithm itself; rebuild the whole logic
Figure 4: What strings can and can't do. It's the first move in reversing, and the first rung on the difficulty ladder.

① The string has been chopped up and hidden — sometimes the compiler or the author splits a string into several pieces, and strings only catches one of them. You end up with half a sentence.
At this point you're forced down a level: go look at the program's actual instructions and put the fragments back together.

② The string is encrypted — the secret isn't sitting there in plaintext; it's encrypted and hidden, and only decrypted at runtime. strings catches nothing but garbage, and now you have more work to do: understand how it decrypts, then run that decryption yourself.

③ The secret isn't a string at all — sometimes the secret is an algorithm, for example "how to decide whether a serial number is valid". There's no string to catch; the secret lives in the computation.
Here you have to rebuild that algorithm in full. This is the hardest rung, and where real reversing skill lives.

So "all I can do is run strings" isn't a lack of ability — it just means you haven't yet hit a challenge that forces you up the ladder. And knowing "what those higher rungs are" is the map this post is trying to hand you.


Why are there so many tools? (the one-line version)

strings, objdump, Ghidra, radare2, gdb… why so many? It makes your head spin when you're starting out.

Because they answer different questions. The moment you classify them by "what question does this tool answer", the confusion evaporates. Here's the coarsest split; details are saved for the methods post:

"What does this program declare or ask the outside world for?" — use symbol and metadata inspectors like nm and readelf. Loads of intelligence without reading a single line of code, so do this first.

"Is there anything in the file that looks like text?" — use strings. Note how it differs from the previous category: it doesn't understand file formats, it just sweeps through looking for runs of printable bytes. Crude, but dirt cheap.

"What are the actual instructions in this chunk of code?" — use a disassembler (like objdump), which translates that pile of ones and zeros into CPU instructions. This is the layer closest to the raw evidence (the truly 100%-interpretation-free thing is the bytes themselves, but humans can't read those), the toughest to read, and also the hardest to be misled by.

"What does this logic say in plain language?" — use a decompiler (like Ghidra), which takes those rigid instructions and reconstructs something resembling C as best it can. Far easier to read — but note that "as best it can" means it's guessing, and sometimes it guesses wrong.

"What does this program actually do when it runs?" — use dynamic analysis tools (like gdb or Frida). The previous three are all "don't run it, just look"; this one is "actually run it and intercept and observe along the way".

One very important trade-off principle to keep in mind:

The more primitive the tool, the more trustworthy (but harder to read); the higher-level the tool, the easier to read (but the more it can lie to you).

So the veteran's approach is: use the high-level tool (Ghidra) to grasp the big picture quickly, and whenever something reads strangely, go back to the low-level tool (objdump) to verify — because the lower the level, the less guesswork has been added.


One diagram to sum it up: what reversing is

You write source code          names, types, structure, intent
    │
    │  compile (lossy compression — labels torn off)
    ▼
Executable                     only locations, actions, and
                               "whatever clues survive"
    │
    │  reverse (use surviving clues to infer what was thrown away)
    ▼
Your reconstructed             names you chose, types you inferred,
understanding                  structure you rebuilt
                               (enough to explain the observed
                                behaviour, but not necessarily
                                equivalent to the original source)

Reversing isn't magic, it's archaeology. You're looking at a finished product with its labels torn off, and from the clues that remain (the actions, their order, the capabilities borrowed from the system, the strings nobody bothered to hide) you piece the original meaning back together, fragment by fragment.


The mindset you should have now

If you've never touched reversing before, what you should take away from this post is not a single tool command, but these three ideas:

  1. Compilation throws things away; reversing is picking them back up. Names, types and structure are gone, but the functionality is still there.
  2. Some things are hard to throw away, and those are your clues — the list of capabilities borrowed from the system, plaintext strings, the order of the program's actions — but remember they're a "starting point", not "the whole picture".
  3. There are no better or worse tools, only division of labour. Read the list, read the instructions, read the plain-language version, watch it run — each answers a different question.

With those three ideas, any "reversing article full of hex and mov" will stop looking like hieroglyphics — you'll know the author is just sticking the torn-off labels back on.


Next up: the methods post

With this conceptual foundation in place, the methods post will have you actually touch assembly for the first time. You'll see with your own eyes:

  • What a "variable" really looks like down at the bottom (globals, parameters, locals, constants — four completely different things)
  • Why strings sometimes only gets you "half a sentence" (with a real example)
  • How to choose between objdump, radare2 and Ghidra, and when to reach for which
  • A repeatable "opening routine for analysing an unfamiliar binary"

By then you'll find that, thanks to this groundwork, all that assembly doesn't look the least bit frightening.


These are my introductory notes on reverse engineering, written for people who want to get started but were scared off by assembly. Corrections welcome.