GCC flags worth knowing by heart
GCC accepts hundreds of options; these are the ~30 that matter. Nearly all work identically in Clang.
Styled for paper — hit Ctrl+P and pin it above your desk.
💡
Your everyday invocation — make it muscle memory (or a shell alias):
gcc -std=c17 -Wall -Wextra -Wshadow -g -Og program.c -o program
Warnings: free code review, on by request
By default GCC is scandalously quiet. Fix that — always:
| flag | what it does | learn |
-Wall | the "sensible warnings" set — badly named: it's nowhere near all. Includes the format-string checks that catch printf mismatches | → lesson |
-Wextra | more good ones: unused parameters, signed/unsigned comparisons… | |
-Wpedantic | complain about anything that isn't strictly ISO C (pair with an explicit -std=) | |
-Werror | promote warnings to errors so nobody can ignore them. Great for your own code; think twice before forcing it on code you distribute — new compiler versions add new warnings and break the build | |
The extras worth adding:
| flag | catches |
-Wshadow | an inner variable hides an outer one — a silent logic-bug generator |
-Wconversion | implicit conversions that can lose data (int rounded = celsius; truncating a double) |
-Wsign-conversion | silent signed↔unsigned changes — the -1 > 1u family of bugs |
-Wdouble-promotion | float silently computed in double — matters for embedded/hot loops |
-Wformat=2 | stricter printf/scanf checking than -Wall, plus warns when the format string isn't a literal (security) |
-Wundef | #if MISPELLED_MACRO silently evaluating as 0 |
-Wvla | variable-length arrays — stack overflows in disguise on untrusted sizes |
-Wwrite-strings | assigning a string literal to char * — modifying one is UB |
-Wnull-dereference | paths that provably dereference NULL |
-Wstrict-prototypes | old-style int f() declarations that accept any arguments (moot in C23, where () finally means (void)) |
Which C? -std=
| flag | meaning | learn |
-std=c17 | portable ISO C17 — today's safe default | → lesson |
-std=c23 | ISO C23 (GCC 14+; on older GCC the working name was -std=c2x) | → lesson |
-std=gnu17 | C17 + GNU extensions — GCC's own default dialect on most installs (GCC 15 moved to gnu23) | → lesson |
-ansi | museum piece: means -std=c90 — you'll meet it in old build scripts | |
Optimization: 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 |
-march=native | use every instruction this CPU supports (AVX2, FMA…) | great for code that runs where it's built; wrong for binaries you ship to older machines |
-flto | link-time optimization — optimize across .c file boundaries | slower builds, real speedups; pass it to both compile and link steps |
-ffast-math | let the optimizer break IEEE float rules | faster, wrong for code that cares about NaN/precision — know what you're signing |
Debug info & sanitizers
| flag | what it does | learn |
-g | embed DWARF debug info mapping machine code back to source lines and variable names. Does not slow the program down — it just makes the file bigger. There is never a reason to omit it during development; ship with it too (you can strip symbols later, but you can't conjure them back when a core dump lands) | → lesson |
-g3 | -g plus macro definitions — lets gdb expand your #defines | |
-fsanitize=address | AddressSanitizer (ASan): catches buffer overflows, use-after-free, stack overflows at the moment they happen; leak check runs at exit. ~2× slowdown — dev/test builds only | → lesson |
-fsanitize=undefined | UBSan: catches signed overflow, bad shifts, null dereference, misaligned access as they execute | → lesson |
-fsanitize=address,undefined | the combo for dev builds — many teams gate every merge on it | |
-fsanitize=thread | TSan: data races. Cannot be combined with ASan; don't mix sanitized binaries with valgrind either | |
-fno-omit-frame-pointer | keeps stack traces (from ASan, perf, gdb) accurate at small cost | |
The plumbing
| flag | does | example / note | learn |
-o file | name the output | gcc app.c -o app — default is the immortal a.out | |
-c | compile to a .o object, don't link | the heart of every Makefile's incremental build | → lesson |
-E / -S | stop after preprocessing / after compiling to assembly | see what the preprocessor or optimizer actually did | → lesson |
-DNAME[=val] | define a macro from the command line | -DDEBUG -DVERSION=3 | → lesson |
-Idir | add a directory to the #include search path | -Iinclude/ — used by the preprocessor | → lesson |
-Ldir | add a directory to the library search path | -Lbuild/lib — used by the linker | → lesson |
-lname | link against libname | -lm → libm, the math library. Order matters: put -l flags after your .c/.o files | → lesson |
-fPIC -shared | build a shared library | gcc -fPIC -shared mylib.c -o libmylib.so | → lesson |
-static | link everything statically — self-contained binary | bigger file, no runtime .so dependencies | → lesson |
Hardening basics (release builds)
| flag | protects against | note |
-D_FORTIFY_SOURCE=2 | adds runtime checks to memcpy/sprintf/… when sizes are known | needs optimization on (-O1+); =3 checks more (GCC 12+, glibc 2.34+) |
-fstack-protector-strong | stack-smashing canaries on vulnerable functions | cheap; distro default in many places |
-fPIE -pie | position-independent executable → full ASLR | compile flag + link flag; default on most modern distros |
-Wl,-z,relro -Wl,-z,now | makes the relocation/GOT tables read-only after startup | "full RELRO" — linker flags, hence the -Wl, prefix |
-fstack-clash-protection | stack-clash attacks via huge stack allocations | cheap insurance |
-fcf-protection | ROP/JOP control-flow hijacks (x86 CET) | hardware-assisted on recent CPUs |
-ftrivial-auto-var-init=zero | reading uninitialized locals becomes reading zeros | GCC 12+; defense-in-depth, not a license to skip initialization — the read is still a bug (→ lesson) |
Put it together
Development — warnings, debuggability, sanitizers:
gcc -std=c17 -Wall -Wextra -Wshadow -Wconversion \
-g -Og -fsanitize=address,undefined \
program.c -o program
Release — optimized, hardened, still debuggable from a core dump:
gcc -std=c17 -Wall -Wextra -O2 -g \
-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fPIE -pie \
-Wl,-z,relro -Wl,-z,now \
program.c -o program -lm
🎉
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.