Bridge: Your First C, Disassembled
main() → objdump → every instruction named
- › Compile a C program and read the assembly it produced, instruction by instruction
- › Recognize the prologue, the body, the return value, and the epilogue on sight
- › Explain why an int return lands in eax and where a function's arguments arrive
- › See that -O2 changes the instructions but not the meaning — the compiler is a translator
The payoff moment
For six days you've been learning the machine's own vocabulary — bits, registers, memory, mov, addressing. Today it pays off: you write C, compile it, and read the assembly it becomes, naming every single instruction. After this, C is no longer a magic language that "runs." It's a *convenient way to write the assembly you already understand*. That shift is the whole point of the spine.
We're not learning C's syntax today (that's week 3). We're proving one claim: there is nothing in a small C program you can't already read at the machine level.
The smallest real program
Here is about the smallest C program that does something:
1int main(void) {2 return 7;3}Compile it *without optimization* (so the compiler translates literally instead of being clever), then disassemble:
1gcc -O0 -g ret7.c -o ret72objdump -d --disassemble=main -M intel ret71main:2 push rbp ; (prologue) save the caller's frame pointer3 mov rbp, rsp ; (prologue) rbp now marks this function's frame4 mov eax, 7 ; (body) put the return value 7 into eax5 pop rbp ; (epilogue) restore the caller's frame pointer6 ret ; (epilogue) pop the return address into rip → go backEvery line, named
- `push rbp` / `mov rbp, rsp` — the prologue. Every function does this to set up its own
- little region of the stack. You'll take this fully apart in week 2 (stack frames); for now, read
- it as "the function is opening for business."
- `mov eax, 7` — the body. Our whole program was
return 7, and here it is: put 7 in - eax. A function's integer return value travels back to the caller in rax (this is the
- calling convention — day 13). Why eax, the 4-byte view, not rax? Because
intis 4 bytes. - The size of the C type picked the register width — exactly the idea from day 6.
- `pop rbp` / `ret` — the epilogue. Undo the prologue, then
retpops the return address - off the stack into rip and control jumps back to whoever called main. (That return address, and
- who owns it, is where security gets interesting — weeks from now.)
ret. Almost every compiled function has exactly this skeleton. Once you see
the skeleton, the body is just the mov/add/cmp/jmp you already know.A function that actually computes
Now one with inputs:
1int add(int a, int b) {2 return a + b;3}1add:2 push rbp3 mov rbp, rsp4 mov [rbp-4], edi ; save arg a (arrived in edi) into a local slot5 mov [rbp-8], esi ; save arg b (arrived in esi) into a local slot6 mov eax, [rbp-4] ; load a7 add eax, [rbp-8] ; eax = a + b8 pop rbp9 ret ; return value already in eaxRead it and everything is familiar. The two arguments a and b arrived in edi and
esi — the first two integer-argument registers of the calling convention (day 13 makes this
precise; here just notice *arguments come in specific registers*). At -O0 the compiler dutifully
spills them to stack slots ([rbp-4], [rbp-8] — base + displacement, straight from day 6), loads
one back, adds the other, and leaves the result in eax to be returned. Nothing here is new to
you.
The compiler is a translator with opinions
Compile the *same* add with optimization on (gcc -O2) and it collapses to:
1add:2 lea eax, [rdi + rsi] ; eax = a + b, in one instruction, no stack at all3 retSame function, completely different instructions — yet the *meaning is identical*: return a + b.
The compiler noticed it didn't need the stack at all and used lea (load effective address) to
compute rdi + rsi as plain arithmetic. This is the mental model to keep: the compiler is a
translator that is allowed to rephrase your program, as long as the observable behaviour is the
same. -O0 shows you the literal translation (best for learning); -O2 shows you what actually
ships. Reading both for the same code teaches you what the optimizer does.
int answer(void) { return 42; }, compiled at -O0. You already saw ret7. Predict the one body
instruction (the register and the value).objdump -d -M intel <binary> disassembles everything; add -g when compiling
and use a debugger later to line C up with assembly. Or paste the C into godbolt.org (Compiler
Explorer) to see C and assembly side by side, colour-matched — an incredible way to build this
skill fast.Name every instruction
Write three tiny C functions in bridge.c: (1) one that returns a constant, (2) add(int,int) returning a+b, (3) one with a single local int that it sets and returns. Compile at -O0 with `gcc -O0 -g`, run objdump -d -M intel, and in a comment beside each function's disassembly, label EVERY instruction as prologue / body / epilogue and say in plain words what it does. Then recompile at -O2 and note, in a sentence per function, what the optimizer removed. Everything you need is in this lesson; you are only reading and naming, not writing assembly.
$ gcc -O0 -g bridge.c -o bridge_o0$ objdump -d -M intel bridge_o0$ gcc -O2 bridge.c -o bridge_o2$ objdump -d -M intel bridge_o2- › You labelled prologue / body / epilogue for each function
- › You found the return value being placed in eax (or rax)
- › You spotted the arguments arriving in edi/esi for add()
- › You can name one thing -O2 removed compared to -O0