⚙️ Compiler & Toolchain Mastery
GCC flags worth knowing by heart
▶ Open the interactive lesson — free, no signupThe example file in this lesson contains two genuine bugs, and plain gcc compiles it without a single complaint. Add four flags and the compiler points at both broken lines before you ever run the program. A handful of well-chosen flags is the closest thing you'll get to a free expert reviewing every line you write — this lesson makes them muscle memory.
GCC accepts hundreds of options. You need maybe twenty — but those twenty are the difference between a professional build and a bug factory. Let's tour them by job: warnings, standards, optimization, debugging, and the everyday plumbing (naming output files, finding headers and libraries).
Warnings: free code review, on by request
By default GCC is scandalously quiet. Fix that — always:
-Wall— the "sensible warnings" set (badly named: it's nowhere near all).-Wextra— more good ones: unused parameters, signed/unsigned comparisons…-Wpedantic— complain about anything that isn't strictly ISO C.-Werror— promote warnings to errors. Great for your own code; be careful forcing it on code you distribute (new compiler versions add new warnings and break the build).
Two underrated gems: -Wshadow (an inner variable hides an outer one — a silent logic-bug generator) and -Wconversion (implicit conversions that can lose data). Watch them catch two real bugs:
#include <stdio.h>
int total(const int *a, int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
int sum = a[i]; /* oops: shadows outer sum */
sum += sum;
}
return sum; /* always returns 0 */
}
int main(void) {
double celsius = 36.6;
int rounded = celsius; /* silently truncates to 36 */
int arr[] = {1, 2, 3};
printf("%d %d\n", total(arr, 3), rounded);
return 0;
}$ gcc warn.c -o warn # not a peep!
$ gcc -Wall -Wextra -Wshadow -Wconversion warn.c -o warn
warn.c: In function 'total':
warn.c:6:13: warning: declaration of 'sum' shadows a previous declaration [-Wshadow]
6 | int sum = a[i];
| ^~~
warn.c:4:9: note: shadowed declaration is here
4 | int sum = 0;
| ^~~
warn.c: In function 'main':
warn.c:14:19: warning: conversion from 'double' to 'int' may change value [-Wfloat-conversion]
14 | int rounded = celsius;
| ^~~~~~~
# two genuine bugs, found for free.🧠 Checkpoint: What does -Werror do?
- Enables every warning GCC knows
- Turns all warnings into hard compile errors
- Prints warnings in red
- Warns about error-handling bugs
Show answer
Turns all warnings into hard compile errors — It promotes warnings to errors so nobody can ignore them. Combine it with -Wall -Wextra in your own projects — but think twice before forcing it on downstream users, since future GCC versions add new warnings.
Which C? -std=
GCC defaults to a GNU dialect (currently gnu17 on most installs). Pin it explicitly: -std=c17 for portable ISO C, -std=c23 for the shiny new stuff from Part 5, or -std=gnu17 to opt into GNU extensions (next lesson!). The museum piece -ansi means -std=c90 — you'll meet it in old build scripts.
Optimization levels: the speed dial
| flag | meaning | trade-off |
|---|---|---|
-O0 | default — no optimization | fast compiles, faithful line-by-line debugging, slow code |
-Og | optimize, but keep debuggable | the sweet spot for development builds |
-O1 | basic optimizations | rarely used directly |
-O2 | full optimization, no size explosion | the production standard |
-O3 | -O2 + aggressive inlining/vectorization | sometimes faster, sometimes bigger & slower — measure! |
-Os | optimize for size | embedded targets, cache-bound code |
#include <stdio.h>
int main(void) {
double sum = 0.0;
for (long i = 1; i <= 200000000L; i++)
sum += 1.0 / (double)i;
printf("%f\n", sum);
return 0;
}$ gcc -O0 bench.c -o bench0 && time ./bench0 19.691044 real 0m1.98s $ gcc -O2 bench.c -o bench2 && time ./bench2 19.691044 real 0m0.61s $ gcc -O2 -march=native bench.c -o benchN && time ./benchN 19.691044 real 0m0.42s # same answer every time — only the speed changes.
Same source, 3× faster — the optimizer kept sum in a register, unrolled the loop, and used vector divides. Add -march=native and GCC may also use every instruction your CPU supports (AVX2, FMA…) — great for code that runs where it's built, wrong for binaries you ship to older machines.
🧠 Checkpoint: You are stepping through code in gdb and variables keep showing <optimized out>. Best flag combo for your dev builds?
- -O3 -g
- -O0 alone
- -Og -g
- -Os -Werror
Show answer
-Og -g — -Og optimizes only in ways that don’t wreck the debugging experience, and -g provides the source mapping. -O3 aggressively deletes/merges variables; -O0 alone works too but lacks debug info without -g.
🤔 With -O2, what does GCC compile return x * 8; into — an imul?
Think first
No — a single left shift: lea eax, [0+rdi*8] (or sal eax, 3). Multiplying by a power of two is just shifting bits left, and the compiler knows it. This is why micro-optimizing x << 3 by hand in source buys you nothing but unreadable code: write x * 8 and let -O2 do its job.
Debug info and sanitizers
-g embeds DWARF debug info — the tables mapping machine code back to your source lines and variable names. Without it, gdb shows you raw addresses. It does not slow the program down; it just makes the file bigger. There is never a reason to omit -g during development.
-fsanitize=address,undefined compiles in runtime detectives that catch buffer overflows, use-after-free, signed overflow and friends the moment they happen. They're the star of the debugging-tools lesson — for now, know that the flag exists and belongs in your dev builds.
The plumbing flags
| flag | does | example |
|---|---|---|
-o file | name the output | gcc app.c -o app (default is the immortal a.out) |
-DNAME[=val] | define a macro from the command line | -DDEBUG -DVERSION=3 |
-Idir | add a directory to the #include search path | -Iinclude/ |
-Ldir | add a directory to the library search path | -Lbuild/lib |
-lname | link against libname | -lm → libm, the math library |
$ cat version.c
#include <stdio.h>
#include <math.h>
int main(void) {
#ifdef DEBUG
fprintf(stderr, "debug build v%d\n", VERSION);
#endif
printf("sqrt(2) = %f\n", sqrt(2));
return 0;
}
$ gcc -DDEBUG -DVERSION=3 version.c -o version -lm
$ ./version
debug build v3
sqrt(2) = 1.414214
# no -DDEBUG? The #ifdef block simply vanishes at preprocess time.🧠 Checkpoint: What does -Iinclude/ do?
- Links the library "include"
- Adds include/ to the directories searched for
#includeheaders - Installs headers into include/
- Ignores all headers in include/
Show answer
Adds include/ to the directories searched for #include headers — Capital -I extends the header search path (used by the preprocessor). Its sibling -L extends the library search path, and lowercase -l names an actual library to link. Three different letters, three different stages.
Your everyday invocation — make it muscle memory (or a shell alias):gcc -std=c17 -Wall -Wextra -Wshadow -g -Og program.c -o program
Ship with -O2 and keep -g anyway — you can strip symbols later, but you can't conjure them back when a core dump lands on your desk.
Clang compatibility: nearly every flag on this page works identically in Clang — clang -std=c17 -Wall -Wextra -O2 just works. The two compilers deliberately share a command-line dialect, so learning one is learning both. Clang's error messages are famously friendly; try both on the same buggy file some day.
Practice
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
Flags configure the standard compiler — but GCC also speaks a whole private dialect of C, and that's where we go next.
▶ Practice this lesson interactively (with live gcc)