| | {{infobox |
| | | title = The C Toolchain |
| | | chapter = Linux Handbook, chapter 15 — gcc, the compile pipeline, make, gdb, libraries |
| | | core | C is Linux's native tongue — the kernel, the libc, the userland |
| | | pipeline = preprocess → compile → assemble → link |
| | | tools | gcc/clang · make · gdb · nm/objdump/readelf · pkg-config |
| | | next | Building from source — the toolchain at work |
| | }} |
| | |
| | **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 |
| | |
| | ```sh |
| | 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 |
| | |
| | ```sh |
| | 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 |
| | |
| | ```sh |
| | 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 |
| | |
| | ```sh |
| | 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 |
| | # 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 |
| | ``` |
| | |
| | ```sh |
| | 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 |
| | |
| | ```sh |
| | 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". |
| | |
| | ## The preprocessor's own powers |
| | |
| | ```sh |
| | gcc -E hello.c | less # the expanded source |
| | gcc -dM -E - < /dev/null # every predefined macro |
| | gcc -E -dD hello.c # expand but keep #defines |
| | gcc -M hello.c # header deps — makefile format |
| | gcc -MM hello.c # deps sans system headers |
| | echo | gcc -x c -E -v - # the include search paths |
| | gcc -DFOO=1 hello.c # define on the line |
| | gcc -UFOO hello.c # undefine |
| | #ifdef/#ifndef/#if/#elif/#else/#endif/#define/#undef/#pragma/#error |
| | ``` |
| | |
| | The dep output `-M` is how Makefiles auto-generate `#include` tracking — `gcc -MMD -MP` emits `.d` files make consumes. |
| | |
| | ## The linker and the object formats |
| | |
| | ```sh |
| | ld --verbose | grep SEARCH_DIR # the .so search order |
| | ldd binary # runtime deps resolved |
| | readelf -d binary | grep NEEDED # the recorded deps |
| | readelf -r binary # relocations |
| | objcopy --strip-debug binary # strip sections |
| | strip --strip-unneeded binary # shrink it |
| | ar t lib.a # list archive contents |
| | ar x lib.a # extract |
| | ranlib lib.a # rebuild the index |
| | ldconfig -p | grep libfoo # the registry |
| | ``` |
| | |
| | ELF sections: `.text` code, `.data` initialized data, `.bss` uninitialized, `.rodata` constants, `.init`/`.fini` constructors, `.interp` the dynamic linker path, `.dynamic` the runtime linking table, `.symtab`/`.strtab` symbols, `.debug_*` debug info, `.comment` compiler version. `readelf -S` shows yours. |
| | |
| | ## Shared library versioning — the soname layer |
| | |
| | ```sh |
| | libfoo.so -> libfoo.so.1 -> libfoo.so.1.2.3 # the three-name chain |
| | readelf -d libfoo.so | grep SONAME # the ABI version |
| | ldconfig -p | grep libfoo # the registry resolution |
| | LD_PRELOAD=./my.so prog # inject a .so — the debug/hook |
| | LD_LIBRARY_PATH=/path prog # extra search dirs |
| | ldconfig -n /path # rescan a dir |
| | ``` |
| | |
| | `SONAME` is the ABI contract — `libfoo.so.1` promises compatibility; `libfoo.so.2` breaks it. The `libfoo.so` dev symlink → the versioned file is how `-lfoo` finds it. `ldconfig` rebuilds `/etc/ld.so.cache` from `ld.so.conf.d/` paths. |
| | |
| | ## make — the deeper grammar |
| | |
| | ```makefile |
| | CC = gcc |
| | CFLAGS = -Wall -O2 |
| | OBJS = main.o util.o |
| | |
| | program: $(OBJS) # target: prereqs |
| | $(CC) $(CFLAGS) -o $@ $(OBJS) # $@ = target, $< = first prereq, $^ = all |
| | |
| | %.o: %.c # pattern rule |
| | $(CC) $(CFLAGS) -c $< -o $@ |
| | |
| | .PHONY: clean all install |
| | clean: |
| | rm -f $(OBJS) program |
| | all: program |
| | install: program |
| | install -m755 program /usr/local/bin/ |
| | ``` |
| | |
| | Automatic vars: `$@` target, `$<` first prereq, `$^` all prereqs, `$?` newer prereqs, `$*` stem. `make -pn` dumps the whole rule+variable database; `make -d` debug-traces decisions; `make -B` forces; `make -k` continues on failure. |
| | |
| | ## gdb — the debugger |
| | |
| | ```sh |
| | 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 |
| | |
| | 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 |
| | |
| | - [[Linux]] · [[Building from source]] · [[LFS Cross-Toolchain]] |
| | |
| | [[Category:Linux]] |
| | [[Category:Handbook]] |
| | [[Category:Compilers]] |
| | [[Category:C]] |
| + | [[Category:Linux Handbook]] |
| | |