Skip to content
ALB Wiki

C Toolchain

This is an old revision of this page — revision 1 (2026-10-25 14:50:01 UTC). It may differ from the current revision.

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

SymptomCause
undefined reference to 'foo'Library not linked — -lfoo or missing .o
cannot find -lfooLibrary dir missing — -L/path
foo.h: no such fileHeader dir missing — -I/path
Works on my machine, libfoo.so missing elsewhereRuntime path — ldconfig, LD_LIBRARY_PATH, rpath
Warnings-as-errors in old code-Werror vs a newer gcc — -Wno-x or fix
make rebuilds nothingTimestamps 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

  1. gcc -save-temps hello.c — open .i, .s, .o — read each stage's artifact.
  2. nm a .o — find the T/U symbols; link a second .o resolving a U.
  3. Build a .a, a .so, and a program using each — ldd them.
  4. objdump -d your hello — find main in the disassembly.
  5. make -n a real Makefile — the dry-run plan.
  6. Crash a program on purpose (*(int*)0=0); gdb the core.

See also

Categories: C · Compilers · Handbook · Linux · Linux Handbook
This page was last edited 2026-10-25 14:50:01 UTC by ALB Team. What links here