← Back to Home

Code Generation

Managing the RAM Stack & Hardware Translation

The "Memory-to-Memory" Stack Model

Our compiler implements a Stack-Based Model targeting the x86-64 System V ABI. It is the exact same strategy GCC uses with Zero Optimization (gcc -O0).

1. The "Fixed-Size" Stack Frame

Whenever a function is called, your code generator executes a standard prologue:

pushq %rbp           # Save caller's base pointer
movq %rsp, %rbp      # Set current base pointer
subq $1024, %rsp     # Allocate memory for locals

Instead of calculating exact byte sizes, we pre-allocate a massive, flat block of memory. This guarantees we never accidentally overwrite another function's memory.

2. Variables & The Delivery Trucks

Every variable gets an 8-byte slot as an offset from the Base Pointer (%rbp). Because the CPU cannot add two RAM addresses directly, registers (%rax, %rbx) are used strictly as temporary delivery trucks. They fetch variables from RAM, do the math, drop the result back in RAM, and immediately forget it.

Simulator 1: The 16-Byte Stack Alignment Failsafe

Modern CPU hardware instructions (like movaps used in C-libraries like printf) demand that the memory address it accesses is a perfect multiple of 16. If it isn't, the CPU throws a Segmentation Fault.

To prevent crashing, we wrap every C-library call in a bitwise mask (andq $-16, %rsp) which acts like a cookie-cutter to force the stack to round down to 16. Watch how the Failsafe adds "Padding" to the stack!

%rax: 0
%r15: 0
%rbp: 0x1000
%rsp: 0x1000
▶ rbp
▶ rsp

Simulator 2: Recursion & Destroying the Sandbox

How does a function call itself without overwriting its own variables? The answer is Activation Records (Frames). Every time call is executed, the CPU jumps to the top of the function and builds a brand new, isolated sandbox on top of the old one.

The Function Epilogue: How `leave` Knows Where To Go

It looks like magic when the pointers instantly jump back to the old frame, but the secret is in the Saved %rbp slot. The leave instruction is actually a shortcut for two hardware commands executed back-to-back:

  1. movq %rbp, %rsp (The Collapse): Instantly moves the stack pointer up to the base pointer, abandoning all local variables in the current sandbox.
  2. popq %rbp (The Restore): The CPU reads the value stored at the new `%rsp` (which is the old `%rbp` address saved during the prologue), puts it into the `%rbp` register, and moves `%rsp` up 8 bytes.

Finally, ret pops the Return Address into the instruction pointer (%rip) so the CPU knows what line of code to resume.

Watch how executing factorial(2) pushes isolated memory frames, calculates the result, and safely dismantles the stack frame using the sub-instructions of leave to hand the output back to main!

%rax (Output): 0
%rip (Line): main
%rbp: 0x2000
%rsp: 0x2000
▶ rbp
▶ rsp

Simulator 3: Control Flow (Branching & Loops)

The CPU only knows how to execute instructions sequentially (top-to-bottom). To create if/else statements or while loops, the compiler generates Compare (cmp) and Conditional Jump (jCC) instructions.

This trace follows the complex if / else if / else logic from our Binary Search example. We are searching for key = 12, but the current arr[mid] = 30. Watch how the CPU compares the values in the RAM Stack and physically jumps over the blocks of code it shouldn't execute!

%rax (Truck): 0
%rbp: 0x3000
▶ rbp
▶ rsp
Waiting to start...

Why Avoid Register Allocation?

The most highly-optimized compilers (like GCC with -O3) use complex algorithms like Graph Coloring to keep variables permanently inside CPU registers rather than writing to RAM.

However, we explicitly chose not to do this. Here is why:

  • Register Starvation: x86-64 only has ~14 general-purpose registers. If a function has 20 variables, the compiler must write highly complex code to "spill" variables back to RAM and track where they went.
  • Algorithmic Complexity: It requires tracking "Live Ranges" across the entire Control Flow Graph to know exactly when a register can be safely overwritten.