CcCompilers · Lesson 6 of 7

Code Generation, Linking & JITs

The last mile: optimized IR becomes real instructions with real registers, separate files get stitched into one executable, and JIT compilers do the whole pipeline live while your program runs.

Codegen makes two hard choices. Instruction selection: which of the CPU's instructions implement each IR operation (x86 can often fold load-add-store into one instruction; ARM can't). Register allocation: the IR pretends registers are infinite, hardware has ~16 — graph coloring assigns the busiest values to registers and 'spills' the rest to stack memory. Register pressure is a real performance force: too many live variables means spills, means memory traffic.

Bash
# Separate compilation + linking — how big projects build:
gcc -c utils.c        # -> utils.o   (machine code, but
gcc -c main.c         # -> main.o     unresolved references)
gcc main.o utils.o -o app     # linker joins them

# main.o contains 'call helper' with a BLANK address and
# a note: "patch this when you find 'helper'". The linker
# resolves every such note across all .o files + libraries.

# 'undefined reference to helper' = linker found no
#   definition anywhere (forgot a file or a library flag).
# 'duplicate symbol' = found two.

nm utils.o            # see a file's symbols: T=defined, U=needed

Static linking copies library code into your binary (bigger, self-contained — Go's default). Dynamic linking loads shared libraries (.so/.dll) at startup, so all programs share one libc in memory and library fixes arrive without recompiling — at the cost of 'DLL hell' version mismatches. Every deployment headache about glibc versions traces here.

Text
JIT compilation — V8 running your JavaScript:

 1. Parse to bytecode, start interpreting immediately
 2. Profile while running: which functions are hot?
    what types actually flow through them?
 3. Hot function -> compile to machine code SPECIALIZED
    to the observed types ("x is always a small int")
 4. Guard checks protect the assumption; if a string
    shows up one day -> DEOPTIMIZE: throw away the
    fast code, fall back, maybe recompile

Why JS engines love consistent types — and why that
advice exists: monomorphic code stays compiled;
type-shifting code bounces between tiers.

The full pipeline, running in milliseconds,
while your page loads. Compilers all the way down.
◆ Note
Where to go deeper: 'Crafting Interpreters' by Robert Nystrom (free online — you build two complete languages), then LLVM's Kaleidoscope tutorial for a real back end. You already have the map; those fill in the territory.