roadmap
week 3 · day 19

C Functions → Assembly

Compile, disassemble, recognize every piece

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

count.cc
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:

count_positive at -O0 (annotated)asm
1count_positive:
2 push rbp ; prologue: set up
3 mov rbp, rsp ; the frame
4 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 i
10 cmp eax, [rbp-28] ; i < n ? (compare, day 9)
11 jge .done ; exit loop if i >= n
12 mov eax, [rbp-8] ; i
13 movsxd rax, eax ; sign-extend i to 64 bits for addressing
14 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 ; epilogue
25 ret

Read it as recognition, not decoding

  • Prologue / epilogue (day 14): push rbp; mov rbp, rsppop rbp; ret.
  • Arguments (day 13): a came in rdi, n in esi; the function spills them to stack slots.
  • Locals (day 14): c at [rbp-4], i at [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] is a[i] — base + index*scale, scale 4 for int.
  • The branch (day 9): cmp eax, 0; jle .skip is if (a[i] > 0).
  • The return (day 13): mov eax, [rbp-4] puts c in eax.

Every line has a home in something you already learned. That's the whole point of the spine: nothing is left as magic.

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

Predict first
In the -O0 disassembly above, one instruction implements the whole a[i] access. Which line is it, and what are its base, index, and scale — and why is the scale 4?
finished reading?
Your task, you write the code

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.

deliverable: myfunc.c (+ fully annotated -O0 disassembly)
build & run
$ 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
self-review before running
  • 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
stretchPaste your function into godbolt.org and use its colour-matching to check your annotations instantly. Then try a version with a struct field access and find the [base + fixed-displacement] that reads the field (day 18).

Self-check

01In a -O0 disassembly, how do you find the loop?
02The instruction `mov eax, [rdx + rax*4]` in this function is:
03Why compile the same function at both -O0 and -O2?
04What does `movsxd rax, eax` do and why is it there?
0/4 correct · 0/4 checked