JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Reading Disassembly of Your Own Code

You have learned what the CPU does, how the stack works, and what a calling convention promises. Now you put it all together by doing the one thing that makes assembly real: taking a tiny C function you wrote, asking the compiler to show you the machine code, and reading it line by line until it stops being a wall of hex and becomes a story you can follow.

What disassembly is, and why you would want it

Across this rung you have built up every piece you need: a register is a tiny named drawer the CPU computes in, rip walks through instructions, rsp marks the top of the stack, and a calling convention fixes who passes arguments where and who cleans up. Disassembly is simply the act of taking the finished machine code — the raw bytes the CPU actually runs — and translating each one back into the human-readable assembly mnemonic it stands for. It is the reverse of what the assembler did: the assembler turned `mov rax, 0` into bytes, and the disassembler turns those bytes back into `mov rax, 0`.

Why bother, when you wrote the C in the first place? Because the compiler does not run your C — it runs its translation of your C, and the gap between the two is where real understanding lives. Reading the disassembly is how you finally see what your C actually became: which lines vanished, which loop got unrolled, where a function call really happens, why a variable lives in a register instead of memory. It is also where you go when a debugger stops on a crash with no source available, when a bug only appears at `-O2`, or when you simply want to stop trusting and start knowing what the machine does.

Getting the bytes to show themselves

There are three everyday ways to see disassembly, and it is worth knowing all three because each shows a slightly different view. First, ask the compiler to emit assembly directly instead of an executable: `gcc -O0 -S add.c` writes `add.s`, the human-readable assembly before it ever became bytes. Second, disassemble a finished object file or program with objdump: `objdump -d a.out` walks the machine code and prints each instruction next to its raw bytes. Third, inside a debugger like gdb, `disassemble main` shows the live instructions of a running program, with an arrow on the one about to execute.

One choice matters more than any other for a first read: turn the optimizer OFF, with `-O0`. At `-O0` the compiler translates your C almost literally — every variable gets a home on the stack, every statement maps to a recognizable clump of instructions, nothing is reordered or deleted. The code is bigger and slower, but it reads like a faithful transcript of your source. At `-O2` the compiler is brilliant and ruthless: it keeps values in registers, deletes dead code, merges and reorders, and the result no longer lines up with your lines. Learn to read `-O0` first; graduate to `-O2` once the vocabulary is fluent.

Walking through one tiny function

Let us read something real. Take the smallest interesting function — one that takes two integers and adds them — and compile it for x86-64 with no optimization. Everything you learned this rung is about to appear in eight lines, and you will recognize all of it. Here is the C alongside the `-O0` disassembly in Intel syntax, with the raw bytes on the left so you can see this is the actual machine code.

int add(int a, int b) { return a + b; }

         bytes            instruction          what it does
add:
 55                push   rbp                ; save caller's base pointer
 48 89 e5         mov    rbp, rsp           ; rbp = frame anchor (prologue)
 89 7d fc         mov    [rbp-0x4], edi     ; spill arg a (edi) into the frame
 89 75 f8         mov    [rbp-0x8], esi     ; spill arg b (esi) into the frame
 8b 55 fc         mov    edx, [rbp-0x4]     ; edx = a
 8b 45 f8         mov    eax, [rbp-0x8]     ; eax = b
 01 d0            add    eax, edx           ; eax = a + b   (result in eax)
 5d               pop    rbp                ; restore caller's rbp (epilogue)
 c3               ret                       ; pop return address into rip, jump back
A four-character C function as the CPU sees it: prologue, the arguments arriving in edi and esi by the calling convention, the add, the epilogue, and ret.

Read it top to bottom and name each part. The first two lines, `push rbp` then `mov rbp, rsp`, are the prologue from guide 3 — every function opens by saving the old base pointer and anchoring a new stack frame. The next two `mov` lines copy the incoming arguments out of the registers the calling convention delivered them in — `int a` arrived in edi, `int b` in esi, exactly the rule from guide 4 — and spill them into the frame at `[rbp-0x4]` and `[rbp-0x8]`. (At `-O0` the compiler always parks arguments in memory, which is why the code is so literal.) Then it loads them back into eax and edx, runs the single `add` from the arithmetic family, and leaves the sum in eax.

The last two lines close the story. `pop rbp` is the epilogue, undoing the prologue by restoring the caller's base pointer. Then `ret` does the single most important thing in the whole listing: it pops the return address that `call` had pushed off the stack and jumps rip back to it — the exact mechanism from guide 3 that lets a function return to whoever called it. And where is the answer? In eax. That is the calling convention again: by agreement, an `int` return value comes back in eax, so the caller knows to look there. Eight instructions, and every one of them was something you already learned the name of.

A method you can run on anything

The point of that walk-through was not the `add` function — it was the procedure. Most disassembly you meet looks intimidating only until you sort it into the same few buckets. Here is a reading method that turns almost any function listing into something you can narrate, even one you did not write.

  1. Find the boundaries first: the prologue (push rbp / mov rbp, rsp, or a sub rsp, N that carves out local space) marks the start, and ret marks the end. Everything between them is one function's body.
  2. Locate the arguments: by the calling convention, the first integer/pointer args live in rdi, rsi, rdx, rcx, r8, r9 on entry. Track where each one gets moved or stored to follow a value through the function.
  3. Sort each instruction into a family: data movement (mov, lea, push, pop), arithmetic/logic (add, sub, and, shl), comparison and branching (cmp, test, jmp, je, jne), or a call to another function. You met all of these as the common instruction families.
  4. Trace control flow by the jumps: a cmp/test followed by a conditional jump is an if or a loop; a backward jump target is almost always the top of a loop. Sketch the arrows and the shape of your C reappears.
  5. Read the return value out of eax/rax just before ret, and you have the function's output. Now you can state, in one sentence, what it computes.

Honest limits, two architectures, and why this pays off

Be clear-eyed about what reading disassembly does and does not give you. Everything in the listing above was x86-64; the very same `add()` compiled for an ARM chip — your phone, a recent laptop — produces different mnemonics and a different register set (x0, x1, w0 instead of edi, esi, eax), because the instruction families are universal but the exact assembly is per-architecture. That is the lesson from guide 1 made concrete: the ideas — registers, a stack, a calling convention, prologue and epilogue — carry across, but the spelling does not. Knowing this keeps you from over-generalizing from one disassembly to all machines.

There is a second honest limit. Reading disassembly tells you precisely what the machine does, but never why you wanted it done — that intent lived in the source, the comments, and your head, and the compiler threw it away as it lowered everything toward bytes. So disassembly is a powerful tool for the right questions ('does this loop actually call malloc()?', 'why does this crash here?', 'did the optimizer keep my null check?') and a poor one for the wrong questions ('what was this program for?'). Use it where the machine's exact behavior is the thing in doubt; reach for the source everywhere else.

That is the whole rung resolved. You started not knowing what a register was; you end able to take a function you wrote, ask the compiler to show its true face, and read every instruction back into the ideas you now own — the prologue building a frame, the convention handing arguments in fixed registers, the arithmetic doing the work, and `ret` following a return address home. You no longer have to take the compiler's word for anything. When someone says 'the machine just does X,' you can open the disassembly and check — and that habit, more than any single fact, is what it means to work close to the metal.