Home Architecture

From Source to Silicon: A Beginner's Guide to the Compiler, Assembler, and Linker Pipeline

Architecture

October 03, 2026

From Source to Silicon: A Beginner's Guide to the Compiler, Assembler, and Linker Pipeline

Type a few lines of C into a file called hello.c, run cc hello.c -o hello, and half a second later there is a working program. Inside that half second, four separate tools read your text, rewrote it twice, translated it into machine instructions, and stitched the result together with code you never wrote. None of it is hidden. Every stage leaves a file behind that you can open, read, and disassemble. This is a walk through that pipeline, using only flags you already have on your machine.

One Command, Four Programs

What most people call "the compiler" is really a driver. GCC and Clang decide which tools to run and in what order; the actual work happens in separate executables. Pass -v and the driver prints every command it executes. Pass -save-temps and it keeps the intermediate files instead of deleting them:

gcc -save-temps hello.c -o hello

You now have hello.i, hello.s, and hello.o sitting next to your source. Those three files are the pipeline made visible.

  1. Preprocessor (cpp): pure text substitution. Headers get pasted in, macros expand, comments vanish.
  2. Compiler (cc1): parses C, checks types, optimizes, emits assembly for your target CPU.
  3. Assembler (as): turns assembly text into machine code inside an object file.
  4. Linker (ld, usually launched through a wrapper called collect2): combines object files and libraries into one executable.
  5. Loader: not part of the build at all. The kernel and the dynamic loader map the program into memory when you run it.

Preprocessing: Text In, Text Out

Ask for the preprocessed result directly with gcc -E hello.c -o hello.i. What comes out is still C, just fatter. Include stdio.h and a three-line program becomes a thousand-line file, because every declaration that header pulls in gets copied into place. Later stages keep track of your original code through line markers: lines that begin with a hash and look like # 1 "hello.c".

Two tricks are worth keeping. When a macro expands into something baffling, run the preprocessor and search the .i file for the call site; the line markers tell you which header is responsible. And gcc -E -dM on an empty input prints every macro the compiler predefines for your machine, which settles most arguments about what a translation unit actually sees. Note that the preprocessor does no type checking. Feed it nonsense and it will produce nonsense without complaining.

Compilation: Preprocessed C to Assembly

gcc -S hello.i -o hello.s is where real translation happens. The compiler parses your code into a syntax tree, checks types, lowers it to an intermediate representation, runs optimization passes over that representation, and only then selects instructions for your architecture.

Read the .s file and the abstractions fall away. You will find a .text section for code, .rodata for string literals, a .globl main directive, labels like .L2, and function calls written as names rather than addresses. With optimization on there are surprises: at -O2, a printf of a constant string ending in a newline often becomes a call to puts, because the compiler knows it produces the same output with less work.

Compare -O0 and -O2 on the same file. Unoptimized output keeps every variable in a stack slot and reloads it for each use, which is why stepping through it in a debugger follows your source so neatly. Optimized output keeps values in registers, inlines small functions, unrolls loops, and reorders code, which is why stepping through it feels like watching a card trick.

Assembly: Mnemonics to Machine Code

gcc -c hello.s -o hello.o runs the assembler. It encodes each instruction into its binary form, resolves labels that are local to the file into offsets, and writes everything into an object file, an ELF file on Linux. The .s file is human-readable and architecture-specific; the .o file is neither.

An object file is not a program. It holds code, data, a symbol table, and relocations: notes saying "this field contains an address that nobody knows yet." Two tools make it legible:

  • nm hello.o lists symbols. T means defined in the code section, U means undefined and expected from elsewhere, D and B cover initialized and uninitialized data.
  • objdump -d hello.o disassembles the instructions. Calls to outside functions show placeholder targets, backed by relocation entries you can view with readelf -r.

One caution: assembly output is not a strict transcript of instructions. Assemblers accept pseudo-instructions, choose between encodings of the same operation, and relax branch sizes. When the two disagree, the disassembly is the truth.

Linking: One Program From Many Objects

The linker takes your object files, the startup code, and the libraries you named, and produces a single executable. Its two jobs are symbol resolution and relocation. Every U in one file has to match a T in another, or the build stops.

This is the stage beginners blame on the compiler. "undefined reference to sqrt" is a linker error: the symbol exists, but you did not hand over the math library with -lm. "multiple definition of main" is also a linker error, meaning two files both define main and both were passed to

Photo: Lee Campbell / Pexels

Related Posts

Static vs Dynamic Binary Analysis: Which Should You Use First?
Reverse Engineering

October 12, 2026

Static vs Dynamic Binary Analysis: Which Should You Use First?

Static analysis maps what a binary could do; dynamic analysis shows what it actually does. Here is how to choose your starting workflow...

read more
Embedded Debugging Checklist: Logic Analyzer, SWD, and UART Before You Rewrite Code
Embedded Systems

October 08, 2026

Embedded Debugging Checklist: Logic Analyzer, SWD, and UART Before You Rewrite Code

A hardware-first checklist for embedded targets: verify power, clock, UART, and SWD before assuming your firmware is at fault.

read more
Endianness Explained: Why Your Binary Looks Backwards
Binary

October 02, 2026

Endianness Explained: Why Your Binary Looks Backwards

Endianness decides which byte of a multi-byte value lands first in memory. Here's how little- and big-endian differ, and how to debug...

read more