C Functions → Assembly
Compile, disassemble, recognize every piece
- › Read a non-trivial C function's disassembly end to end, naming every instruction
- › Map C constructs (locals, if, loop, array access, call) to their assembly patterns
- › Use -O0 vs -O2 to separate literal translation from optimization
- › Reach the point where C and its assembly feel like one thing
The convergence
This is where the two halves of the spine meet. You've learned the machine (registers, flags, branches, loops, the stack, the ABI, frames) and you've learned C's building blocks (types, pointers, arrays, structs). Today you take a *whole function* that uses several of these and read its assembly straight through, naming everything. Nothing here is new — it's recognition. By the end, looking at C and looking at its assembly should feel like reading the same sentence in two fonts.
A function worth disassembling
Here's a function that touches a local, a loop, an array, and a branch:
1int count_positive(int *a, int n) {2 int c = 0;3 for (int i = 0; i < n; i++) {4 if (a[i] > 0) {5 c++;6 }7 }8 return c;9}Compiled at -O0 (literal translation, best for learning), it becomes roughly:
1count_positive:2 push rbp ; prologue: set up3 mov rbp, rsp ; the frame4 mov [rbp-24], rdi ; save arg 'a' (1st arg, from rdi — ABI)5 mov [rbp-28], esi ; save arg 'n' (2nd arg, from esi)6 mov dword [rbp-4], 0 ; c = 0 (local at [rbp-4])7 mov dword [rbp-8], 0 ; i = 0 (local at [rbp-8])8.loop:9 mov eax, [rbp-8] ; load i10 cmp eax, [rbp-28] ; i < n ? (compare, day 9)11 jge .done ; exit loop if i >= n12 mov eax, [rbp-8] ; i13 movsxd rax, eax ; sign-extend i to 64 bits for addressing14 mov rdx, [rbp-24] ; a (base)15 mov eax, [rdx + rax*4] ; a[i] ([base + index*scale], scale=4, day 6/17)16 cmp eax, 0 ; a[i] > 0 ?17 jle .skip ; if not, skip the c++18 add dword [rbp-4], 1 ; c++19.skip:20 add dword [rbp-8], 1 ; i++21 jmp .loop ; back-edge (loop, day 10)22.done:23 mov eax, [rbp-4] ; return value = c, into eax (day 13)24 pop rbp ; epilogue25 retRead it as recognition, not decoding
- Prologue / epilogue (day 14):
push rbp; mov rbp, rsp…pop rbp; ret. - Arguments (day 13):
acame inrdi,ninesi; the function spills them to stack slots. - Locals (day 14):
cat[rbp-4],iat[rbp-8]— negative offsets from rbp. - The loop (day 10): a top test (
cmp+jge .done), a body, and a back-edge (jmp .loop). - The array access (day 6/17):
[rdx + rax*4]isa[i]— base + index*scale, scale 4 for int. - The branch (day 9):
cmp eax, 0; jle .skipisif (a[i] > 0). - The return (day 13):
mov eax, [rbp-4]putscin eax.
Every line has a home in something you already learned. That's the whole point of the spine: nothing is left as magic.
movsxd rax, eax sign-extends the 32-bit index i into the full 64-bit rax before it's used
in an address (addresses are 64-bit). Sign-extension copies the sign bit into the upper bytes so a
negative int stays negative. It's a small but real detail: index math is 64-bit even when the index
is a 32-bit int.Then look at what ships (-O2)
Recompile at -O2 and the optimizer may transform this beyond recognition — keeping the counter in a
register (no stack), reordering, even vectorizing. The *meaning* is preserved (count the positives),
but the instructions differ wildly. Reading both versions is the exercise: -O0 teaches you the
mapping; -O2 teaches you what the optimizer is willing to do. Do this on your own code often and
you build an intuition no textbook gives.
a[i] access. Which line is it,
and what are its base, index, and scale — and why is the scale 4?Name every line of a real function
Write a C function of your own that uses at least: a local variable, a loop, an array indexed by the loop counter, and an if inside the loop (e.g. sum the even elements, or find the max). Compile at -O0 with -g, disassemble with objdump -d -M intel, and annotate EVERY instruction with the C construct it implements — prologue, arg registers, each local's offset, the loop's test/body/back-edge, the [base+index*scale] access, the branch, and the return. Then compile at -O2 and write a short note on what the optimizer changed. This is pure recognition using days 6-18.
$ gcc -O0 -g -Wall myfunc.c -o myfunc_o0 && objdump -d -M intel myfunc_o0$ gcc -O2 myfunc.c -o myfunc_o2 && objdump -d -M intel myfunc_o2- › Every -O0 instruction is annotated with the C it came from
- › You identified the arg registers, the locals' offsets, and the loop back-edge
- › You pointed at the [base+index*scale] instruction and stated its scale and why
- › You noted at least one thing the -O2 optimizer did differently