C — the GCC Toolchain GCC 15 · binutils · gdb · make · 683 entries

Written for someone who already knows what a translation unit is, and wants the toolchain rather than the language — the language is on its own sheet. gcc is not a compiler; it is a driver that runs cc1, as and ld, and knowing which of the three produced a message is most of knowing what to do about it. Cards 1–20 are the working guide: the collection and what is really in it, the four phases, the gnu23 default that GCC 15 changed under you, the preprocessor's four search lists, warnings as a taxonomy rather than a wall, what each -O level actually turns on, LTO and PGO, debug info and gdb, the sanitizers, -fanalyzer, static and shared linking, choosing a linker, reading a binary with binutils, profiling and coverage, make, CMake and Ninja, the GNU extension surface, cross-compiling for the bench, and how to get a real GCC onto a Mac. The remaining 19 cards index the flags, attributes, builtins, predefined macros, pragmas, gdb commands and tools. 627 of the 683 entries also work in Clang; the 49 marked in blue are GNU-only, and the 7 marked in orange are spelled the same there and mean something else.

Dots: also works in Clang GCC / GNU binutils only Clang spells it the same, differently
Sources: the GCC 15 manual (Using the GNU Compiler Collection, GNU FDL), the GNU Binutils, GNU Make and GDB manuals, and ISO/IEC 9899:2024 where the language is mentioned at all. Behaviour was checked against GCC 15 on Linux; the macOS notes were checked against Homebrew GCC 15 beside Apple clang on macOS 26. Hover a clipped row for the whole entry. Companion sheets: C — the Language and C — the Clang Toolchain.

The Working Guide

GCC 15 as a toolchain — the driver and its four phases, the flags that matter, and the binutils programs that answer every question about what came out

The Collection

what “gcc” actually is

gcc is not a compiler. It is a driver — an argument-shuffling front end that decides which real programs to run, in what order, with what arguments. Knowing the four it runs is most of knowing why a build failed.

ProgramIsComes from
gccthe driver; runs the othersgcc
cc1the C compiler proper — parses, optimises, emits assemblygcc (not on PATH; a libexec program)
asthe assemblerbinutils
collect2ldthe linkerbinutils
cppthe preprocessor — built into cc1 since GCC 3, kept as a programgcc

So half the errors people call "gcc errors" come out of binutils. undefined reference to is ld. Error: no such instruction is as. Read the first word of the message.

Where the pieces live

gcc -print-prog-name=cc1the real compiler's path — proves you have GCC and not a shimgcc -print-search-dirsevery directory it will look in, programs then librariesgcc -print-file-name=libc.aresolve one library to a pathgcc -dumpmachinethe target triple this build producesgcc -dumpversion15 — just the major, unlike --versiongcc -v -E - < /dev/nullthe include search path, in the order it is searched

The version that matters

gcc 15 (2025)default dialect moved to gnu23; C23 essentially complete; -fanalyzer maturedgcc 14gnu17 default; -Wnrvo, -fhardenedgcc 13C23 work begins in earnest; -Wenum-int-mismatch__GNUC__ __GNUC_MINOR__15 and 1 — but see the warning below
__GNUC__ does not mean GCC. Clang, ICC and several vendor compilers define it too, to claim GCC compatibility — Clang has said __GNUC__ 4, __GNUC_MINOR__ 2 for fifteen years and will not move. To test for real GCC you need both halves: #if defined(__GNUC__) && !defined(__clang__) && !defined(__INTEL_COMPILER).

Is this machine's cc really GCC?

cc --versionthe honest answer; on a Mac it prints "Apple clang"gcc -print-prog-name=cc1GCC prints a path; a Clang shim prints "cc1" straight backecho | cpp -dM - | grep __clang__if anything comes back, it is not GCC

On macOS /usr/bin/gcc is a Clang shim and always has been. Real GCC from Homebrew installs version-suffixed — gcc-15, g++-15 — precisely so it cannot be mistaken for it.

Driving the Compiler

four phases, one command

Every invocation runs some prefix of the same four stages. The flag that stops it names the stage you want.

gcc -E f.cpreprocess only → stdoutgcc -S f.c→ f.s, assemblygcc -c f.c→ f.o, relocatable objectgcc f.c -o progall four, then linkgcc -c a.c b.c && gcc a.o b.o -o progthe separate-compilation shapegcc -fsyntax-only f.cparse and type-check, emit nothing — the fast check
-fsyntax-only does not run the optimiser, and GCC's dataflow warnings (-Wuninitialized, -Wmaybe-uninitialized, most of -Wanalyzer-*) are computed during optimisation. A file that is clean under -fsyntax-only can be filthy under -O2 -Wall.

Seeing what the driver decided

gcc -v f.crun, printing every sub-process with its full argvgcc -### f.cprint the sub-commands, quoted, and run NOTHINGgcc -save-temps f.ckeep f.i, f.s, f.o beside the sourcegcc -save-temps=objkeep them beside the object instead of in cwdgcc -Q --help=optimizers -O2every optimiser flag and whether -O2 enabled itgcc -Q --help=warningsthe same for warnings — the ground truth for what -Wall covers

-### is the honest one: exactly what would run, without the noise of running it. Reach for it the moment a command works by hand and fails under the build system — the difference is always visible in the argv.

Passing arguments past the driver

FlagGoes to
-Wp,-optthe preprocessor
-Wa,-optas — e.g. -Wa,-march=armv8.2-a
-Wl,-optld — commas become argument separators
-Xlinker argthe linker, one argument verbatim (use when the argument contains a comma)
-Xassembler argthe assembler, one argument verbatim
--param name=va numeric tuning knob of the optimiser; --help=params lists them
-fplugin=x.soa GIMPLE pass plugin GCC only
-Wl,-rpath,/opt/libidentical to -Xlinker -rpath -Xlinker /opt/lib-Wl,--start-group,-la,-lb,--end-groupone -Wl can carry several-Wl,-rpath,'$ORIGIN/../lib'quote it — the shell must not expand $ORIGIN

Input language and file kinds

-x ctreat what follows as C regardless of suffix-x c-headerbuild a precompiled header (.gch)-x nonego back to guessing by suffixgcc -x c - <<< 'int main(){}'compile from stdin.c .i .s .S .o .a .soC, preprocessed C, asm, asm-needing-cpp, object, archive, shared

Uppercase .S goes through the preprocessor; lowercase .s does not. This is why half of all hand-written assembly builds fail on the first try.

Long command lines

gcc @args.rsp -o progread arguments from a file, whitespace-separated-pipepipes rather than temp files between stages-o -write to stdout (with -E or -S)-fmax-errors=3stop after three, before the screen fills
Order matters more than people expect. Later flags beat earlier ones (-O0 -O2 is -O2); -Wno-x only cancels warnings enabled to its left; and objects and libraries are resolved strictly in the order given on the link line. A build system that appends CFLAGS after its own is telling you it expects to be overridden. One that prepends them is telling you it does not.

Dialects & Defaults

and what GCC 15 changed under you

GCC 15 moved its default from -std=gnu17 to -std=gnu23. That is the largest default-dialect change since C99, and it breaks working code silently — the same source still compiles under GCC 14, under Clang, and on the build machine that has not been upgraded yet.

What the gnu23 default breaks. bool, true, false, nullptr, static_assert, thread_local and alignas became keywords, so typedef enum { false, true } bool; is now a syntax error. foo() in a declaration now means foo(void), not "unspecified arguments", so a call with the wrong count is an error rather than undefined behaviour. K&R definitions are gone. And an enum may now have an underlying type wider than int. State -std= explicitly in every Makefile and this whole class of surprise disappears.

The dialect axis

-std=c90 / c89 / iso9899:1990ANSI C. -ansi is a synonym for -std=c90-std=c99// comments, declarations anywhere, VLAs, restrict, _Bool, designated initialisers-std=c11_Generic, _Static_assert, anonymous struct/union, atomics, threads-std=c17 / c18C11 with the defect reports applied; no new features-std=c23ISO/IEC 9899:2024 — see the C23 card on the Language sheet-std=c2ythe next one, in flux; do not ship against it-std=gnuNNthe same dialect plus the GNU extensions

The gnu variants are not "looser C". They are the same strict dialect plus statement expressions, typeof, nested functions GCC only, case ranges, computed goto, __attribute__, binary literals before C23 standardised them, and several hundred builtins. See the extension card.

Turning strictness up

-pedanticdiagnose anything the chosen -std forbids-pedantic-errorsmake those hard errors — the real conformance switch-Wpedanticthe same thing, spelled as a warning-std=c17 -pedantic-errorsthe closest a compiler comes to enforcing "portable C"-fno-asmdrop asm/inline/typeof as keywords, keeping __asm__ etc.-funsigned-charpin plain char signedness — it differs per target
-pedantic is not conformance. It emits the diagnostics the standard requires; it does not disable the extensions, and it will still happily accept __attribute__ and __builtin_expect. A program clean under -pedantic-errors can still be full of undefined behaviour — that is what the sanitizers are for.

Freestanding and the odd corners

-ffreestandingno hosted library assumptions; only the freestanding headers exist-fno-builtinstop treating memcpy, strlen, printf as known functions-fno-commontentative definitions get their own section — the default since GCC 10-fshort-enumsABI-changing; must match across every object and library-fsigned-bitfieldsplain int bitfield signedness, which is implementation-defined
-fno-builtin and -ffreestanding change code generation, not just the headers. A kernel or firmware build that omits them will find GCC turning a hand-written loop into a call to a memcpy that does not exist at that link stage.

The Preprocessor

search order, and getting deps right

The preprocessor's whole job is textual, and almost every mystery in it is a search-order question. GCC searches four lists, in this order, and -v prints them.

Directory listSearched forAdded by
the including file's own directory"quoted" onlyimplicit
the -iquote list"quoted" only-iquote dir
the -I listboth forms-I dir
the system listboth forms-isystem dir, then the built-ins
Use -isystem for third-party headers, not -I. Headers found through the system list have their warnings suppressed, so somebody else's -Wsign-compare noise stops drowning yours. That is the entire difference, and it is worth having.

Defining and interrogating

-DNAME=value / -DNAMEdefine; bare -DNAME defines it as 1-UNAMEundefine, after all the -D on the line-include hdr.has if #include "hdr.h" were the first line — how config.h is delivered-imacros hdr.htake its macros only, discard its declarations-nostdincdrop the built-in system list entirelyecho | gcc -dM -E -every predefined macro — the single most useful preprocessor commandgcc -dM -E f.cthe macros in force at the end of f.cgcc -E -dD f.cpreprocessed output WITH the #defines kept

Dependency generation, the part that makes builds correct

-MMD -MPthe pair you actually want, alongside -c-MDas -MMD but includes system headers — usually noise-MF f.dwhere to write the rule-MT targetoverride the target name in the emitted rule-M / -MMprint the rule to stdout and compile nothing
-MP is the one people leave out. It emits a phony target for every header. Without it, deleting or renaming a header makes make stop dead with "No rule to make target 'old.h'" until you wipe the .d files by hand. With it, make just rebuilds. Cost: nothing.

Header hygiene

#pragma oncesupported by GCC, Clang and MSVC; not ISO, but universal in practice#ifndef H_ / #define H_ / #endifthe portable guard; the name must be unique across the whole program-Hprint the inclusion tree, indented — finds the header you did not know you pulled in-fdirectives-only -Eexpand directives but not macros; for fast preprocessing

Macro traps worth the ink

#define SQ(x) ((x)*(x))parenthesise every parameter AND the whole bodySQ(i++)evaluates i++ twice — no parenthesising fixes this#define MAX(a,b) ({typeof(a) _a=(a),_b=(b); _a>_b?_a:_b;})the GNU statement-expression fix GCC onlydo{...}while(0)the wrapper that makes a multi-statement macro safe after if#x / a##bstringify; token-paste. Both need a second level of macro to expand arguments first

Warnings, Properly

a taxonomy, not a wall

GCC ships hundreds of warnings and enables perhaps a fifth of them. Treating them as a list to switch on is how people end up with -Wno-everything in a Makefile. Treat them as four groups instead.

GroupWhat it meansPolicy
Almost always a bug-Wreturn-type, -Wuninitialized, -Wformat, -Wimpliciterror, no exceptions
Usually a bug-Wshadow, -Wsign-compare, -Wnull-dereferenceerror in new code
House style-Wpadded, -Wswitch-enum, -Wconversionopt in per project
Noise-Wunused-parameter in callback-heavy codesuppress precisely, never globally

The bundles, and what they leave out

-Wallbadly named — it is "the warnings we are confident about". About 60 of them-Wextraanother 30; -Wall -Wextra is the real baseline-Wpedanticnon-conforming constructs for the chosen -std-Wall -Wextra -Wpedanticstill misses -Wshadow, -Wconversion, -Wcast-qual, -Wwrite-strings-Werrorpromote every warning to an error-Werror=return-typepromote exactly one — far more useful than blanket -Werror-Wno-error=deprecated-declarationsdemote one back under a blanket -Werror
-Werror belongs in CI, not in the tarball. It makes your release unbuildable on the next compiler version, which will invent warnings you have never seen. Ship warnings on and -Werror off; run CI with -Werror so nobody can merge past it.

The additions worth making by hand

-Wshadowa local hiding an outer name; the classic refactoring bug-Wconversion -Wsign-conversionevery implicit narrowing; loud at first, then quiet forever-Wcast-qualcasting away const or volatile-Wwrite-stringsstring literals get const char[], so writing to one is caught-Wstrict-prototypes -Wold-style-definitionpre-C23 declaration hygiene-Wvlaban variable-length arrays — unbounded stack growth-Walloca -Walloc-zerothe same argument, for alloca and zero-sized allocations-Wdouble-promotionessential on a float-only MCU-Wformat=2-Wformat plus -Wformat-security and -Wformat-nonliteral-Wswitch-enumevery enumerator must be handled, even with a default-Wimplicit-fallthrough=5only [[fallthrough]] counts; comments no longer do

Suppressing precisely

#pragma GCC diagnostic pushsave the current state#pragma GCC diagnostic ignored "-Wunused"off for this region only#pragma GCC diagnostic poprestore — always pair them__attribute__((unused))this parameter or variable is deliberately unused(void)x;the portable version of the same statement-isystemsilence a whole third-party tree, properly

A #pragma GCC diagnostic without a matching pop leaks to the end of the translation unit — and because it is textual, to every file that includes the header it sits in. The two commonest ways a project "loses" a warning are an unpaired pragma and an over-broad -Wno- in CFLAGS.

Making the output readable

-fdiagnostics-color=alwayskeep colour through a pipe to less -R-fdiagnostics-show-optionon by default; names the flag that fired-fdiagnostics-path-format=inline-events-fanalyzer paths drawn in the source-fdiagnostics-format=sarif-filemachine-readable, for CI annotation GCC only-fmax-errors=Nstop after NGCC_COLORS=set empty to turn colour off entirely

Optimisation

what each -O really turns on

-O levels are not a speed dial. Each is a named set of passes, and gcc -Q --help=optimizers -O2 prints exactly which. The useful mental model is what each level costs you.

LevelTurns onCosts
-O0nothing; every variable lives in memoryspeed. The default
-Ogoptimisations that do not confuse the debuggeralmost nothing — the right default for development
-O1~50 passes; no big compile-time gamblesa little debuggability
-O2~90 passes: inlining, vectorisation (GCC 12+), schedulingdebuggability. The release default
-O3aggressive inlining and loop transformscode size; sometimes speed, via icache
-Os-O2 minus what grows the binarya few per cent of speed
-Ozsize at any costreal speed. Firmware only
-Ofast-O3 plus -ffast-math plus non-conforming behaviourcorrectness — see below
Do not ship -Ofast. It implies -ffast-math, which sets FTZ/DAZ in the floating-point control word process-wide — including for libraries that never asked for it — and permits reassociation that changes results. NaN and infinity handling stop being guaranteed. If you need fast maths, enable the specific piece you can defend (-fno-math-errno, -fassociative-math) and say why in a comment.

Telling GCC about the machine

-march=x86-64-v3a portable microarchitecture level (AVX2, BMI2) — better than naming a chip-march=nativethis exact CPU. Never for anything you distribute-mtune=nativeschedule for this CPU, but keep the baseline ISA. Safe to ship-march=armv8.2-a+cryptoAArch64 features are additive with +-mcpu=cortex-m4 -mfpu=fpv4-sp-d16the embedded Arm spellinggcc -march=native -Q --help=targetwhat "native" resolved to on this machine

The passes worth knowing by name

-finline-functionsat -O2 up; --param max-inline-insns-auto tunes it-funroll-loopsnot in any -O level; measure before believing it-ftree-vectorizeon at -O2 since GCC 12, -O3 before that-fopt-info-vec-missedwhy a loop did NOT vectorise — the single most useful -fopt-info-fno-semantic-interpositionlet a shared library inline its own calls; big win, changes override semantics-fomit-frame-pointeron by default; -fno-omit-frame-pointer for profilers-fno-strict-aliasingthe escape hatch. See the Language sheet on aliasing before you reach for it

Seeing inside

-fopt-info-all=opt.txtevery optimisation decision, to a file-fdump-tree-all / -fdump-rtl-allthe IR after every pass. Dozens of files GCC only-fdump-tree-optimizedthe one dump most people want: final GIMPLE-fverbose-asm-S output with the variable names in comments-fstack-usagea .su file per translation unit, per function. Firmware gold-fcallgraph-infothe call graph, for stack-depth analysis
Optimisation is where undefined behaviour becomes visible. Signed overflow, strict aliasing and a null check written after the dereference are all invisible at -O0 and load-bearing at -O2. When a bug appears only in the release build, the answer is almost never "a compiler bug" — build the same source with -fsanitize=undefined -O2 and it usually names itself.

LTO & PGO

the two whole-program optimisations

Both trade build time for run time, and both are switched on with a pair of flags most people get half right.

Link-time optimisation

-flto=autothe modern spelling — one job per core. Put it on BOTH the compile and the link line-flto=1serial; use when a parallel LTO link exhausts memory-ffat-lto-objectsemit real machine code as well as GIMPLE, so the .o works without LTO-fno-fat-lto-objectsthe default; smaller and faster, but the .o is useless to a normal link-flto-partition=oneone partition — best code, worst link time
Under LTO, use gcc-ar, gcc-nm and gcc-ranlib. A plain ar writes an archive index that cannot see inside GIMPLE objects, so the link silently drops symbols and you get undefined reference to a function that is demonstrably in the archive. The gcc- wrappers load the LTO plugin; ar --plugin $(gcc -print-file-name=liblto_plugin.so) is the long way round.
LTO is where sloppy declarations finally bite. Two translation units declaring the same symbol with different types compiled fine for twenty years; with LTO, GCC can see both and says -Wlto-type-mismatch. That warning is not LTO breaking your program — it is LTO finding a bug that was always undefined behaviour.

Profile-guided optimisation

gcc -fprofile-generate -O2 …build the instrumented binary./prog typical-workloadrun it. Representative input matters more than the flagsgcc -fprofile-use -O2 …rebuild, using the .gcda files-fprofile-dir=DIRwhere the data goes; needed the moment the build tree is not the run tree-fprofile-partial-trainingdo not pessimise code the profile never covered-fprofile-update=atomicrequired if the workload is threaded, or the counters race-Wmissing-profilewarn when -fprofile-use finds no data for a function

Real gains from PGO are 5–15% on branch-heavy code — interpreters, parsers, compression — and close to nothing on numerical kernels, where the branches were predictable anyway. Measure before adopting the workflow; it is a permanent tax on your build.

The combination

-O2 -flto=auto -fprofile-use -fno-semantic-interpositionwhat a distribution actually ships-fprofile-use -fltoworks, and is where LTO pays best — the profile tells the inliner what matters

Debug Info & gdb

making a binary answer questions

Debug info is a separate axis from optimisation. -O2 -g is legal, normal, and what you should ship for anything you might have to debug in the field.

-gDWARF, default level 2-g3level 2 plus macro definitions, so gdb can expand them-g1backtraces only — small, and enough for a crash report-gdwarf-5the DWARF version; GCC 11+ default-ggdb3maximum, with GCC/gdb extensions-Og -gthe development pair-fno-omit-frame-pointera frame pointer for perf and for cheap unwinding-fasynchronous-unwind-tableson by default on x86-64; needed for unwinding from a signal

Splitting debug info out

objcopy --only-keep-debug prog prog.debuglift it outobjcopy --strip-debug progshrink the shipped binaryobjcopy --add-gnu-debuglink=prog.debug progleave a pointer gdb will follow-gsplit-dwarfemit .dwo files at compile time instead — much faster links-Wl,--build-ida hash gdb and debuginfod use to find the symbols later
Keep the unstripped binary for every release you ship. A stripped crash dump plus the matching unstripped binary is a full backtrace; a stripped crash dump on its own is a list of hex addresses that nobody will ever resolve. Store it by build-id.

gdb, the ten commands that cover most sessions

gdb --args ./prog a bstart with argv already setb f.c:42 / b func / b f if x==3breakpoints, plainlyrun / c / n / s / finishstart, continue, over, into, outbt / bt fullthe stack; "full" adds every frame's localsp expr / p/x n / p *arr@10evaluate; hex; ten array elementsinfo locals / info args / info registersframe statewatch xstop when x is written — hardware watchpoint where availablecatch throw / catch syscall writestop on an event rather than a linelayout srcthe curses source view (Ctrl-X A toggles)gdb -batch -ex bt -ex 'info locals' ./prog corescripted post-mortem

Cores, and why you have none

ulimit -c unlimitedthe shell limit — the usual reason there is no corecat /proc/sys/kernel/core_patternwhere they go; often piped to systemd-coredumpcoredumpctl list / coredumpctl gdbthe systemd path, which is most Linuxes nowgdb ./prog corethe direct pathset debuginfod enabled onfetch distribution debug info on demand

value optimized out is not a gdb failure — it is -O2 telling the truth: that variable never had a home in memory. Rebuild that one file at -Og rather than fighting it.

Sanitizers

the debugger for problems with no line number

Sanitizers are compile-time instrumentation plus a runtime library. They do not replace warnings or the analyzer — they catch the things only visible while the program runs, and they cost 2–20× in speed, so they belong in the test suite rather than production.

FlagCatchesCost
-fsanitize=addressoverflow, use-after-free, double free, leaks (Linux)~2×, ~3× memory
-fsanitize=undefinedsigned overflow, bad shifts, null deref, misaligned access, bad enum~1.2×
-fsanitize=threaddata races — real ones, not "suspicious"~10×, ~7× memory
-fsanitize=leakleaks alone, without the rest of ASansmall
-fsanitize=pointer-comparecomparing pointers into different objectswith ASan
-fsanitize=bounds-strictevery array index, including trailing flexible memberssmall
ASan and TSan cannot be combined — they both own the allocator and the shadow map. UBSan combines with either. And every sanitizer must be on the link line as well as the compile line, or the runtime is missing and you get pages of undefined references to __asan_*.
GCC has no MemorySanitizer. Uninitialised-memory reads are Clang's -fsanitize=memory only, and it requires every library in the process to be instrumented too. Under GCC the nearest equivalents are -Wmaybe-uninitialized -O2, -ftrivial-auto-var-init=pattern and valgrind's memcheck.

Making the output useful

-fsanitize=undefined -fno-sanitize-recover=allabort on the FIRST finding rather than logging and continuing-fno-omit-frame-pointer -galways, or the report has no readable stackUBSAN_OPTIONS=print_stacktrace=1UBSan prints no stack without this. Set it in the test harnessASAN_OPTIONS=detect_leaks=1:abort_on_error=1leaks on, and stop where a debugger can see itASAN_OPTIONS=detect_stack_use_after_return=1off by default; catches a whole bug familyLSAN_OPTIONS=suppressions=lsan.suppfor leaks inside libraries you do not own-fsanitize-recover=allthe opposite: log everything, keep going, for a survey pass

Hardening — the instrumentation you leave on

-D_FORTIFY_SOURCE=3needs -O1 or better; checks sizes at run time. Level 3 handles dynamic sizes-fstack-protector-strongcanaries where they matter; -all is rarely worth it-fstack-clash-protectionprobe each stack page — defeats stack-clash attacks-fcf-protection=fullx86 CET: endbr64 and shadow stack-fPIE -pieaddress-space randomisation for the executable itself-Wl,-z,relro -Wl,-z,nowfull RELRO — the GOT is read-only after load-Wl,-z,noexecstackno executable stack; the linker warns if an object asks for one-ftrivial-auto-var-init=zerozero every uninitialised local. Cheap defence in depth-fhardenedGCC 14+: most of the above in one flag GCC only

-fhardened is the flag to reach for first on a new project; it turns on _FORTIFY_SOURCE=3, -fstack-protector-strong, -fstack-clash-protection, -fcf-protection=full, PIE and full RELRO, and it will keep tracking the consensus as it moves. It is not retroactive: it does not touch -D_FORTIFY_SOURCE if you already set it.

-fanalyzer

the part of GCC that reads your code

GCC's static analyzer runs a path-sensitive symbolic execution over the whole function, and it is the only part of GCC that can tell you how a bug happens rather than where. It arrived usable in GCC 11 and is genuinely good by GCC 15. It is C-only.

-fanalyzerturn it on. Expect the build to take 2-5x longer-fanalyzer -fdiagnostics-path-format=inline-eventsdraw the path in the source. Use this always-fdiagnostics-format=sarif-filemachine-readable output for CI-Wno-analyzer-malloc-leakeach check is a normal warning and can be disabled by name--param analyzer-max-enodes-per-program-point=…when it gives up on a big function__attribute__((malloc(free_fn)))teach it your allocator/deallocator pairs

What it finds that -Wall -Wextra does not

-Wanalyzer-double-freealong a specific path, through several functions-Wanalyzer-use-after-freesame-Wanalyzer-malloc-leakan allocation with a path where nothing frees it-Wanalyzer-null-dereferencea NULL that reaches a dereference-Wanalyzer-use-of-uninitialized-valuepath-sensitive, unlike -Wmaybe-uninitialized-Wanalyzer-file-leaka FILE* that escapes without fclose-Wanalyzer-fd-leak / -fd-use-after-closethe same for file descriptors-Wanalyzer-va-arg-type-mismatchvarargs read back at the wrong type-Wanalyzer-tainted-array-indexneeds -fanalyzer-checker=taint
Taint mode is the one people miss. -fanalyzer -fanalyzer-checker=taint marks data that came from outside the program — a read, a socket, getenv — and follows it into array indices, allocation sizes, divisors and loop bounds. Annotate your own entry points with __attribute__((access(read_only, 1, 2))) and it follows those too.

What else to run, and why it is not redundant

ToolFindsNotes
-fanalyzermemory and resource lifetimes, along pathsin the compiler; no separate build
cppcheck --enable=alla wide, shallow net; style and portabilityparses without a full build; noisy but cheap
valgrind --tool=memcheckuninitialised reads, invalid accessno rebuild at all; ~20×, but finds what ASan misses
clang-tidymodernisation, bugprone patterns, CERT rulesneeds compile_commands.json; works fine on a GCC project
frama-cproof, given ACSL contractsa different activity, not a linter

The analyzer and the sanitizers are complementary, not alternatives: the analyzer reasons about paths it can see statically and reports things that may never happen; the sanitizers report only what actually happened, but they are never wrong about it.

Static Linking

archives, order, and the symbol resolver

An archive is a container with an index, not a library in any deeper sense. ld walks the link line left to right, keeping a set of still-undefined symbols, and pulls a member out of an archive only if that member defines something currently undefined. This one rule explains nearly every static-link failure.

ar rcs libx.a a.o b.ocreate/replace, with an index (s)ar t libx.alist membersar x libx.a a.oextract oneranlib libx.arebuild the index; the same as ar snm --defined-only libx.awhat it offersgcc main.o -L. -lx -o proglibraries AFTER the objects that need them
gcc -lx main.o silently links nothing from libx. At the point ld reads the archive, no symbol is undefined yet, so no member is pulled in; then main.o arrives and needs one. Libraries go last, and a library that needs another goes before it. Circular dependencies need -Wl,--start-group … -Wl,--end-group, which rescans until nothing changes.

Forcing the issue

-Wl,--whole-archive -lplugin -Wl,--no-whole-archivetake EVERY member, needed or not — for self-registering code-Wl,-u,symbolpretend `symbol` is undefined, so its member is pulled-Wl,--start-group -la -lb -Wl,--end-groupbreak a cycle between two archives-staticlink everything statically, libc included-static-libgcconly the compiler runtime
Fully static glibc programs are a trap. getaddrinfo, getpwnam and the rest of NSS dlopen their backends at run time, so a -static binary either fails or silently behaves differently on a machine whose /etc/nsswitch.conf differs. If you want a genuinely static binary, build against musl. On macOS, static linking of libSystem is not supported at all.

Reading a failed link

undefined reference to `foo'nothing on the line defines it, or it came too earlymultiple definition of `foo'a definition in a header; add static or inline, or use extern in the headerundefined reference to `foo(int)'C++ mangling — you needed extern "C"relocation R_X86_64_32S … cannot be useda non-PIC object being linked into a shared library-Wl,--trace-symbol=fooprint every file that mentions foo-Wl,-Map=out.mapthe full map: what came from where, and why-Wl,--crefa cross-reference table of symbol to file

Shared Libraries

PIC, sonames and visibility

A shared object is loaded at an address nobody chose in advance, so every object in it must be position-independent, and every symbol it exports is a permanent interface.

gcc -fPIC -c a.c b.cposition-independent code, one object at a timegcc -shared -o libx.so.1.2.3 a.o b.obuild itgcc -shared -Wl,-soname,libx.so.1 …the name recorded in every program that links itln -s libx.so.1.2.3 libx.so.1the runtime linkln -s libx.so.1 libx.sothe development link, which -lx findsgcc main.c -L. -lx -Wl,-rpath,'$ORIGIN/../lib'find it at run time, relative to the executable
NameCalledUsed at
libx.sothe linker namelink time, by -lx
libx.so.1the sonamerun time, by the loader
libx.so.1.2.3the real nameon disk
Bump the soname when, and only when, you break the ABI. Adding a function is compatible; changing a struct's size or a function's signature is not. The soname is the entire mechanism by which two versions of a library coexist on one machine — getting it wrong is how you get a program that crashes after an unrelated package update.

Visibility — the flag that should be a default

-fvisibility=hiddennothing is exported unless it says so. Put it in CFLAGS on day one__attribute__((visibility("default")))this one is public#define API __attribute__((visibility("default")))the usual macro-Wl,--version-script=x.mapthe same, with versioned symbols; what glibc itself does-Wl,--exclude-libs,ALLdo not re-export symbols from the static archives you swallowednm -D --defined-only libx.socheck what you really export

A default-visibility library exports every non-static function, which means faster growth in load time, a larger dynamic symbol table, real risk of colliding with another library's init(), and an ABI you never agreed to. Hiding by default typically cuts the exported table by 90% and speeds up startup measurably.

Link-time rules worth enforcing

-Wl,-z,defsrefuse to build a .so with unresolved symbols. Should be on always-Wl,--as-neededdrop DT_NEEDED entries the binary does not actually use (default on most distros)-Wl,--no-undefineda synonym for -z defs-Wl,-z,relro -Wl,-z,nowfull RELRO-fno-semantic-interpositionlet the library inline its own calls; loses LD_PRELOAD override of internal calls

Loading by hand

dlopen("libx.so.1", RTLD_NOW|RTLD_LOCAL)RTLD_NOW surfaces missing symbols immediatelydlsym(h,"f")NULL is a legal return; check dlerror(), not the pointer-ldlneeded before glibc 2.34; folded into libc since-rdynamicexport the executable's own symbols so a plugin can call backreadelf -d libx.soNEEDED, SONAME, RPATH, RUNPATH — safer than ldd, which runs the programLD_DEBUG=libs ./progwatch the loader search

Which Linker

bfd, gold, lld, mold

GCC forks whatever ld it finds. On a large C++ or LTO link that choice is the difference between a one-second and a forty-second edit cycle; on an ordinary C program it barely matters.

LinkerSpeedNotes
ld.bfd1× (the baseline)binutils. Handles every format and every linker script. The default
ld.gold~2×ELF only; deprecated and unmaintained since binutils 2.44
ld.lld~4×LLVM's; ELF, COFF and Mach-O. Works fine driving GCC
mold~10×ELF (and Mach-O in sold). Drop-in; the choice for a big link
gcc -fuse-ld=lld …pick one, by namegcc -fuse-ld=mold …same; needs binutils 2.42+ or a mold-wrappergcc -B/usr/libexec/mold …the older way, pointing at a directory holding an `ld`gcc -Wl,--versionwhich linker actually ranld --help | head -1the same question, asked directly
Linker scripts are bfd's domain. Firmware, kernels and anything with a custom memory map depend on MEMORY/SECTIONS scripts that lld implements partially and mold not at all. If your build has a .ld file, stay on ld.bfd and take the seconds.

The linker options you will actually type

-Wl,-Map=out.mapthe map file: every section, every symbol, and which object supplied it-Wl,--gc-sectionsdrop unreferenced sections; pair with -ffunction-sections -fdata-sections-Wl,--print-gc-sectionswhat --gc-sections removed. Firmware size work starts here-Wl,-T,link.lduse this linker script-Wl,--defsym,sym=0x8000define a symbol on the command line-Wl,--wrap=mallocredirect calls to __wrap_malloc; the interposition trick for tests-Wl,--no-undefined -Wl,-z,defsfail early rather than at dlopen time-Wl,--sort-section=alignmenta little size, free

Section garbage collection, properly

CFLAGS += -ffunction-sections -fdata-sectionsone section per function and per objectLDFLAGS += -Wl,--gc-sectionsthen let the linker discard what nothing reachesKEEP(*(.vectors))in the linker script, for sections only hardware references__attribute__((used))the source-level version of KEEP

Typical saving on an embedded build: 20–40% of flash. On a hosted program it is usually noise, because the libc you link against was not built that way.

Reading a Binary

binutils, and what to ask it

Every question about a built artefact — why is it this size, what does it need, which compiler made it, why does it not start — is answered by four binutils programs.

file progformat, architecture, PIE or not, stripped or not. Always the first questionsize -A -d progper-section sizes; .text .rodata .data .bssnm --size-sort -S prog | tail -20the twenty biggest symbols. Where the size wentnm -u progwhat it still needs from elsewherenm -D --defined-only libx.sowhat a shared object actually exportsreadelf -d progNEEDED, SONAME, RPATH, RUNPATH — safer than lddreadelf -p .comment progwhich compiler and version built each objectreadelf -n prognotes: build-id, ABI tag, property flagsstrings -a -n 8 progprintable runs; -a because the default skips non-loadable sections
Do not use ldd on a binary you do not trust. On glibc it works by running the program with a special loader environment — it is not a passive inspection. readelf -d and objdump -p answer the same question without executing anything.

Disassembly

objdump -d progdisassemble the executable sectionsobjdump -dS progwith the source interleaved. Needs -g and the sources still in placeobjdump -d -M intel progIntel syntax rather than AT&Tobjdump -h progsection headers, sizes and flagsobjdump -T libx.sothe dynamic symbol tableobjdump -R progdynamic relocations — what the loader will patchgcc -S -fverbose-asm -O2 f.coften better: the compiler's own output, with variable namesc++filt _Z3fooidemangle a C++ name that got into your C link

Symbols: the three columns that matter

ColumnValuesMeans
typeT t / D d / B b / U / Wtext / data / bss / undefined / weak. Lowercase = local
bindingGLOBAL, LOCAL, WEAKwhether another object may resolve to it
visibilityDEFAULT, HIDDEN, PROTECTEDwhether it is exported from a shared object at all

A symbol that is GLOBAL DEFAULT in readelf -Ws but missing from nm -D was hidden by a version script or by -fvisibility=hidden; a symbol that is U in every object on the link line is the one ld is about to complain about.

Rewriting what you have

strip -s progremove the symbol tablestrip --strip-unneeded libx.sokeep what the dynamic linker needsobjcopy -O binary f.elf f.binraw image, for flashingobjcopy -O ihex f.elf f.hexIntel hex, the sameobjcopy --redefine-sym old=new f.orename a symbol to break a collisionobjcopy --add-section .note.x=file fembed a blobaddr2line -e prog -fCi 0x401136address to function and file:line, following inlines

Profiling & Coverage

where the time and the tests went

Three different questions, three different tools. Confusing them wastes an afternoon.

QuestionToolRebuild?
Which functions burn the time?perf recordno — just -g -fno-omit-frame-pointer
Which call paths burn it?perf record -g, flame graphno
Which lines did the tests reach?gcov / lcovyes, --coverage
Why is it slow at the microarchitecture level?perf stat, valgrind --tool=cachegrindno

perf, which is what you actually want

perf record -g ./progsample with call graphsperf reportthe interactive viewperf stat ./progcycles, instructions, IPC, branch and cache missesperf stat -d ./progplus cache detailperf topa live system-wide profileperf record --call-graph=dwarfwhen you cannot spare a frame pointerperf script | stackcollapse-perf.pl | flamegraph.pl > f.svgthe flame graph pipeline-fno-omit-frame-pointer -gbuild with these or the stacks are fiction

gprof, and why it is mostly history

gcc -pg -O2 …instrument; changes the code being measured./progwrites gmon.out into the CWDgprof ./prog gmon.out | lessflat profile then call graph

-pg instrumentation distorts what it measures — it adds a call to mcount to every function, so small hot functions look far more expensive than they are, and it does not see into shared libraries at all. Use perf unless you are on a platform that has no perf events.

Coverage

gcc --coverage -O0 -g …--coverage is -fprofile-arcs -ftest-coverage plus -lgcov at link./prog && gcov f.cwrites f.c.gcov, one annotated copy of the sourcegcov -b -c f.cbranch and call counts as well as line countslcov -c -d . -o cov.infocollect the whole treegenhtml cov.info -o html/the browsable reportgcovr --html-details -o cov.htmlthe same, in one tool-fprofile-abs-pathabsolute paths in the data; needed for out-of-tree builds
Build coverage at -O0. At -O2 the optimiser merges, splits and deletes lines, and the line numbers gcov reports stop corresponding to anything you can point at. Coverage and optimisation are answering different questions; do not try to ask both at once.

The measurement discipline

hyperfine './a' './b'proper statistics on wall-clock, with warmupperf stat -r 10 ./progten runs, with a standard deviationtaskset -c 2 ./progpin to a core; stops the scheduler adding noisecpupower frequency-set -g performancestop the governor moving the goalposts

make

the model, then the syntax

make is a language for one idea: this file is stale if it is older than the files it is built from. Everything awkward about it follows from being a declarative dependency graph with a macro processor bolted on.

target: prereq1 prereq2then a TAB, then the recipe. A tab, never spaces$@ $< $^ $?the target, first prerequisite, all of them, the out-of-date ones%.o: %.ca pattern rule — how to make any .o from the matching .c.PHONY: clean alltargets that are not files. Always declare themmake -ndry run — print the recipe, execute nothingmake -pevery rule and variable, built-ins includedmake -dwhy make decided to rebuild thatmake -j$(nproc)parallel. Needs correct dependencies to be safe

The three assignments, which are not interchangeable

FormExpandedUse for
=every time it is used (recursive)a value that must see later definitions
:=once, at the point of definitionalmost everything. The safe default
?=once, only if not already setletting the environment override
+=append, inheriting the flavour of the originaladding to CFLAGS
!=run a shell command, onceGITREV != git rev-parse HEAD
CFLAGS = -O2 $(EXTRA) with = re-runs every shell command inside it on every use. A recursive variable holding $(shell pkg-config --cflags gtk4) will fork pkg-config once per compilation. Use := and it forks once.

A correct small Makefile

CC := gccCFLAGS := -std=c17 -O2 -g -Wall -Wextra -MMD -MPnote -MMD -MPSRC := $(wildcard src/*.c)OBJ := $(SRC:.c=.o)prog: $(OBJ) $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@-include $(OBJ:.o=.d)the header dependencies, generated by -MMD.PHONY: cleanclean: $(RM) $(OBJ) $(OBJ:.o=.d) prog

The leading - on -include is what makes the first build work: the .d files do not exist yet, and without it make stops. That five-line dependency setup is the difference between a Makefile that rebuilds correctly and one that needs make clean after every header edit.

The functions worth memorising

$(wildcard *.c)glob the filesystem$(patsubst %.c,%.o,$(SRC))the general form of $(SRC:.c=.o)$(addprefix -I,$(DIRS))build a flag list$(notdir …) $(dir …) $(basename …) $(suffix …)path surgery$(shell cmd)run a command at parse time$(foreach v,$(LIST),$(v).o)iterate$(if cond,then,else)conditional expansion$(error message)fail the parse with a message

The traps

$$a literal $ in a recipe — shell variables need doublingeach line is its own shellcd in one line does not affect the next. Use `cd x && make` on one line, or .ONESHELLmake -j with wrong depsbuilds that work serially and fail at -j8. The deps are wrong, not make.DELETE_ON_ERROR:delete a half-written target when the recipe fails. Should be in every MakefileMAKEFLAGS += --warn-undefined-variablescatch the typo in $(CFALGS)@cmddo not echo the command-cmdignore its exit status

CMake & Ninja

when the Makefile stops scaling

CMake is a build-system generator: it produces Makefiles, Ninja files or an Xcode project from one description. It is worth adopting at the point where you need a second platform, a config-time feature test, or an installable library.

cmake -S . -B build -G Ninjaconfigure, out of tree, generating Ninjacmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo-O2 -g. The type you usually wantcmake --build build -jbuild, whatever the generatorcmake --build build --target installinstallcmake --install build --prefix /opt/xthe modern spellingctest --test-dir build --output-on-failurerun the tests, showing why they failedcmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ONemit compile_commands.json for clangd and clang-tidycmake --fresh -B buildreconfigure from scratch without deleting the tree

Build types, and what they really pass

CMAKE_BUILD_TYPEFlags
Debug-g
Release-O3 -DNDEBUG
RelWithDebInfo-O2 -g -DNDEBUG
MinSizeRel-Os -DNDEBUG
unsetnothing — no optimisation at all
An unset CMAKE_BUILD_TYPE means no -O flag whatsoever with the Makefile and Ninja generators. Benchmarks published from a default CMake build are, more often than anyone admits, measuring -O0. Set it, or set CMAKE_BUILD_TYPE to RelWithDebInfo in your presets.

A minimal modern CMakeLists.txt

cmake_minimum_required(VERSION 3.20)project(x C)naming C keeps it from looking for a C++ compilerset(CMAKE_C_STANDARD 17)set(CMAKE_C_STANDARD_REQUIRED ON)otherwise it silently falls backadd_library(x src/a.c src/b.c)target_include_directories(x PUBLIC include)PUBLIC propagates to consumerstarget_compile_options(x PRIVATE -Wall -Wextra)PRIVATE does notadd_executable(prog src/main.c)target_link_libraries(prog PRIVATE x)everything x needs comes along

Modern CMake is entirely about target_* commands and the PRIVATE/PUBLIC/INTERFACE keywords. The old directory-scoped include_directories() and add_definitions() apply to everything below them and are the reason large CMake builds become unpredictable. If a tutorial uses them, it is pre-2015.

Ninja, directly

ninja -C buildbuildninja -C build -t targetslist what it can makeninja -C build -t deps progthe recorded dependenciesninja -C build -d explainwhy it is rebuilding thatninja -C build -t compdb > compile_commands.jsonthe compilation database from any Ninja buildNINJA_STATUS='[%f/%t %e] 'a progress line with elapsed time

The GNU Extension Surface

what gnu23 buys you

The gnu dialects add a large, stable, well-documented set of extensions. Most have been copied by Clang; a few have not, and those are marked. All of them are non-ISO, so a project that must stay portable should reach for them behind a macro.

Expressions and control flow

({ int t = a; t*t; })statement expression — the last expression is the valuetypeof(x) / __typeof__(x)standardised in C23 as typeof__auto_type v = f();type inference before C23's autocase 'a' ... 'z':case ranges. Note the spaces around ...void *tab[] = {&&l1,&&l2}; goto *tab[i];computed goto — how fast interpreters dispatch GCC onlyint f(int n) { int g(void){return n;} … }nested functions, with trampolines GCC onlya ?: bthe elvis operator: a if non-zero, else b, evaluating a once__builtin_choose_expr(c,a,b)compile-time ?: that does not type-check the dead arm

Attributes, the ones that earn their keep

__attribute__((warn_unused_result))make ignoring a return value a warning__attribute__((format(printf,1,2)))let -Wformat check YOUR logging function__attribute__((cleanup(fn)))run fn when the variable goes out of scope — RAII in C__attribute__((constructor)) / ((destructor))run before main / after exit__attribute__((packed))no padding. Read the warning below__attribute__((aligned(64)))cache-line alignment__attribute__((nonnull(1,2)))documents and enables a warning; also licenses the optimiser__attribute__((malloc(free)))GCC 11+: teaches -fanalyzer the pairing__attribute__((access(write_only,1,2)))the buffer and its size, for -Wstringop-overflow__attribute__((counted_by(n)))GCC 14+: a flexible array's length field, for -fsanitize=bounds
__attribute__((packed)) does more than remove padding — it makes every member potentially unaligned, so taking a pointer to one and dereferencing it is undefined behaviour on strict-alignment targets and generates byte-at-a-time code on x86. Use it for wire formats you memcpy out of, never as a way to make a struct smaller.

Builtins

__builtin_expect(x,1)the likely() macro. Mostly obsoleted by C23 [[likely]] and by PGO__builtin_unreachable()tell the optimiser a path cannot happen__builtin_clz / ctz / popcount / paritybit counting, one instruction where it exists__builtin_add_overflow(a,b,&r)checked arithmetic that returns true on overflow. Use it__builtin_types_compatible_p(a,b)compile-time type equality__builtin_offsetofwhat offsetof is defined as__builtin_prefetch(p,0,3)rarely helps; measure__builtin_constant_p(x)true if x is known at compile time. The basis of half of glibc's headers__builtin_frame_address(0)the current frame; for hand-written unwinders

Type and layout extensions

__int128 / unsigned __int128on 64-bit targets. No printf conversion for itstruct { int n; int a[]; }flexible array member — ISO since C99, but the GNU a[0] form predates ittypedef int v4si __attribute__((vector_size(16)));portable SIMD vectors with ordinary operators__thread int x;thread-local storage; C11 spells it _Thread_local__asm__ __volatile__("" ::: "memory")a compiler barrierasm gotoinline assembly that can branch to a C label__label__ l;a local label inside a statement expression

Guard the ones you use: #if defined(__GNUC__) for the family, or better, #ifdef __has_attribute and then #if __has_attribute(cleanup) — a feature test that works under any compiler that has adopted the extension, which is the point.

Cross-Compiling

for the bench, and for the Pi

A cross toolchain is named by its target triple and prefixed with it. Once you have the prefix, everything else is the ordinary toolchain with a longer name.

TripleTargetsComes from
arm-none-eabi-bare-metal Cortex-M — no OS, no libc syscallsArm GNU Toolchain
aarch64-linux-gnu-Raspberry Pi 5, most Arm SBCsdistribution packages
arm-linux-gnueabihf-32-bit Pi, hard-floatdistribution packages
riscv64-unknown-elf-bare-metal RISC-Vriscv-gnu-toolchain
avr-ATmega, ATtinyavr-gcc
xtensa-esp32-elf-ESP32ESP-IDF
aarch64-linux-gnu-gcc -o prog main.cthe whole idea — a prefixed driverarm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hardthe four flags a Cortex-M4 build needs--sysroot=/path/to/rootfswhere the target's headers and libraries are-specs=nosys.specsbare metal: stub out the syscalls libc wants-specs=nano.specsnewlib-nano — a much smaller libcPKG_CONFIG_SYSROOT_DIR / PKG_CONFIG_LIBDIRstop pkg-config answering with host paths
The failure mode is silent. A cross build that picks up a host header or a host .so usually links, and then crashes on the target in a way that looks like a hardware fault. Set --sysroot, set PKG_CONFIG_LIBDIR, and check with readelf -A that the float ABI matches — a soft-float object linked into a hard-float program is the classic Arm version of this.

CMake, cross

cmake -B build --toolchain=arm.cmakea toolchain file, not a pile of -D flagsset(CMAKE_SYSTEM_NAME Generic)Generic means bare metal; stops CMake trying to run test binariesset(CMAKE_C_COMPILER arm-none-eabi-gcc)set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)the incantation that makes the compiler check pass on bare metalset(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)never find a host library

Running what you built

qemu-aarch64 -L /usr/aarch64-linux-gnu ./proguser-mode emulation; enough for a test suiteqemu-system-arm -M mps2-an385 -kernel f.elf -nographicsystem emulation for firmwareopenocd -f interface/stlink.cfg -f target/stm32f4x.cfgthen arm-none-eabi-gdb, target remote :3333arm-none-eabi-size f.elfdoes it fit in flash? The question that ends every firmware buildarm-none-eabi-objcopy -O binary f.elf f.binthe image to flash

Getting a Real GCC

and living beside a system compiler

On Linux GCC is the system compiler and the only question is which version. On macOS it is not present at all, and everything named gcc is a Clang shim.

Linux

apt install gcc-15 g++-15Debian/Ubuntu; versioned, side by sidednf install gcc gcc-c++ / dnf install gcc-toolset-14Fedora / RHEL's SCL-style newer toolchainpacman -S base-develArch: gcc, make, binutils in one groupscl enable gcc-toolset-14 bashenter a shell where gcc IS the new one (RHEL)update-alternatives --config gccchoose the default among installed versions (Debian)make CC=gcc-15usually easier than changing the system default

macOS — what you are really getting

/usr/bin/gccApple clang with a different name. Always has beenbrew install gccreal GCC, installed as gcc-15 / g++-15 / gfortran-15gcc-15 --version"gcc-15 (Homebrew GCC 15.x)" — the real thingmake CC=gcc-15 CFLAGS=…how to use it without touching /usr/binbrew install binutilskeg-only; objdump, readelf etc. are prefixed g* to avoid clashing
Real GCC on macOS is a second-class citizen and that is Apple's doing, not GCC's. It cannot consume Apple's module maps, it lags the SDK's Objective-C and availability attributes, and every Xcode update can break it until Homebrew rebuilds. Use it for portability testing — building your project under both compilers finds real bugs — and use Clang for anything that must talk to the platform frameworks.

Checking what you actually have

gcc --version && gcc -dumpmachine && gcc -dumpversionthe three questions, in ordergcc -print-prog-name=cc1a path means real GCC; the name echoed back means a shimecho | gcc -dM -E - | grep -E '__GNUC__|__clang__|__VERSION__'the definitive answergcc -Q --help=targetevery target flag and its current valuegcc -v 2>&1 | grep configurehow this GCC was built — which languages, which default flags

Building GCC from source, briefly

./contrib/download_prerequisitesgmp, mpfr, mpc — run this first, in the source treemkdir ../build && cd ../buildGCC refuses to build in its own source directory../configure --prefix=/opt/gcc-15 --enable-languages=c,c++ --disable-multilibthe usual linemake -j$(nproc) && make installan hour or so; --disable-bootstrap halves it and halves the confidence

Worth doing exactly twice: when you need a version your distribution does not carry, and when you want to bisect a compiler bug. Otherwise take the package.

Flag, Attribute & Tool Index

Every switch worth remembering, grouped by what it does — type in the filter box, or press /

Driver & Output

30

Stopping after a stage

-Epreprocess only
-Scompile to assembly
-ccompile and assemble
-o filename the output
-fsyntax-onlyparse and check, emit nothing
-pipepipes instead of temp files

Seeing what it does

-vrun, printing every sub-command
-###print sub-commands, run nothing
-save-tempskeep .i .s .o
-print-search-dirsprogram and library paths
-print-file-name=libresolve a library to a path
-print-prog-name=cc1resolve a sub-program
-dumpmachinethe target triple
-dumpversioncompiler version only
-print-multi-directorywhich multilib variant
-print-libgcc-file-namethe compiler runtime in use

Passing arguments down

-Wp,ARGto the preprocessor
-Wa,ARGto the assembler
-Wl,ARGto the linker
-Xlinker ARGone linker argument verbatim
-Xassembler ARGone assembler argument
-Xpreprocessor ARGone preprocessor argument
--param NAME=Van optimiser tuning knob
@fileread arguments from a file

Input language

-x LANGforce the language
-read from stdin
-include fileas if #included first
-imacros fileinclude for macros only
--sysroot=DIRlogical root for headers and libs
-isysroot DIRheaders-only sysroot

Language & Dialect

34

Standard selection

-std=c89ANSI C / C90
-std=c99C99
-std=c11C11
-std=c17C17 / C18
-std=c23C23 (ISO/IEC 9899:2024)
-std=gnu23C23 plus GNU extensions
-std=gnu17C17 plus GNU extensions
-std=c2ythe next standard, in flux
-ansisynonym for -std=c90
-pedanticdiagnose non-conforming code
-pedantic-errorsand make those errors

Language behaviour

-funsigned-charplain char is unsigned
-fsigned-charplain char is signed
-fshort-enumsenums take the smallest type that fits
-fno-builtinno built-in knowledge of libc
-ffreestandingno hosted environment
-fhostedthe default
-fno-commontentative definitions are not merged
-fcommonrestore the old merging
-fno-strict-aliasingdo not assume type-based non-aliasing
-fwrapvsigned overflow wraps
-ftrapvsigned overflow traps
-fno-delete-null-pointer-checkskeep null checks after a deref
-fvisibility=hiddensymbols hidden by default
-fPIC / -fpicposition-independent code
-fPIE / -fpieposition-independent executable
-fno-semantic-interpositiona shared lib may inline its own calls
-fno-pltcall through the GOT, not the PLT
-ftls-model=initial-execthread-local storage model
-fexec-charset=UTF-8execution character set
-fmacro-prefix-map=a=brewrite __FILE__
-ffile-prefix-map=a=brewrite __FILE__ and debug paths
-fno-asynchronous-unwind-tablesdrop .eh_frame
-fstrict-flex-arrays=3flexible array members are strict

Preprocessor

32

Macros and paths

-Dname=valuedefine a macro
-Unameundefine a macro
-I diradd to the include path
-iquote dirquote-form includes only
-isystem dirsystem include path
-idirafter dirafter the built-in dirs
-nostdincdrop the built-in system dirs
-iprefix / -iwithprefixprefix-relative additions

Dependency generation

-Mmake rules for all headers
-MMthe same, no system headers
-MDcompile and write the .d
-MMDcompile and write .d, no system headers
-MF filewhere to write the .d
-MPadd a phony target per header
-MT targetoverride the rule target
-MQ targetthe same, quoting make specials
-MGtolerate missing generated headers

Interrogation

-dM -Edump every defined macro
-dD -Eoutput with #defines kept
-Pno # line markers
-Ckeep comments through -E
-Hprint the include tree
-fdirectives-onlyexpand directives, not macros
-trigraphsenable trigraphs

Conditional helpers

__has_include(<x.h>)is this header available
__has_include_next(<x.h>)the next one on the path
__has_attribute(x)is this attribute known
__has_c_attribute(x)C23 bracket-attribute form
__has_builtin(x)is this builtin known
__VA_OPT__(x)expand only if variadic args exist
#embed "f.bin"embed a binary file
_Pragma("...")a pragma from inside a macro

Warnings

80

The bundles

-Wallthe common set
-Wextrathe next tier
-Wpedanticanything the -std forbids
-wsuppress all warnings
-Werrorwarnings become errors
-Werror=NAMEpromote one warning
-Wno-error=NAMEdemote one under -Werror
-Wfatal-errorsstop at the first error
-fmax-errors=Ncap the error count

Correctness

-Wuninitializeduse before set
-Wmaybe-uninitializedpossibly used before set
-Wreturn-typemissing return in a non-void function
-Wimplicit-fallthroughswitch case falls through
-Wswitchunhandled enum value in a switch
-Wswitch-enumunhandled even with a default
-Wswitch-defaultswitch with no default label
-Wnull-dereferencea path that dereferences null
-Wsequence-pointunsequenced modification
-Warray-boundsindex provably out of range
-Wunused-resultignoring a warn_unused_result return
-Wunused-valuea computed value thrown away
-Wunreachable-codedead code
-Wduplicated-condrepeated condition in an if/else chain
-Wduplicated-branchesidentical if and else bodies
-Wlogical-opbitwise where logical was meant

Integers and conversions

-Wconversionimplicit conversion may change the value
-Wsign-conversionimplicit signed/unsigned conversion
-Wsign-comparecomparison between signed and unsigned
-Wfloat-equalexact comparison of floats
-Wdouble-promotionfloat silently promoted to double
-Wshift-overflow=2shift that overflows the type
-Wshift-count-overflowshift by more than the width
-Wshift-negative-valueleft-shifting a negative value
-Wbad-function-castcasting a function result oddly
-Wenum-conversionconverting between enum types

Memory, strings and formats

-Wformat=2format checking, thoroughly
-Wformat-securitya non-literal format with no arguments
-Wformat-truncation=2snprintf output may be truncated
-Wformat-overflow=2sprintf output may overflow
-Wstringop-overflow=4str/mem call writes past the end
-Wstringop-truncationstrncpy may not terminate
-Wrestrictoverlapping arguments to a restrict parameter
-Wmemset-transposed-argsmemset(p, n, 0)
-Wsizeof-pointer-memaccesssizeof a pointer used as a length
-Wsizeof-array-argumentsizeof an array parameter
-Wfree-nonheap-objectfreeing something that was not malloced
-Wallocaany use of alloca
-Wvlaany variable-length array
-Wvla-larger-than=1024a VLA that may be large
-Walloc-zeromalloc(0)
-Wstack-usage=8192a frame larger than N bytes
-Wframe-larger-than=Nthe same, older spelling

Declarations and style

-Wshadowa declaration shadows an outer one
-Wstrict-prototypesa declaration with no argument types
-Wmissing-prototypesa global function with no prior prototype
-Wmissing-declarationsa global definition with no declaration
-Wold-style-definitionK&R-style function definition
-Wredundant-declsdeclared more than once
-Wnested-externsextern declaration inside a function
-Wundefundefined macro used in #if
-Wunused-macrosa macro defined and never used
-Wcast-quala cast that discards const or volatile
-Wcast-align=stricta cast that increases alignment requirements
-Wpointer-aritharithmetic on void* or function pointers
-Wwrite-stringsstring literals get type const char[]
-Wpaddeda struct had padding inserted
-Wpackeda packed attribute that changed nothing
-Winlinean inline function was not inlined

Concurrency and analysis

-fanalyzerenable the GCC static analyser
-Wanalyzer-double-freea path that frees twice
-Wanalyzer-use-after-freea path that uses freed memory
-Wanalyzer-malloc-leaka path that leaks
-Wanalyzer-null-dereferencea path that dereferences null
-Wanalyzer-fd-leaka path that leaks a file descriptor
-Wanalyzer-tainted-array-indexattacker-controlled index

Suppressing precisely

#pragma GCC diagnostic pushsave the current state
#pragma GCC diagnostic ignored "-Wx"silence one warning
#pragma GCC diagnostic poprestore
(void)x;mark a parameter used
[[maybe_unused]]C23: legitimately unused

Optimisation

48

Levels

-O0none; the default
-O1the cheap transformations
-O2the release default
-O3aggressive inlining and loops
-Osoptimise for size
-Ozsize at any speed cost
-Ogoptimise but stay debuggable
-Ofast-O3 plus non-conforming maths

Code generation

-finline-functionsinline anything worthwhile
-fno-inlineinline nothing
-funroll-loopsunroll where the count is known
-ffunction-sectionsone section per function
-fdata-sectionsone section per data object
-fomit-frame-pointerfree the frame-pointer register
-fno-omit-frame-pointerkeep it
-fstrict-aliasingassume no cross-type aliasing
-falign-functions=32align function entry points
-fno-stack-protectorno canaries
-fipa-ptainterprocedural points-to analysis
-fipa-icfmerge identical functions
-ftrivial-auto-var-init=zerozero uninitialised locals

Loops and vectorisation

-ftree-vectorizeenable the vectoriser
-fno-vectorizeturn it off
-fvect-cost-model=dynamicvectoriser cost model
-fopt-info-vec-missedwhy a loop was not vectorised
#pragma GCC ivdepassert no loop-carried dependence
#pragma GCC unroll 4unroll this loop

Link-time and profile-guided

-fltolink-time optimisation
-flto=autoone LTO job per core
-ffat-lto-objectsemit IR and machine code
-fprofile-generateinstrument for PGO
-fprofile-useconsume the .gcda profiles
-fauto-profilesampling PGO from perf
-fprofile-dir=DIRwhere the profile data goes

Floating point

-ffast-mathall the unsafe maths at once
-fno-math-errnolibrary maths does not set errno
-fassociative-mathreassociate FP expressions
-freciprocal-mathx/y becomes x*(1/y)
-ffinite-math-onlyassume no NaN or Inf
-ffp-contract=fastallow FMA contraction
-frounding-mathdo not assume round-to-nearest
-fexcess-precision=standardno extra intermediate precision
-fsingle-precision-constantunsuffixed literals are float

Seeing inside

-fdump-tree-allevery GIMPLE pass to a file
-fdump-rtl-allevery RTL pass
-fopt-info-inline-optimizedwhat was inlined
-fverbose-asmannotate -S output
-masm=intelIntel syntax assembly

Target & Machine

32

Generic

-march=NAMEthe ISA baseline you may use
-mtune=NAMEschedule for this CPU, run anywhere
-mcpu=NAMEboth at once
-march=nativethis exact machine
-m32 / -m64pointer width
-mabi=lp64the ABI variant

x86-64

-march=x86-64the 2003 baseline: SSE2 only
-march=x86-64-v2SSE4.2, POPCNT (Nehalem)
-march=x86-64-v3AVX2, BMI, FMA (Haswell)
-march=x86-64-v4AVX-512
-mavx2 -mfma -mbmi2individual extensions
-mno-red-zoneno 128-byte red zone
-mcmodel=largecode model
-fcf-protection=fullIntel CET: endbr64 and shadow stack

AArch64 and Arm

-mcpu=apple-m1Apple silicon tuning
-march=armv8.2-a+crypto+fp16ISA plus feature list
-mcpu=cortex-m4Cortex-M target
-mthumb / -marminstruction set
-mfpu=fpv4-sp-d16the FPU present
-mfloat-abi=hardFP arguments in FP registers
-mbranch-protection=standardPAC and BTI
-mgeneral-regs-onlyno FP or SIMD registers
-mstrict-alignnever emit unaligned accesses

Other targets

-mmcu=atmega328pthe AVR device
-march=rv32imac -mabi=ilp32RISC-V ISA and ABI
-mlong-callsno PC-relative call range assumption
-nostartfilesno crt0
-nostdlibno startup files, no libraries
-nodefaultlibsno default libraries
--specs=nano.specsnewlib-nano
--specs=nosys.specsstub the syscalls
--specs=rdimon.specssemihosting

Debug Info

22

Levels and formats

-gdebug info at the default level
-g0 / -g1 / -g2 / -g3increasing detail
-ggdb3the richest GNU-flavoured output
-gdwarf-4 / -gdwarf-5pin the DWARF version
-gsplit-dwarfdebug info into .dwo files
-gzcompress debug sections
-g -O2debug info for an optimised build
-fdebug-prefix-map=a=brewrite paths in debug info
-fno-eliminate-unused-debug-typeskeep unused type info

Making it debuggable

-Ogthe level built for debugging
-fno-inlinekeep every frame
-fno-optimize-sibling-callsno tail-call elimination
-fno-omit-frame-pointerwalkable stacks
-fasynchronous-unwind-tables.eh_frame for any address
-rdynamicexport symbols for backtrace_symbols

Instrumentation and coverage

--coveragegcov instrumentation
-fprofile-arcsarc counters
-ftest-coveragethe .gcno notes file
-pggprof instrumentation
-finstrument-functionscall your hooks on entry and exit
-fstack-usagewrite a .su file per function
-fcallgraph-infoemit a call graph

Sanitizers & Hardening

37

The sanitizers

-fsanitize=addressASan: overflow and use-after-free
-fsanitize=undefinedUBSan: the UB catalogue
-fsanitize=threadTSan: data races
-fsanitize=leakLSan alone
-fno-sanitize-recover=allabort on the first report
-fsanitize-trap=alltrap instead of calling the runtime
-fsanitize-recover=allreport and keep going
__attribute__((no_sanitize("address")))exempt one function

UBSan checks worth naming

-fsanitize=signed-integer-overflowthe classic UB
-fsanitize=shiftshift by too much or by a negative
-fsanitize=integer-divide-by-zerointeger division by zero
-fsanitize=nullnull dereference
-fsanitize=alignmentmisaligned load or store
-fsanitize=boundsarray bounds where the size is known
-fsanitize=object-sizeaccess beyond a known object
-fsanitize=returnfalling off the end of a non-void function
-fsanitize=bool,enuma value outside the type range
-fsanitize=vla-bounda VLA with a non-positive length
-fsanitize=nonnull-attributenull passed to a nonnull parameter
-fsanitize=unreachablereaching __builtin_unreachable
-fsanitize=float-cast-overflowfloat to integer out of range

Runtime options (environment)

ASAN_OPTIONScolon-separated ASan settings
detect_stack_use_after_return=1an extra ASan bug class
UBSAN_OPTIONS=print_stacktrace=1stack traces for UBSan
TSAN_OPTIONS=second_deadlock_stack=1both sides of a lock inversion
LSAN_OPTIONS=suppressions=fsilence known leaks
ASAN_SYMBOLIZER_PATHpath to llvm-symbolizer
MSAN_OPTIONSMSan settings

Hardening (these stay on in production)

-D_FORTIFY_SOURCE=3checked str/mem functions
-fstack-protector-strongcanaries where they matter
-fstack-clash-protectionprobe each page when growing the stack
-fcf-protection=fullx86 CET
-mbranch-protection=standardAArch64 PAC and BTI
-fPIE -pieASLR for the executable
-Wl,-z,relro -Wl,-z,nowread-only GOT after loading
-Wl,-z,noexecstacknon-executable stack
-fhardeneda curated hardening bundle

Linking (via the driver)

16

Libraries

-lfoolink libfoo.so or libfoo.a
-L diradd a link-time search directory
-l:libfoo.so.1link an exact filename
-Wl,-Bstaticprefer .a from here on
-Wl,-Bdynamicprefer .so from here on
-staticfully static link
-static-piestatic and position-independent
-static-libgccstatic compiler runtime only
-sharedbuild a shared library
-rdynamicexport the executable symbols
-pthreadthreads: macro AND library

Startup and runtime

-nostdlibno startup files, no default libs
-nostartfilesno crt0/crt1
-nodefaultlibsno default libraries
-nolibcno C library
-fuse-ld=lldchoose the linker

Linker Options

37

Symbol resolution

-Wl,--start-group ... --end-groupre-scan until fixpoint
-Wl,--whole-archivepull in every archive member
-Wl,--no-whole-archivestop doing that
-Wl,-u,symbolforce a symbol to be undefined
-Wl,--no-undefineda shared lib must resolve everything
-Wl,--allow-multiple-definitionfirst definition wins
-Wl,--trace-symbol=fooevery file that mentions foo
-Wl,--defsym,sym=0x1000define a symbol on the command line
-Wl,--wrap=mallocredirect calls to __wrap_malloc

Layout and stripping

-Wl,--gc-sectionsdrop unreachable sections
-Wl,--print-gc-sectionslist what was dropped
-Wl,-Map=out.mapwrite a map file
-Wl,--crefcross-reference table
-Wl,--print-memory-usageflash and RAM percentages
-Wl,-sstrip all symbols at link
-Wl,--strip-debugstrip debug info only
-T script.ldreplace the linker script
-Wl,--section-start=.text=0x8000place a section
-Wl,--sort-section=alignmentreduce padding

Dynamic behaviour

-Wl,-soname,libfoo.so.1the name consumers record
-Wl,-rpath,$ORIGIN/../librun-time search path
-Wl,--enable-new-dtagsemit RUNPATH not RPATH
-Wl,--disable-new-dtagsemit the old RPATH
-Wl,--as-neededomit unused DT_NEEDED entries
-Wl,--no-as-neededrecord every library
-Wl,-Bsymbolicbind internal references internally
-Wl,--version-script=v.mapcontrol exported symbols
-Wl,--exclude-libs,ALLdo not re-export static libs
-Wl,-z,nowresolve everything at load
-Wl,-z,relroread-only relocations
-Wl,-z,originpermit $ORIGIN expansion
-Wl,--build-id=sha1a build identifier note

Diagnostics

-Wl,-ttrace files as they are opened
-Wl,--verboseprint the default linker script
-Wl,--fatal-warningslinker warnings become errors
-Wl,--warn-commonwarn on common symbols
-Wl,--warn-backrefswarn on order-dependent resolution

Analysis & Diagnostics

15

Static analysis

-fanalyzerGCC path-sensitive analyser
-fanalyzer-checker=taintenable the taint checkers
-fdiagnostics-path-format=inline-eventsrender the path in the source
cppcheck --enable=all src/independent analyser

Diagnostic presentation

-fdiagnostics-color=alwayskeep colour through a pipe
-fdiagnostics-format=jsonmachine-readable
-fdiagnostics-format=sarifSARIF for CI ingestion
-fno-diagnostics-show-caretone line per diagnostic
-fdiagnostics-show-optionname the -W flag responsible
-fdiagnostics-plain-outputstable text for test suites
-fdiagnostics-generate-patchemit a diff of the fix-its
-fdiagnostics-show-hotnessannotate remarks with profile weight

Formatting and tooling

bear -- makegenerate compile_commands.json
ninja -t compdb > compile_commands.jsonstraight from ninja
-DCMAKE_EXPORT_COMPILE_COMMANDS=ONCMake writes it for you

Attributes

45

Optimisation and codegen

always_inlinea demand, not a hint
noinlinenever inline this
flatteninline everything this calls
hot / coldoptimise for speed / for size
pureno side effects; may read memory
constno side effects; reads no memory
mallocthe return aliases nothing
returns_nonnullnever returns null
noreturnnever returns
optimize("O0")per-function optimisation level
target("avx2")compile one function for another ISA
target_clones("avx2","default")automatic multiversioning

Diagnostics and contracts

format(printf, 1, 2)your wrapper gets -Wformat checking
format_arg(1)this returns a format string
warn_unused_resultthe return must be used
nonnull(1, 2)these parameters are never null
deprecated("use foo")warn on use, with a message
unavailableerror on use
unusedsuppress the unused warning
usedkeep even if apparently unreferenced
fallthroughthis case falls through deliberately
error("msg") / warning("msg")fire if this call survives optimisation

Layout and linkage

packedno padding
aligned(64)minimum alignment
alignedthe target maximum useful alignment
section(".fast")place in a named section
visibility("hidden")not exported from the shared object
weakmay be overridden; resolves to 0 if absent
alias("real")a second name for one definition
weak_alias / weakrefa weak second name
constructor(101)run before main, by priority
destructorrun after main returns
cleanup(fn)call fn(&var) at scope exit
common / nocommontentative-definition placement
may_aliasthis type aliases anything
transparent_uniona union parameter passed as its members
vector_size(16)a GCC vector type
counted_by(len)this flexible array has this length

Sanitizers and instrumentation

no_sanitize("address")exempt this function
no_sanitize_addressthe older spelling
no_instrument_functionskip -finstrument-functions hooks
no_stack_protectorno canary here
nakedno prologue or epilogue
interruptan interrupt handler
access(read_only, 1, 2)describe pointer parameter use

Builtins

40

Branches and constants

__builtin_expect(x, 1)branch probability hint
__builtin_expect_with_probability(x,1,.9)with an explicit probability
__builtin_unreachable()this path cannot happen
__builtin_trap()emit an illegal instruction
__builtin_constant_p(x)did x fold to a constant
__builtin_assume_aligned(p, 64)assert the alignment
__builtin_types_compatible_p(a, b)type equality at compile time
__builtin_choose_expr(c, a, b)compile-time ?: that does not typecheck the dead arm
__builtin_offsetof(T, m)what offsetof expands to

Bit manipulation

__builtin_clz(x)count leading zeros
__builtin_ctz(x)count trailing zeros
__builtin_popcount(x)set bits
__builtin_parity(x)parity of the set bits
__builtin_clzll / ctzll / popcountllthe 64-bit versions
__builtin_bswap16/32/64(x)byte swap
__builtin_rotateleft32(x, n)rotate
__builtin_ffs(x)index of the lowest set bit, 1-based

Checked arithmetic

__builtin_add_overflow(a, b, &r)true on overflow
__builtin_sub_overflow(a, b, &r)true on overflow
__builtin_mul_overflow(a, b, &r)
__builtin_add_overflow_p(a, b, type)test without storing

Memory and objects

__builtin_memcpy / memsetthe recognised forms
__builtin_object_size(p, 1)known size of the pointed-to object
__builtin_dynamic_object_size(p, 1)the run-time version
__builtin_prefetch(p, 0, 3)prefetch hint
__builtin_alloca(n)stack allocation
__builtin_launder(p)defeat pointer-provenance assumptions

Introspection

__builtin_frame_address(0)this frame
__builtin_return_address(0)this function return address
__builtin_LINE() / FILE() / FUNCTION()caller location as a default argument
__func__the current function name
__builtin_cpu_supports("avx2")runtime CPU dispatch
__builtin_va_arg_pack()forward all variadic arguments

Atomics (the GNU set)

__atomic_load_n(p, order)atomic load
__atomic_store_n(p, v, order)atomic store
__atomic_exchange_n(p, v, order)atomic swap
__atomic_compare_exchange_n(...)CAS
__atomic_fetch_add(p, v, order)atomic RMW
__atomic_thread_fence(order)a standalone fence
__sync_fetch_and_add(p, v)the pre-C11 legacy set

Predefined Macros

36

Which compiler

__GNUC__GNU-compatible compiler
__GNUC_MINOR__ __GNUC_PATCHLEVEL__the rest of the version
__VERSION__a version string
_MSC_VERMSVC
__INTEL_LLVM_COMPILERIntel oneAPI

Language and mode

__STDC__a conforming C implementation
__STDC_VERSION__202311L for C23
__STDC_HOSTED__1 if hosted, 0 if freestanding
__STRICT_ANSI__defined under -std=cNN not gnuNN
__STDC_NO_ATOMICS__no _Atomic support
__cplusplusthis is C++
__OPTIMIZE__-O1 or above
__NO_INLINE__inlining is disabled
__SANITIZE_ADDRESS__ASan is on
__SANITIZE_THREAD__TSan is on

Target

__x86_64__ / __i386__x86
__aarch64__ / __arm__Arm
__riscv / __riscv_xlenRISC-V
__AVR__ / __AVR_ATmega328P__AVR
__linux__ __APPLE__ __FreeBSD__operating system
__unix__ / __MACH__family
__BYTE_ORDER__endianness
__SIZEOF_POINTER__pointer size in bytes
__CHAR_BIT__bits per char
__INT_MAX__ __LONG_MAX__limits without limits.h
__CHAR_UNSIGNED__plain char is unsigned
__SIZE_TYPE__ __PTRDIFF_TYPE__the underlying types
__ELF__the object format is ELF
__PIC__ / __PIE__compiled position-independent

Source location

__FILE__ __LINE__the classics
__DATE__ __TIME__build timestamp
__func__the enclosing function
__FILE_NAME__basename only
__COUNTER__a unique integer, incremented per use
__INCLUDE_LEVEL__nesting depth of includes
__BASE_FILE__the outermost source file

Pragmas

21

Diagnostics

#pragma GCC diagnostic push/popsave and restore
#pragma GCC diagnostic ignored "-Wx"silence one
#pragma GCC diagnostic error "-Wx"promote one
#pragma GCC diagnostic warning "-Wx"demote one
#pragma GCC system_headertreat the rest of this file as system
#pragma message("text")print at compile time
#pragma GCC poison identifiererror on any use of a name

Optimisation

#pragma GCC optimize("O3")per-function level
#pragma GCC push_options / pop_optionssave and restore them
#pragma GCC target("avx2")per-region ISA
#pragma GCC unroll 4unroll this loop
#pragma GCC ivdepno loop-carried dependence
#pragma omp parallel forOpenMP

Layout and language

#pragma pack(push, 1)struct packing
#pragma onceinclude guard
#pragma STDC FP_CONTRACT OFFstandard: no FMA contraction
#pragma STDC FENV_ACCESS ONstandard: I touch the FP environment
#pragma STDC CX_LIMITED_RANGE ONstandard: simpler complex arithmetic
#pragma pop_macro("X") / push_macrosave and restore a macro
#pragma weak symboldeclare a weak symbol
_Pragma("GCC diagnostic push")a pragma inside a macro

gdb

55

Starting

gdb --args ./prog a bstart with argv set
gdb -p PIDattach to a running process
gdb ./prog corepost-mortem on a core file
coredumpctl gdbthe systemd path to the same
gdb -batch -ex bt ./prog corescripted, non-interactive
run / rstart the program
startrun and stop at main
set args a bchange argv for the next run
killstop the inferior, keep the session

Breakpoints and watchpoints

b f.c:42by file and line
b funcby function
b f.c:42 if x==3conditional
tbreaktemporary: deleted when hit
rbreak regexa breakpoint on every match
watch xstop when x is written
rwatch x / awatch xstop on read / on either
catch syscall writestop on a syscall
info breakpointslist them, with hit counts
delete N / disable N / enable Nmanage them
ignore N 100skip the next 100 hits
commands Nrun commands automatically when hit

Stepping

ccontinue
n / sstep over / step into
ni / sione instruction
finishrun to the end of this frame
until Nrun to line N in this frame
advance LOCrun to LOC or until the frame returns
return exprforce a return now
jump f.c:50move the PC

The stack and the data

btbacktrace
bt fullbacktrace with every frame’s locals
f N / up / downselect a frame
info locals / info argsthis frame’s variables
p exprevaluate a C expression
p/x p/t p/c p/dformat: hex, binary, char, decimal
p *arr@10print 10 elements from a pointer
x/16xb pexamine raw memory
ptype T / ptype varthe full definition of a type
set var x=5change a variable
display exprprint it after every stop
info registers / p $rspregisters
info symbol 0x401136address to symbol
info line *0x401136address to source line
info framethe frame’s saved registers and layout

Threads, and the session

info threadslist them, marking the current
thread Nswitch
thread apply all btevery stack
set scheduler-locking onstep only this thread
info sharedlibraryloaded libraries and their addresses
disassemble /sthe current function, with source
layout src / layout asmthe curses view
set debuginfod enabled onfetch distro debug info on demand
set print pretty onreadable struct printing
source script.pya Python extension
~/.gdbinitstartup file

Binutils

38

First questions

file progformat, arch, PIE, stripped
size -A -d progper-section sizes in decimal
nm --size-sort -S prog | tailthe biggest symbols
nm -u progstill-undefined symbols
nm -C --defined-only f.owhat this object offers
nm -D --defined-only libx.sowhat a shared object exports
strings -a -n 8 progprintable runs of 8+ characters

ELF metadata

readelf -h progthe ELF header
readelf -d progNEEDED SONAME RPATH RUNPATH
readelf -Ws progthe full symbol table
readelf -n prognotes: build-id, ABI tag
readelf -p .comment progwhich compiler built it
readelf -A progarchitecture attributes
readelf -S progevery section header
readelf -l progprogram headers and segments
readelf --debug-dump=info progthe DWARF

Disassembly

objdump -d progdisassemble executable sections
objdump -dS progwith source interleaved
objdump -d -M intel progIntel syntax
objdump -h progsection headers, sizes, flags
objdump -t progthe symbol table
objdump -T libx.sothe dynamic symbol table
objdump -R progdynamic relocations
objdump -p progprivate headers: NEEDED, RPATH
c++filt _Z3fooidemangle a name
addr2line -e prog -fCip 0x401136address to function and file:line

Archives and rewriting

ar rcs libx.a a.o b.ocreate an archive with an index
ar t libx.a / ar x libx.alist / extract members
ranlib libx.arebuild the index
strip -s progremove the symbol table
strip --strip-unneeded libx.sokeep what the loader needs
objcopy --only-keep-debug prog prog.debuglift the debug info out
objcopy --add-gnu-debuglink=prog.debug progpoint at the separated symbols
objcopy -O binary f.elf f.bina raw image
objcopy -O ihex f.elf f.hexIntel hex
objcopy --redefine-sym old=new f.orename a symbol
objcopy --add-section .note.x=f fembed a blob
elfedit --output-osabi none progpatch the ELF header in place

Environment

19

Compiler and linker

CPATHextra include dirs, all languages
C_INCLUDE_PATHextra include dirs, C only
LIBRARY_PATHextra -L at LINK time
LD_LIBRARY_PATHextra search at RUN time
LD_PRELOADload these first
LD_DEBUG=libstrace the dynamic loader
LD_BIND_NOW=1resolve everything at load
SOURCE_DATE_EPOCHfreeze __DATE__ and __TIME__
TMPDIRwhere intermediates go
GCC_COLORSdiagnostic colour scheme
CCACHE_DIR / CCACHE_DISABLEccache control

Runtime instrumentation

ASAN_OPTIONSAddressSanitizer settings
UBSAN_OPTIONSUBSan settings
TSAN_OPTIONSThreadSanitizer settings
LSAN_OPTIONSLeakSanitizer settings
LLVM_PROFILE_FILEwhere .profraw is written
GMON_OUT_PREFIXgprof output name
MALLOC_CHECK_=3glibc heap consistency checks
MALLOC_PERTURB_=165fill malloc and free

make

46

Automatic variables

$@the target
$<the first prerequisite
$^all prerequisites, deduplicated
$+all prerequisites, duplicates kept
$?prerequisites newer than the target
$*the stem matched by %
$(@D) $(@F)directory and file parts of $@
$$@a literal $@ in a shell command

Assignment and conditionals

:=expand once, immediately
=expand at every use
?=set only if not already set
+=append
!=assign the output of a shell command
override VAR = xwin against a command-line assignment
export VARput it in the recipe environment
ifeq ($(A),$(B))conditional
target: private VAR = xtarget-specific variable

Functions

$(wildcard src/*.c)glob at parse time
$(patsubst %.c,%.o,$(S))pattern substitution
$(subst a,b,$(S))plain substitution
$(filter %.c,$(S))keep matching words
$(sort $(S))sort and deduplicate
$(dir ...) $(notdir ...)path parts
$(addprefix -I,$(D))build flag lists
$(foreach v,$(L),$(v).o)iteration
$(if c,then,else)conditional expansion
$(call fn,a,b)call a user-defined function
$(eval $(TEXT))parse text as makefile syntax
$(shell cmd)run a command at parse time
$(error msg)stop with a message
$(file > f,text)write a file directly

Special targets and flags

.PHONY: clean allnot a real file
.DELETE_ON_ERROR:delete a half-written target on failure
.SECONDARY:do not delete intermediates
.NOTPARALLEL:serialise this makefile
.ONESHELL:one shell for the whole recipe
.DEFAULT_GOAL := allwhich target runs bare
-j Nparallel jobs
--output-sync=targetstop interleaved output
-ndry run
-pdump every rule and variable
-d / --debug=bwhy it rebuilt
--traceprint each recipe with its line
-W filepretend a file just changed
$(MAKE)recurse
$(MAKEFLAGS)the flags in force