Visual walkthrough — Disassembly — objdump, reading assembly
We will use exactly one function the whole way:
int add(int a, int b) {
return a + b;
}Everything below is built from zero — we define every box, arrow, and word before using it.
Step 1 — The two languages, and the bridge between them
WHAT. A .c file is text a human types. The CPU cannot read text; it reads machine code
— a stream of numbers. In between sits the assembler: a fixed rulebook that turns short
word-like names (mnemonics) into those numbers. Disassembly walks that rulebook backwards.
WHY. Before we can read a single assembly line we must agree on what a line even is: it is
one row in a three-way translation. If we skip this, every later symbol (rbp, edi, 0x8)
floats with no home.
PICTURE. Look at Step-1 figure. The orange arrow (left→right) is what the compiler + assembler does. The teal arrow (right→left) is what objdump does. The
middle column — the numbers 55 48 89 e5 ... — is the only thing the CPU ever sees. Assembly
(the right column) is a human costume worn over those exact bytes.

Step 2 — The CPU's tiny boxes: registers
WHAT. A register is a small named box inside the CPU that holds one number. It is not
in RAM; it is right where the arithmetic happens, so it is the fastest storage there is. On
x86-64 the boxes we care about here are rax, rbp, rsp, rdi, rsi, rdx.
WHY. Our function must add a + b. Addition happens between registers. So before values can
be added, they must first arrive in registers. Every line of our assembly is really about
moving numbers into and between these boxes. You cannot read the derivation without a
mental picture of the boxes — so here they are, drawn.
PICTURE. Step-2 figure shows six boxes. Note the nesting: eax is the lower 32 bits of
the 64-bit rax. The names r.. (64-bit) and e.. (32-bit) are the same box viewed at two
widths. Because our int is 32 bits, the code will use the e-named views.

Step 3 — The contract: where do a and b arrive?
WHAT. When something calls add(3, 4), where does the 3 go, and where does the 4 go?
This is not a choice the compiler makes freely — it obeys a calling convention, a fixed
agreement so caller and callee understand each other.
WHY. Without an agreed rule, the caller might leave arguments in rdi while the function
looks for them in rsi — chaos. The rule (System V x86-64, used on Linux/macOS) says:
integer arg 1 → rdi, arg 2 → rsi, and the return value comes back in rax. We must
know this before line 4 of the disassembly makes sense.
PICTURE. Step-3 figure: two incoming values slide into edi and esi (the arg slots), and a
dashed teal arrow shows the result later leaving through eax. Learn this shape — it is the same
for every function you will ever disassemble.

Step 4 — The prologue: carving out a frame
WHAT. The first two instructions are always the same at -O0:
0: 55 push rbp
1: 48 89 e5 mov rbp, rspWHY. A function needs a stable "home base" address to find its own local variables, even as
the stack grows and shrinks. rbp (base pointer) is that home base. But rbp currently
holds the caller's home base, so we (1) save it by pushing it onto the stack, then (2) copy
the current stack top (rsp) into rbp to make a fresh base. This pair is the function
prologue.
PICTURE. Step-4 figure: the stack drawn as a downward stack of boxes (addresses shrink going
up — that is the x86 convention). push rbp writes the old base and drops rsp by 8 bytes; then
rbp is planted at the new top. Term-by-term:

Step 5 — Spilling arguments to the stack
WHAT.
4: 89 7d fc mov [rbp-0x4], edi ; store a
7: 89 75 f8 mov [rbp-0x8], esi ; store bWHY. At -O0 ("no optimization") the compiler is deliberately literal: it gives every
variable a real home in memory so a debugger can always find it. So it copies a (in edi) and
b (in esi) out of registers into stack slots. This copying-to-stack is called a
spill.
PICTURE. Step-5 figure: rbp is the anchor at offset 0; [rbp-0x4] is 4 bytes below it
(one int), [rbp-0x8] is 8 bytes below (the next int). The square brackets mean "the memory
at this address", not the address itself. Reading the operand:

Step 6 — Load back and add
WHAT.
a: 8b 55 fc mov edx, [rbp-0x4] ; edx = a
d: 8b 45 f8 mov eax, [rbp-0x8] ; eax = b
10: 01 d0 add eax, edx ; eax = eax + edx = b + aWHY. Arithmetic works register-to-register, so the just-spilled values must be loaded back
into registers. The compiler pulls a into edx and b into eax, then adds them. It chose
eax for b on purpose: the sum lands in eax, which — from Step 3 — is exactly where the
return value belongs. So no extra move is needed afterward.
PICTURE. Step-6 figure: two loads (memory → register, teal arrows) then one add that folds
edx into eax (orange). Term-by-term on the add:

Step 7 — Epilogue: tear down and return
WHAT.
12: 5d pop rbp ; restore caller's base
13: c3 ret ; jump back to callerWHY. We borrowed rbp; we must give the caller theirs back. pop rbp reads the value we
saved in Step 4 and bumps rsp back up — undoing the prologue exactly. Then ret pops the
return address (put on the stack by the original call) and jumps to it. The answer is
already sitting in eax, honoring the contract from Step 3.
PICTURE. Step-7 figure: the stack un-winds — rsp climbs back to where it started, rbp
returns to the caller's base, and control flies back along the teal arrow with eax carrying the
result.

Step 8 — The degenerate/optimized case: it all collapses
WHAT. Recompile with -O2 and the entire eight-instruction story shrinks to two:
0: 8d 04 37 lea eax, [rdi+rsi]
3: c3 retWHY. With optimization on, the compiler proves the local variables and stack frame are
never needed: nothing is debugged, nothing outlives the call. So it deletes the prologue, the
spills, the reloads, and the epilogue. It then uses lea — an instruction built to compute an
address like rdi + rsi — as a sneaky one-shot adder that writes straight into eax. This is
the edge case you must recognize: the same C, radically different bytes.
PICTURE. Step-8 figure: the tall -O0 tower on the left, an arrow labelled "optimizer", and
the tiny two-line -O2 result on the right — with dotted crossings-out over everything that got
removed.

The one-picture summary
Everything above, on one canvas: C on the far left, the two register arg-slots feeding in, the
-O0 shuffle through the stack, the value landing in eax, and the -O2 shortcut drawn as a
dotted express lane that skips the whole detour.

Recall Feynman retelling — the whole walkthrough in plain words
Two numbers show up at the front door in two labelled cubbies called edi and esi.
The function first nails a little signpost (rbp) into the ground so it can find its own stuff,
then — because we told the compiler don't be clever (-O0) — it carefully copies both numbers
into two floor tiles just below the signpost. A moment later it picks them right back up, one
into edx and one into eax, and adds them. It deliberately used eax for the answer because
eax is the "outbox" everyone reads when the function is done. Finally it pulls up the signpost,
puts the ground back the way it found it, and jumps home carrying the answer in eax.
When we flip the switch to be clever (-O2), the compiler realizes all that copying was
pointless, throws it away, and adds the two cubbies straight into the outbox with a single
address-math trick (lea). Same answer, one-quarter the instructions.
Connections
- Disassembly — objdump, reading assembly (index 5.3.17) — the parent; this page derives its central example from zero.
- Calling Conventions (System V x86-64) — Step 3's
edi/esi/eaxcontract. - Stack Frames & The Stack Pointer — Steps 4–7 (prologue, spills, epilogue).
- Compiler Optimization Levels — Step 8's
-O0vs-O2collapse. - Compilation Pipeline — the forward arrow disassembly reverses.
- Debugging with GDB — why
-O0keeps everything on the stack. - ELF Object Files & Sections — the
.textsection these bytes live in. - CISC vs RISC — why x86 bytes are variable length and
leacan do math.