Skip to content
ALB Wiki

Diff — C Toolchain

Comparing rev 1 → rev 2

{{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]]