When a linker error first interrupted code that had “compiled fine,” the vocabulary suddenly mattered. GCC had translated each source file successfully; the failure happened while combining those pieces. The easiest way to make that distinction stick is to keep every intermediate and look inside it.
The files produced at each boundary
hello.c— human-written C source.hello.i— preprocessed C after headers, macros, and conditional directives are handled.hello.s— target-specific assembly emitted by the compiler proper.hello.o— relocatable ELF object containing machine code, sections, symbols, and relocation records.hello— linked ELF executable with a resolved layout and any declared shared-library dependencies.Running process — virtual-memory mappings, initialized runtime state, stack, heap, libraries, and threads created during execution.
Use a small program with a real external reference
#include <stdio.h>
#define MESSAGE "Hello from the compiler pipeline"
static int twice(int value)
{
return value * 2;
}
int main(void)
{
printf("%s: %d\n", MESSAGE, twice(21));
return 0;
}Why this tiny source is revealing
#includeand#definegive the preprocessor visible work.staticgivestwiceinternal linkage, so it is not exported as a global definition from this translation unit.printfis declared by the header but defined by the C library, leaving the object file with an external symbol to resolve.mainreturns an integer status to the C runtime, which ultimately reports it to the operating system.
Ask GCC to show the programs it invokes
gcc -v -O0 -o hello hello.c... cc1 ...
... as ...
... collect2 ... ld ...GCC is acting as a driver
gccselects tools and options appropriate for C and the target platform.-vprints the subprocesses and search paths; exact output varies by distribution and GCC build.-O0keeps the learning example less optimized, though it does not guarantee a one-to-one source-to-instruction mapping.-o hellonames the final output; nosudois needed to compile inside a writable project directory.
Stage 1: preprocessing creates a translation unit
gcc -E hello.c -o hello.i
grep -n "Hello from the compiler pipeline" hello.i | tail -1... printf("%s: %d\n", "Hello from the compiler pipeline", twice(21));What changed before C was compiled
-Estops after preprocessing and-osaves output that would otherwise go to standard output.Header contents are included according to the implementation’s include search rules.
Object-like macro
MESSAGEis replaced by its string literal token sequence.Conditional compilation is evaluated and comments are normally removed.
The
.ifile is often large because system declarations are now part of one translation unit.
Stage 2: compilation proper emits assembly
gcc -S -O0 hello.i -o hello.s
sed -n '1,80p' hello.s...
twice:
...
main:
...
call printf@PLT
...This is where language semantics become target code
-Sstops after compilation proper, before the assembler creates an object.GCC parses and diagnoses C, builds internal representations, applies requested optimizations, and emits target assembly.
Assembly syntax and instructions depend on architecture, ABI, compiler version, and options.
A call through
printf@PLTis common in position-independent Linux output, but its spelling is not portable or guaranteed.
Stage 3: assembly creates a relocatable ELF object
gcc -c hello.s -o hello.o
file hello.o
readelf -h hello.o
readelf -sW hello.o | grep -E '(main|printf|twice)'
readelf -rW hello.ohello.o: ELF ... relocatable ...
... FUNC ... LOCAL ... twice
... FUNC ... GLOBAL ... main
... NOTYPE ... GLOBAL ... UND printf
Relocation section ...The object has code but no final addresses
-ccompiles or assembles but deliberately skips linking.fileidentifies the object format and architecture;readelfreports ELF structures without executing the file.mainis defined globally,twiceis local, andprintfremains undefined in this object.Relocation records tell the linker where symbol-dependent instruction or data fields must be adjusted.
A relocatable object is not normally executable because its sections have not been assigned their final program layout.
Stage 4: the linker resolves and lays out the program
gcc hello.o -o hello
file hello
readelf -hW hello
readelf -lW hello
readelf -dW hello | grep NEEDEDhello: ELF ... pie executable ... dynamically linked ...
... INTERP ...
... LOAD ...
... Shared library: [libc.so.6]Why invoking GCC for the link is usually correct
The linker combines object and archive inputs, assigns addresses, lays out sections, applies relocations, and ties up symbol references.
The GCC driver supplies startup objects, runtime libraries, architecture options, and the correct linker invocation for the toolchain.
Modern Linux distributions commonly build a position-independent executable by default; confirm with
filerather than assuming.Program headers describe segments the kernel maps at execution; section headers mainly describe linking and inspection units.
DT_NEEDEDrecords direct shared-library dependencies. It does not copy those libraries into the executable.
Execution: the kernel and dynamic linker load the program
readelf -lW hello | grep -A1 INTERP
ldd ./hello
./hello
printf 'exit status: %d\n' "$?"[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
... libc.so.6 => ...
Hello from the compiler pipeline: 42
exit status: 0Loading begins only when the file is executed
execvereplaces a process image; the kernel validates ELF program headers and maps loadable segments into virtual memory.For a dynamic ELF executable, the
.interppath selects the runtime linker/loader.The loader maps required shared objects, performs dynamic relocations and symbol resolution, prepares runtime state, and transfers control through the process entry path.
lddoutput and interpreter paths vary by architecture and libc; run it only on binaries you trust.The shell receives the status returned through the C runtime after
mainfinishes.
Compile and link multiple source files separately
int answer(void)
{
return 42;
}One source file forms one translation unit
The function has external linkage because it is not declared
static.Compiling this file does not require the definition of
main.A header should normally declare
answerso callers and the definition share a checked signature.
gcc -Wall -Wextra -Wpedantic -c main.c -o main.o
gcc -Wall -Wextra -Wpedantic -c answer.c -o answer.o
gcc main.o answer.o -o answer-appSeparate compilation is what makes incremental builds practical
Each
-ccommand produces one object without demanding that every external reference is resolved.The final command links both objects and reports duplicate or unresolved global symbols.
Changing only
answer.callows a build system to rebuildanswer.oand relink without recompilingmain.c.Warnings belong on compilation commands; link-specific options belong on the final driver invocation.
Read failures by the stage that reported them
Header not found or malformed macro: preprocessing/search-path problem; inspect
-I, conditional directives, andgcc -Eoutput.Syntax error or incompatible type: compiler proper understood tokens but rejected C semantics.
Assembler rejects an instruction: generated or hand-written assembly does not match the selected target or assembler syntax.
Undefined reference: linking could not find a required definition; add the correct object/library and respect library ordering where applicable.
Multiple definition: more than one linked input exports the same strong symbol, often because a definition was placed in a header.
Shared library cannot be opened: execution-time loader search failed; inspect
DT_NEEDED, RPATH/RUNPATH, loader cache, and deployment.Illegal instruction at runtime: the binary executed but contains an instruction unsupported by that CPU, often from an overly aggressive
-march.
Optimization and LTO blur the teaching boundaries
The conceptual pipeline remains useful, but modern toolchains can integrate stages. Optimization may inline or delete functions, assembler integration can avoid a durable .s file, and link-time optimization keeps an intermediate representation in objects so whole-program optimization happens during linking. Debug real builds with their actual flags, not only the neat classroom sequence.
Primary references
GCC options controlling output defines
-E,-S,-c, suffix handling, and the four compilation stages.GNU assembler documentation explains how assembly becomes object code and relocation information.
GNU linker overview describes object combination, relocation, and symbol resolution.
GNU readelf documentation documents ELF header, symbol, section, and relocation inspection.
Linux dynamic linker manual explains the ELF interpreter and shared-object loading.
Comments and corrections