C Compilation Pipeline: Source Code to Binary

The C compilation pipeline is a multi-stage process that converts human-readable C source code into an executable machine binary. This pipeline consists of four distinct phases: preprocessing, compilation, assembly, and linking. Each stage accepts the output of the previous stage, applies specific transformations, and generates intermediate representations until the final standalone binary is produced for the target hardware architecture.

1. Preprocessing

The pipeline begins with the preprocessor (often invoked internally as cpp). The preprocessor processes all preprocessor directives, which are lines beginning with the # symbol.

Key tasks performed during preprocessing include:

  • Header Inclusion (#include): Replacing #include directives with the actual contents of the specified header files (such as stdio.h).
  • Macro Expansion (#define): Scanning the code for defined macro names and replacing them with their corresponding values or code blocks.
  • Conditional Compilation (#ifdef, #ifndef, #endif): Evaluating conditional flags to include or exclude blocks of code prior to compilation.
  • Comment Removal: Stripping all single-line (//) and multi-line (/* */) comments from the source file.

The output of this stage is an expanded, pure C source file, typically denoted with a .i extension.

2. Compilation

In the compilation phase, the preprocessed source code is analyzed and converted into assembly language specific to the target CPU architecture. Compilers like GCC or Clang run their primary compilation engine (such as cc1) during this step.

This phase involves several internal sub-steps:

  • Lexical Analysis (Tokenization): Converting the stream of characters into recognized tokens (keywords, identifiers, operators).
  • Syntax and Semantic Analysis: Parsing tokens into an Abstract Syntax Tree (AST) to verify grammatical correctness and type safety.
  • Intermediate Representation (IR) Generation: Converting the AST into a hardware-independent intermediate representation.
  • Optimization: Analyzing the IR to eliminate dead code, unroll loops, inline functions, and optimize register usage.
  • Code Generation: Translating the optimized IR into target-specific assembly code.

The output of the compilation stage is an assembly language file, typically carrying a .s extension.

3. Assembly

The assembly phase translates human-readable assembly instructions into raw machine code (native CPU instructions). This step is handled by an assembler (such as as).

The assembler converts mnemonic instructions (such as mov, add, or jmp) into their binary opcode equivalents. Because external function addresses and cross-file memory locations cannot be determined yet, the assembler leaves placeholders for these references within the output.

The resulting output is an unlinked object file, typically with a .o (on Unix-like systems) or .obj (on Windows) extension. Object files contain machine code organized into standard formats such as ELF (Executable and Linkable Format) or PE (Portable Executable), complete with symbol tables and relocation entries.

4. Linking

The final stage of the pipeline is linking, handled by the linker (such as ld). A program often consists of multiple source files and relies on external libraries, such as the C Standard Library (libc).

The linker performs two primary tasks:

  • Symbol Resolution: Matching undefined external symbols (such as calls to printf) to their corresponding definitions across other object files and libraries.
  • Relocation: Merging the code and data segments from all input object files into a single unified layout and adjusting memory addresses so every instruction correctly targets valid memory locations.

Linking can be static (embedding library code directly into the final file) or dynamic (leaving references that the operating system resolves at runtime). Once linking is complete, the linker writes out the final, executable binary ready to be loaded into memory and executed by the operating system.