How C Becomes Assembly: Case Studies
This chapter walks through concrete examples of how C constructs — locals, pointers, arrays, structs, loops, branches, and calls — map to assembly.
Why This Matters
Reading the assembly your compiler emits is the surest way to understand what your C code actually does. It connects the abstract machine to the real CPU and is the foundation of optimization and debugging.
Prerequisites
c.cpu.1— CPU fundamentals.
Core Concept
The compiler lowers C to assembly following the target ABI. Simple local variables may live in registers; pointers are addresses; arrays are contiguous memory accessed by scaled addressing; structs are offsets; loops and branches become jumps; calls become call/ret.
Case Studies
Locals and arithmetic
int f(int x) { int y = x * 3 + 1; return y; }
x86-64 (-O2):
f:
lea eax, [rdi + rdi*2 + 1]
ret
The expression is folded into a single scaled-address computation.
Arrays
int g(int *a, int i) { return a[i]; }
g:
movsx rax, esi
mov eax, DWORD PTR [rdi + rax*4]
ret
Indexing compiles to a scaled load (*4 for int).
Structs
struct P { int x; int y; };
int get_y(struct P *p) { return p->y; }
get_y:
mov eax, DWORD PTR [rdi + 4]
ret
p->y is a load at offset 4 (assuming no padding before y).
Loops and branches
int sum(int n) {
int s = 0;
for (int i = 0; i < n; i++) s += i;
return s;
}
The compiler emits a loop with a conditional branch, and at -O3 may vectorize or unroll it.
Function calls
int add(int a, int b);
int h(int x) { return add(x, 1); }
h:
mov esi, 1
jmp add ; tail call
How It Works
The compiler's back end does instruction selection, register allocation, and instruction scheduling, producing assembly from the IR. The ABI dictates argument/return placement.
Variations
Optimization changes the mapping
At -O0, locals are stored on the stack; at -O2, they live in registers or disappear entirely. The same source can produce very different assembly.
Common Mistakes
- Assuming each C line maps to one assembly instruction.
- Assuming the source order is preserved (the compiler reorders freely).
Undefined Behavior
- Assembly is generated assuming no UB; UB can yield assembly that does not
match the source's apparent intent (c.opt.2).
Portability
- Assembly is target-specific; the mapping depends on the architecture and ABI.
Under the Hood
-S shows assembly; objdump -d disassembles a binary; -fverbose-asm adds source comments.
Practical Usage
- Use
gcc -S -fverbose-asm(or Compiler Explorer) to study mappings. - Read assembly to verify the optimizer did what you expect.
- Use assembly reading to debug optimized code (
c.debug.5).
Exercises
1. Compile each case study at -O0 and -O2 and compare. 2. Identify the prologue/epilogue, scaled addressing, and tail call. 3. Add a switch and observe jump table vs. branch chain.
Deep Challenge
Write a function that the compiler vectorizes at -O3, and explain, in the generated assembly, the vector load, the packed operation, and the scalar remainder loop.
Related Concepts
c.cpu.1— CPU fundamentals.c.cpu.2— stack frames.c.opt.1— optimizer.
References
gcc -S,objdump, architecture manuals, Compiler Explorer.
Verification
- Assembly mappings are compiler/architecture-specific.
COMPILER-SPECIFIC - No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] Locals → registers/stack
- [ ] Arrays → scaled addressing
- [ ] Structs → offsets
- [ ] Loops/branches → jumps
- [ ] Calls → call/ret
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.cpu.c-to-asm | 0 | 6 |