roadmap
week 1 · day 7

Bridge: Your First C, Disassembled

main() → objdump → every instruction named

Easy 35 min 180 xp
After this you can
  • 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:

ret7.cc
1int main(void) {
2 return 7;
3}

Compile it *without optimization* (so the compiler translates literally instead of being clever), then disassemble:

compile and disassemblesh
1gcc -O0 -g ret7.c -o ret7
2objdump -d --disassemble=main -M intel ret7
main(), disassembled (Intel syntax)asm
1main:
2 push rbp ; (prologue) save the caller's frame pointer
3 mov rbp, rsp ; (prologue) rbp now marks this function's frame
4 mov eax, 7 ; (body) put the return value 7 into eax
5 pop rbp ; (epilogue) restore the caller's frame pointer
6 ret ; (epilogue) pop the return address into rip → go back

Every 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 int is 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 ret pops 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.)
Key idea
A method for reading any function: find the prologue (push rbp; mov rbp, rsp) to see where it starts, look for arguments arriving in the ABI registers, read the body, and find the epilogue ending in 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:

add.cc
1int add(int a, int b) {
2 return a + b;
3}
add(), at -O0 (literal translation)asm
1add:
2 push rbp
3 mov rbp, rsp
4 mov [rbp-4], edi ; save arg a (arrived in edi) into a local slot
5 mov [rbp-8], esi ; save arg b (arrived in esi) into a local slot
6 mov eax, [rbp-4] ; load a
7 add eax, [rbp-8] ; eax = a + b
8 pop rbp
9 ret ; return value already in eax

Read 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:

add(), at -O2 (the compiler gets clever)asm
1add:
2 lea eax, [rdi + rsi] ; eax = a + b, in one instruction, no stack at all
3 ret

Same 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.

Predict first
int answer(void) { return 42; }, compiled at -O0. You already saw ret7. Predict the one body instruction (the register and the value).
Note
Tip for the lab: 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.
finished reading?
Your task, you write the code

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.

deliverable: bridge.c (+ your annotated objdump notes)
build & run
$ 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
self-review before running
  • 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
stretchAdd `int mul(int a,int b){return a*b;}` and find the `imul` in the -O2 output. Then write a function returning a+a and see whether the compiler uses add, shl (shift left by 1), or lea — and reason about why.

Self-check

01Where does a small function put its integer return value?
02In `add(int a, int b)` at -O0, where do a and b arrive?
03The same C compiled at -O0 and -O2 produced different instructions. What does that tell you?
04What is the prologue (push rbp; mov rbp, rsp) for?
0/4 correct · 0/4 checked