From Source to Executable: The Two-Stage Build
Most developers think of compilation as a single step: source code goes in, a binary comes out. In reality, it is two distinct stages, and understanding the boundary between them explains a whole class of build errors.
Stage 1: Compilation → Object Files (.o)
The compiler (cc1, invoked by gcc) translates each source file independently into an object file — produced by stopping the toolchain right after compilation, before any linking happens. An object file is an ELF file, but it is incomplete — it contains machine code with unresolved references. For example, when main.c calls printf, the compiler generates a call instruction but leaves the target address blank, annotated with a note: "fill this in later — it's called printf." Symbol-inspection tools can list these gaps directly: a symbol like printf shows up marked U for "Undefined" — referenced in this object file but not defined here.
Stage 2: Linking → Executable
The linker (ld, invoked by gcc once it's given something to link rather than just compile) takes all object files and libraries, resolves every undefined symbol, assigns final memory addresses, and produces the complete ELF executable. Whether you compile and link multiple source files in one invocation or compile each one separately first, the same linker step runs at the end to stitch everything together.
Why does this matter?
The classic "undefined reference to foo" linker error means the linker searched all provided object files and libraries and never found a definition for foo. This is a linker error, not a compiler error — the distinction tells you where to look. Compiler errors mean your syntax or types are wrong. Linker errors mean you forgot to include a file or library. A common instance of this: calling a math function like sin() compiles cleanly but fails at link time, because the math library isn't linked by default — it has to be requested explicitly.