Frontend Labs
⋯

2. The toolchain: the four-stage pipeline, and your first program

2.1 The four stages, made visible

"Compiling" a C file is actually four separate steps, and understanding them explains almost every confusing compiler error you'll ever see.

cat > hello.c <<'EOF'
#include <stdio.h>

int main(void) {
    printf("hello, c\n");
    return 0;
}
EOF

# 1. Preprocessing: expand #include, #define, #if — pure text substitution
clang -E hello.c -o hello.i
wc -l hello.i          # thousands of lines, all from stdio.h's chain of includes

# 2. Compilation: C source -> assembly for the target architecture
clang -S hello.i -o hello.s
head -20 hello.s

# 3. Assembly: assembly -> machine code, produces an object file
clang -c hello.s -o hello.o
file hello.o            # Mach-O 64-bit object arm64 (or ELF 64-bit on Linux)

# 4. Linking: resolve symbols (printf lives in libc, not your object file)
clang hello.o -o hello
./hello                 # hello, c

# In practice you just run:
clang -std=c17 -Wall -Wextra -o hello hello.c

Real-world example. "Undefined reference to foo" is always a linking error — the compiler understood your code fine, but no object file or library provides a symbol named foo. "Implicit declaration of function foo" is a compilation warning from stage 2 — you called something the compiler hasn't seen a prototype for (usually a missing #include). Knowing which stage produced an error tells you where to look: linker errors mean check your -l/-L flags and whether you compiled every .c file; compiler errors mean check your headers and syntax.

2.2 The flags you should always use

clang -std=c17 -Wall -Wextra -Wpedantic -Werror \
      -fsanitize=address,undefined -g -O0 \
      -o prog prog.c
Flag Why
-std=c17 (or -std=c23) Pin the language version; don't rely on "whatever GNU extensions the compiler defaults to"
-Wall -Wextra Turns on the warnings that catch real bugs: uninitialized reads, signed/unsigned comparison, unused variables
-Wpedantic Warns on non-standard extensions — important if you need portability
-Werror Warnings fail the build. Excellent for CI and most project code; you may relax it for exploratory/compiler-portability builds
-fsanitize=address,undefined Catches buffer overflows, use-after-free, and UB at runtime — Part V covers this in depth
-g -O0 Debug symbols, no optimization, so a debugger's line numbers match your source

A portability trap -std=c17 sets for you on Linux. Strict -std=c17 defines __STRICT_ANSI__, and on glibc that hides POSIX declarations (strdup, fork, execvp, getopt, pthread_*'s POSIX-only helpers, pipe, dup2, BSD socket calls) unless a feature-test macro is set — so code that calls them fails with "implicit declaration of function" under -Werror, even though the exact same command compiles cleanly on macOS (Apple's libc doesn't gate these the same way, which is why every snippet in this guide compiled clean on the machine it was tested on). Any time a later section's code touches POSIX (Part III's threads and sockets, Part IV's fork/termios/getopt), compile it with either -std=gnu17 (GNU's C17, POSIX/BSD extensions visible by default) or -std=c17 -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE on Linux. This guide keeps -std=c17 in the command lines for the pure-ISO-C sections (Parts I–II) and switches to -std=gnu17 from Part III onward for exactly this reason.

2.3 make, at the level you actually need on day one

CC      = clang
CFLAGS  = -std=c17 -Wall -Wextra -Wpedantic -Werror -g
SRC     = main.c
BIN     = prog

$(BIN): $(SRC)
	$(CC) $(CFLAGS) -o $(BIN) $(SRC)

clean:
	rm -f $(BIN)

.PHONY: clean

Section 13 goes deep on multi-file make; this is enough to stop typing the full clang command by hand.


Generated from real-life-c-guide.md · 8 Oct 2026 16:36