C Toolchain
C is Linux's native language — the kernel is C, glibc is C, most of the userland is C, and every syscall boundary is a C interface. The toolchain that turns C into binaries — the preprocessor, compiler, assembler, linker, plus make and the debugger — is the deepest instrument in the kit. This chapter covers the pipeline end to end: what each stage does, what each artifact is, and how to read the results.
The four-stage pipeline
gcc hello.c -o hello # all four stages at once — but see them separately:
gcc -E hello.c -o hello.i # 1. PREPROCESS — #include expanded, macros resolved
gcc -S hello.i -o hello.s # 2. COMPILE — C → assembly text
gcc -c hello.s -o hello.o # 3. ASSEMBLE — asm → object file (ELF relocatable)
gcc hello.o -o hello # 4. LINK — objects + libraries → executable
Each stage's output is inspectable: hello.i is text, hello.s is real assembly, hello.o is an ELF object (file hello.o → "relocatable"), hello is "executable". The -save-temps flag keeps all intermediates — read them once and the magic ends forever.
gcc — the flags that matter
gcc -Wall -Wextra hello.c # all warnings — the baseline
gcc -Werror hello.c # warnings are errors — CI mode
gcc -O0/-O1/-O2/-O3 hello.c # optimization levels
gcc -g hello.c # debug info — for gdb
gcc -std=c17 hello.c # the C standard
gcc -I./include hello.c # extra header dirs
gcc -L./lib -lmylib hello.c # library dirs + link a lib
gcc -static hello.c # fully static binary
gcc -shared -fPIC lib.c -o lib.so # shared library
gcc -DNAME=value hello.c # define a macro on the command line
gcc -fsyntax-only hello.c # check without compiling
gcc --version; gcc -v hello.c # version; verbose — the include/link paths
-Wall -Wextra -Werror is the discipline; -g pairs with -O0 for debugging (optimization reorders code confusingly); -static produces a binary with no shared dependencies — the LFS bootstrap binaries lean on it.
Object files and symbols — the anatomy
nm hello.o # the symbol table — functions/vars, defined/undefined
objdump -d hello # disassemble — the actual machine code
objdump -h hello # section headers — .text .data .bss
readelf -h hello # the ELF header — type, arch, entry point
readelf -S hello # sections
readelf -d hello # dynamic section — NEEDED libs
readelf -s hello # symbol table
ldd hello # which shared libs it loads
file hello # the one-line verdict
nm output letters: T = defined text (function), D/B = data/uninit, U = undefined (to be resolved at link). The linker's whole job: resolve every U against other objects' T/D.
Static vs shared libraries
ar rcs libmy.a obj1.o obj2.o # STATIC archive — a bundle of .o files
gcc main.c libmy.a -o prog # link it — code copies INTO the binary
gcc -shared -fPIC -o libmy.so my.c # SHARED — a single .so, loaded at runtime
gcc main.c -L. -lmy -o prog # links against libmy.so or .a
LD_LIBRARY_PATH=. ./prog # runtime search for .so
ldconfig -p | grep my # the system's .so registry
Static bakes the library in — no runtime deps, bigger binary, no update path. Shared (.so) loads at runtime via ld.so — small binaries, shared memory, updates propagate — but the .so must be found: ldd, LD_LIBRARY_PATH, /etc/ld.so.conf + ldconfig govern the search.
soname versioning — libfoo.so.1 is the ABI version; libfoo.so → libfoo.so.1.2.3 symlinks — is how apt upgrade replaces a library under running programs.
make — the build automator
# Makefile — targets: prerequisites + recipe
hello: hello.o
gcc hello.o -o hello
hello.o: hello.c
gcc -c hello.c -o hello.o
clean:
rm -f hello hello.o
make # first target
make hello # named target
make -j$(nproc) # parallel — the LFS speedup
make -C dir # enter dir first
make -n # dry run — what WOULD happen
make clean # convention target
make install # convention target — DESTDIR/prefix matter
Make's real power: dependency tracking — rebuild only what changed. make reads timestamps; a .o newer than its .c isn't rebuilt. The tab-indent requirement is make's famous sharp edge — recipe lines must be literal tabs.
The preprocessor and the header world
gcc -E hello.c | less # watch #include explode
gcc -M hello.c # the header dependency list — make uses this
echo | gcc -E -v - # the built-in include search path
Headers: <stdio.h> = system (/usr/include); "my.h" = local. The -I dirs prepend the search. pkg-config --cflags --libs gtk returns the right flags for installed libraries — the bridge between "installed" and "found".
gdb — the debugger
gcc -g hello.c -o hello # -g embeds debug info
gdb ./hello
(gdb) break main # breakpoint
(gdb) run # start
(gdb) next / step # over / into
(gdb) print var # inspect
(gdb) backtrace # the call stack — a crash's map
(gdb) list # source around current line
(gdb) continue # resume
For crashes without gdb: ulimit -c unlimited + the program dumps core → gdb ./prog core post-mortems it.
Compiler family — clang, cc, and friends
cc → gcc is the POSIX convention; clang/llvm is the modern alternative — same flags mostly, better diagnostics, faster in places, and the LLVM infrastructure (lld, the IR, sanitizers). clang-format, -fsanitize=address/undefined (in both compilers now) — AddressSanitizer and UBSan — turn silent memory bugs into loud crashes. Every serious C project compiles with -fsanitize in test builds.
The toolchain in context — the LFS payoff
On an LFS system, gcc --version reports the compiler you compiled — the cross-toolchain of chapter 3 grown into the native toolchain of chapter 5. ldd /usr/bin/gcc shows it linking the glibc you built. The toolchain is the system's reproductive organ — the one package that can build all the others, including a newer version of itself.
Where compilation goes wrong
| Symptom | Cause |
|---|---|
undefined reference to 'foo' | Library not linked — -lfoo or missing .o |
cannot find -lfoo | Library dir missing — -L/path |
foo.h: no such file | Header dir missing — -I/path |
Works on my machine, libfoo.so missing elsewhere | Runtime path — ldconfig, LD_LIBRARY_PATH, rpath |
| Warnings-as-errors in old code | -Werror vs a newer gcc — -Wno-x or fix |
make rebuilds nothing | Timestamps confused — touch the sources |
| "relocation against ... recompile with -fPIC" | Static code in a shared lib — -fPIC |
| Different binary each build | -O/-g nondeterminism — -frandom-seed, or check flags |
Exercises
gcc -save-temps hello.c— open.i,.s,.o— read each stage's artifact.nma .o — find theT/Usymbols; link a second .o resolving aU.- Build a
.a, a.so, and a program using each —lddthem. objdump -dyour hello — findmainin the disassembly.make -na real Makefile — the dry-run plan.- Crash a program on purpose (
*(int*)0=0);gdbthe core.