C — the Clang Toolchain Clang 21 · LLVM · lldb · clang-tidy · 713 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. Clang is a front end and LLVM is everything behind it, and that split is why this toolchain has an opt and an llc you can run by hand, why clang-tidy, clang-format and clangd exist at all, and why cross-compiling is a flag rather than a second toolchain. Cards 1–17 are the working guide: the pipeline and what each stage will show you, the driver, the gnu17 default Clang kept, the diagnostic engine that is the whole reason Clang was written, what each -O level is, ThinLTO and both flavours of PGO, debug info and lldb, the full sanitizer set including the two GCC has not got, clang-tidy and the analyzer, linking on ELF and Mach-O, the LLVM tools, source-based coverage, reading the IR, cross-compiling, what is genuinely different on a Mac, and which of the three compilers called "clang" you actually have. The remaining 19 cards index the flags, attributes, builtins, macros, lldb commands and tools. 601 of the 713 entries also work in GCC; the 105 marked in orange are Clang or LLVM only, and the 7 marked in blue are spelled the same in GCC and mean something else.

Dots: also works in GCC Clang / LLVM only GCC spells it the same, differently
Sources: the Clang 21 User's Manual and Clang Language Extensions, the LLVM Command Guide, the LLDB documentation, the clang-tidy and clang-format references, and Apple's toolchain documentation for the Mach-O and codesigning material. Everything platform-specific was checked against Apple clang 21 on macOS 26 and upstream Clang 21 on Linux. Hover a clipped row for the whole entry. Companion sheets: C — the Language and C — the GCC Toolchain.

The Working Guide

Clang 21 and LLVM as a toolchain — the driver, the diagnostics that are the product, the pipeline you can take apart, and what is different on a Mac

Clang and LLVM

a compiler that is a library

Clang is a front end; LLVM is everything behind it. The split is the whole design, and it is why the toolchain has an opt and an llc you can run by hand, why clang-tidy and clangd exist, and why every vendor with a GPU ships a fork.

StageProducesInspect with
Clang parserthe Clang AST — full source fidelity, comments included-Xclang -ast-dump
CodeGenLLVM IR — SSA, typed, textual and serialisable-S -emit-llvm
the middle endoptimised IR, target-independentopt -O2 -S
ISel → MIRmachine IR, still SSA at firstllc -print-after-all
MCobject code — the assembler is inside clang-fno-integrated-as to opt out

One consequence you feel daily: Clang does not fork an assembler, so -S and -c are a single process and error messages from bad inline assembly come out of clang itself, with a caret.

Version numbers, and the Apple problem

clang 21 (upstream)default dialect gnu17; C23 complete; the LLVM 21 releaseApple clang 21tracks XCODE, not LLVM. It is NOT upstream 21__clang_major__21 in both, meaning two different compilers__apple_build_version__defined only by Apple's buildclang --versionApple's says "Apple clang version 21.x (clang-2100.x.y.z)"
Never gate a feature on __clang_major__. Apple's version numbers and upstream's have been unrelated for a decade, so a comparison that is right on Linux is wrong on a Mac. Feature-test instead — __has_builtin, __has_attribute, __has_feature, __has_include — which is exactly what Clang invented them for.

Clang also claims to be GCC

__GNUC__ 4 / __GNUC_MINOR__ 2Clang has said this for fifteen years and will not move#if defined(__clang__)test this FIRST, then __GNUC____has_feature(c_atomic)the modern spelling Clang only__has_extension(x)true even outside the standard that added x Clang onlyclang -print-target-triplearm64-apple-darwin25.6.0, x86_64-pc-linux-gnu, …

Code that says #if __GNUC__ >= 5 to enable a feature loses it under Clang, forever, silently. That single line is the most common portability bug in GNU-flavoured C.

Is this machine's cc Clang?

cc --versionthe honest answerecho | cpp -dM - | grep __clang__anything back means Clangclang -print-prog-name=cc1Clang echoes the name; real GCC prints a pathxcrun -f clangon macOS, which clang the active SDK will use

Driving the Compiler

four phases, one process

The driver runs some prefix of the same four stages every C compiler does — but the assembler stage is inside the same binary, so there are fewer processes and fewer places to lose an argument.

clang -E f.cpreprocess only → stdoutclang -S f.c→ f.s, assemblyclang -c f.c→ f.o, straight out of MC — no `as` is forkedclang f.c -o progall four, then linkclang -fsyntax-only f.cparse and type-check only. Very fast; what clangd does per keystrokeclang -S -emit-llvm f.c→ f.ll, readable LLVM IR Clang onlyclang -c -emit-llvm f.c→ f.bc, bitcode

Seeing what the driver decided

clang -v f.crun, printing every sub-process with its full argvclang -### f.cprint the sub-commands, quoted, and run NOTHINGclang -save-temps f.ckeep f.i, f.bc, f.s, f.oclang -ccc-print-phases f.cthe phase graph the driver built Clang onlyclang -print-search-dirs / -print-file-name=libc.awhere it looks; resolve one libraryclang -print-target-triple / -print-targetsthis target; every target this build supportsclang -print-resource-dirwhere the builtin headers and the sanitizer runtimes live
-print-targets is the difference that matters. One clang binary contains every backend LLVM was built with, so cross-compiling is a --target= flag rather than a second toolchain. There is no aarch64-linux-gnu-clang, and there does not need to be.

Passing arguments past the driver

FlagGoes to
-Wp,-optthe preprocessor
-Wa,-optthe assembler (the integrated one, unless -fno-integrated-as)
-Wl,-optthe linker — commas become argument separators
-Xlinker argthe linker, one argument verbatim
-Xclang argcc1 directly — an unstable internal interface
-mllvm argthe LLVM backend's own cl::opt flags — also unstable
-Xarch_arm64 argonly for that slice of a universal build
-Xclang and -mllvm are not a public interface. They reach internal options that are renamed, repurposed and removed between releases without notice. Every one of them in a Makefile is a build that will break on the next compiler upgrade. If a Stack Overflow answer starts with -Xclang, look for the driver flag that does the same job first.

Input language and long lines

-x c / -x c-header / -x noneforce the language; build a PCH; go back to guessing.c .i .s .S .ll .bc .o .a .so .dylibthe suffixes it knowsclang @args.rsp -o progarguments from a file-fmax-errors=3 / -ferror-limit=3the second spelling is Clang's own; both work-o -write to stdout, with -E or -S

Uppercase .S goes through the preprocessor; lowercase .s does not — the same rule everywhere, and the same first-try failure in every hand-written assembly build.

Dialects & Defaults

gnu17, and why Clang did not move

Clang 21 still defaults to -std=gnu17. GCC 15 moved to gnu23. Nothing else about the split between the two compilers causes as much confusion, and the fix is one flag.

State -std= explicitly in every build file. Then a C23 feature that works here works there, code that assumes bool is a typedef keeps compiling, and the difference between the two default dialects stops being your problem. It costs eleven characters.

The dialect axis

-std=c89 / c90 / 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. The default dialect's base-std=c23ISO/IEC 9899:2024 — complete in Clang 18+-std=c2ythe next one, in flux; do not ship against it-std=gnuNNthe same, plus the GNU extensions Clang implementsclang -print-supported-cpusand, separately, what this target accepts for -mcpu

Turning strictness up

-pedanticdiagnose anything the chosen -std forbids-pedantic-errorsmake those hard errors — the real conformance switch-Wgnuwarn on every GNU extension in use, by name Clang only-Wgnu-statement-expression / -Wgnu-case-range …each one individually Clang only-std=c17 -pedantic-errors -Wgnuthe honest "is this portable C?" build-funsigned-charpin plain char signedness; it differs by target

-Wgnu has no GCC equivalent and is genuinely useful: it names every extension your code depends on, one warning per construct, so you can decide about each rather than discovering them on a different compiler.

Feature tests, which Clang invented

__has_builtin(__builtin_add_overflow)ask about one builtin__has_attribute(cleanup)ask about one attribute__has_c_attribute(fallthrough)the [[…]] spelling__has_feature(address_sanitizer)true when built with -fsanitize=address Clang only__has_extension(c_generic_selections)available even outside the standard that added it Clang only__has_include(<threads.h>)C23 standard; Clang had it first__has_builtin(__builtin_x) && !defined(__GNUC__)guard the guard: GCC only got __has_builtin in 10
Wrap the feature tests themselves. #ifndef __has_attribute / #define __has_attribute(x) 0 / #endif at the top of a header makes them safe under any compiler, including one that predates them. Every portable C project does this; it is four lines.

The Preprocessor

search order, deps and modules

The same four search lists as every C compiler, searched in the same 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, the SDK, the resource dir
-isystem, not -I, for third-party headers. Warnings are suppressed inside the system list, so somebody else's -Wsign-compare noise stops burying yours. On macOS the SDK arrives this way too — -isysroot $(xcrun --show-sdk-path).

Defining and interrogating

-DNAME=value / -UNAMEdefine; undefine, after every -D on the line-include cfg.has if #include "cfg.h" were line 1 — how config headers are delivered-imacros cfg.hits macros only, not its declarations-nostdinc / -nobuiltinincdrop the system list; drop Clang's own builtin headersecho | clang -dM -E -every predefined macro. The most useful preprocessor command there isclang -E -dD f.cpreprocessed output WITH the #defines keptclang -H f.cthe inclusion tree, indented

Dependencies

-MMD -MPthe pair you want, alongside -c: a .d file, plus phony targets for headers-MDthe same but including system headers-MF f.d / -MT targetwhere to write it; what to call the target-MJ f.o.jsonemit a compile-command fragment Clang onlyclang -MJ … *.c && cat *.json | sed -e '1s/^/[/' -e '$s/,$/]/' > compile_commands.jsona compilation database with no build system at all

Precompiled headers and modules

clang -x c-header pch.h -o pch.h.pchbuild a PCHclang -include-pch pch.h.pch f.cuse it-fmodulesC headers as modules; the default on macOS for framework headers-fmodules-cache-path=DIRwhere the compiled modules go-fno-implicit-modulesbuild them explicitly, for a reproducible build-fmodules-validate-system-headersthe fix for a stale module cache after an SDK update
A stale module cache produces errors that make no sense — a header "not found" that is plainly there, or a redefinition of something you never defined. After an Xcode or SDK update, delete the cache (~/Library/Caches/clang/ModuleCache, or the path -fmodules-cache-path names) before you believe any of it.

Diagnostics

the reason Clang exists

Clang was written because GCC's error messages were bad. Everything about the diagnostic engine — source ranges, carets, fix-its, notes attached to the declaration you got wrong — is the product, not decoration. It is worth knowing how to drive it.

The bundles

-Wallthe confident ones. Not "all", and never has been-Wextrathe next tier; -Wall -Wextra is the baseline-Wpedanticnon-conforming constructs for the chosen -std-Weverythingliterally every warning Clang has Clang only — see below-Werrorpromote all to errors-Werror=return-typepromote exactly one. Far more useful than blanket -Werror-Wno-error=deprecated-declarationsdemote one back, under a blanket -Werror
-Weverything is a discovery tool, not a policy. It includes mutually contradictory warnings, warnings about C++ in a C build, and -Wpadded, which fires on nearly every struct. Run it once, read the list, promote the six you agree with into your real flags, and turn it off. Shipping -Weverything -Werror guarantees your project stops building on the next Clang release.

Worth adding by hand

-Wshadowa local hiding an outer name-Wconversion -Wsign-conversionevery implicit narrowing-Wcast-qualcasting away const-Wformat=2-Wformat plus -security and -nonliteral-Wstrict-prototypes() means (void) in C23, but say so anyway-Wdocumentationdoc comments that disagree with the signature Clang only-Wthread-safetyreal lock analysis, given __attribute__((guarded_by(mu))) Clang only-Wunreachable-code-aggressivecode the optimiser proved dead-Wcommaa comma operator where you probably meant a semicolon Clang only-Wimplicit-fallthroughdemands [[fallthrough]]; comments do not count-Wnullable-to-nonnull-conversionwith _Nullable / _Nonnull annotations Clang only
-Wthread-safety has no GCC equivalent and is the strongest static tool in this toolchain. Annotate a mutex with __attribute__((capability("mutex"))) and the data it protects with __attribute__((guarded_by(mu))), and Clang checks every access at compile time — no run-time cost, no false negatives from an untaken path. It is what LLVM and Chromium use on their own locks.

Shaping the output

-fcaret-diagnostics / -fno-caret-diagnosticsthe source line and caret; on by default-fdiagnostics-fixit-infoshow the suggested editclang -Xclang -fixit f.cAPPLY the fix-its to the file, in place-fdiagnostics-show-template-treeC++ only, but the reason people love it-fcolor-diagnosticskeep colour through a pipe-fdiagnostics-print-source-range-infomachine-readable ranges, for an editor--serialize-diagnostics f.diaa binary log; how Xcode collects them Clang only-fno-diagnostics-show-optionhide the [-Wname] suffix — do not, it is how you turn one off

Suppressing precisely

#pragma clang diagnostic push / popalways as a pair#pragma clang diagnostic ignored "-Wunused"for this region only#pragma clang diagnostic warning "-Wshadow"or promote/demote in place_Pragma("clang diagnostic ignored \"-Wx\"")the form usable inside a macro__attribute__((unused))this one is deliberate-isystemsilence a whole third-party tree, properly

An unpaired push leaks to the end of the translation unit — and, since it is textual, into every file that includes the header it lives in. That and an over-broad -Wno- in CFLAGS are the two ways a project quietly loses a warning it thought it had.

Optimisation

the levels, and the LLVM pipeline

Each -O is a named pass pipeline. Unlike GCC's, you can print it, run it by hand, and diff it.

LevelIsCosts
-O0nothing, plus optnone on every functionspeed. The default
-O1the cheap, always-profitable passesa little debuggability
-O2inlining, vectorisation, the full pipelinedebuggability. The release default
-O3-O2 plus aggressive inlining and unrollingsize; sometimes speed, via icache
-Os-O2 tuned for sizea few per cent of speed
-Ozsize above all — even calls to memcpy for small copiesreal speed. Firmware
-Ofast-O3 -ffast-math and non-conforming behaviourcorrectness. Deprecated in Clang 19+
-Ogan alias for -O1 here, not its own level
-Ofast is deprecated and should not be in your build. It implies -ffast-math, which links a startup object that sets FTZ/DAZ process-wide — including for shared libraries that never asked for it — and licenses reassociation that changes results. Enable the specific relaxation you can defend (-fno-math-errno, -fno-signed-zeros) and write down why.

Telling Clang 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, keep the baseline ISA. Safe to ship-mcpu=apple-m1 … apple-m4the Apple silicon spelling; -mcpu, not -march, on AArch64-march=armv8.5-a+sve2AArch64 features are additive with +clang -print-supported-cpuswhat this target will acceptclang -E - -march=native -###what "native" expanded to

Seeing inside the pipeline

-Rpass=inlinea remark for every inlining decision made Clang only-Rpass-missed=loop-vectorizeevery loop that did NOT vectorise, and why. The best flag on this sheet-Rpass-analysis=loop-vectorizethe analysis behind the refusal-fsave-optimization-recordthe same, as YAML, for a whole build-mllvm -print-after-allthe IR after every pass. Enormous-S -emit-llvm -o -the IR itself, readableclang -O2 -S -emit-llvm f.c -o - | opt -passes='print<cost-model>'run LLVM passes by hand-mllvm -opt-bisect-limit=Nbisect WHICH pass broke your program Clang only
-opt-bisect-limit is the tool nobody knows about. It runs only the first N optimisation passes and reports which one it stopped at. Binary-search N until the miscompilation appears and LLVM names the pass that caused it — turning "the release build is broken" into a one-line bug report, in about ten minutes.

The flags with real consequences

-fno-strict-aliasingthe escape hatch. See the Language sheet before reaching for it-fwrapvsigned overflow wraps instead of being undefined. A real fix for old code-ftrapvsigned overflow traps instead. Prefer -fsanitize=signed-integer-overflow-fno-omit-frame-pointerkeep the frame pointer, for profilers. Apple silicon keeps it anyway-ffunction-sections -fdata-sectionswith -Wl,--gc-sections (or -dead_strip on macOS)-fvisibility=hiddenshould be on from day one in any library-fno-semantic-interpositionELF only; a real speed win for a shared library

ThinLTO & PGO

whole-program optimisation that still builds fast

LLVM's LTO comes in two shapes, and the interesting one is ThinLTO: it keeps a per-module summary rather than merging everything into one enormous module, so it parallelises and it caches. This is why LTO is a default on Apple platforms and an occasional heroic effort elsewhere.

-flto=thinon the compile AND the link line. The one you want-flto=fullthe classic monolithic LTO; better code on small programs, terrible link times on large ones-Wl,--thinlto-cache-dir=… (lld)incremental LTO. Turns a 40-second relink into 4-Wl,-cache_path_lto,… (ld64)the macOS spelling of the samellvm-ar / llvm-nm / llvm-ranlibuse these on archives of bitcode, not the plain ones-fno-ltoper-file opt-out, for a file with inline assembly LTO cannot see through
Plain ar cannot index a bitcode object. The archive builds, the link finds nothing in it, and you get undefined symbol for a function that is demonstrably there. Use llvm-ar (and llvm-ranlib) any time -flto is in CFLAGS.

Profile-guided optimisation, both flavours

InstrumentedSampled
Build-fprofile-instr-generateordinary -O2 -g
Collectrun it; writes default.profrawperf record -b, then create_llvm_prof
Mergellvm-profdata merge -output=x.profdata *.profrawsame
Rebuild-fprofile-instr-use=x.profdata-fprofile-sample-use=x.prof
Cost2–4× slower while collecting~1% — can profile production
LLVM_PROFILE_FILE="p-%p.profraw" ./prog%p is the pid; %m the module. Without it, parallel runs overwrite each otherllvm-profdata show --topn=10 x.profdatasanity-check the profile before trusting it-fprofile-generatethe GCC-compatible spelling; -fprofile-instr-generate is the LLVM-native one-Wprofile-instr-out-of-datethe source moved on since the profile. Do not ignore this-fcs-profile-generatecontext-sensitive, a second pass after the first

Sampled PGO is the underrated one: no instrumented build, no separate binary to ship, and you can collect from the real workload in production. Google runs its whole fleet this way.

What a release build looks like

-O2 -flto=thin -fprofile-instr-use=x.profdata -fvisibility=hiddenthe full combination-Wl,--thinlto-cache-dir=.ltocache -Wl,--thinlto-cache-policy=cache_size=10%bounded incremental LTO-Wl,-mllvm,-import-instr-limit=40the ThinLTO inlining budget, when link time matters more than the last 2%

Debug Info & lldb

and the .dSYM question

-O2 -g is the normal shape of a shippable build. What differs by platform is where the debug info ends up.

-gfull DWARF-gline-tables-onlyfile and line only — enough for a backtrace, a fraction of the size-g1 / -g2 / -g3-g3 adds macro definitions so the debugger can expand them-gdwarf-5 / -gdwarf-4the DWARF version-fstandalone-debugemit full type info even for types used only by reference. The default on macOS-fno-standalone-debugthe smaller default elsewhere-gsplit-dwarf.dwo files, for much faster links (ELF)-fdebug-macromacro info, without all of -g3
On macOS, debug info does not live in the binary. The compiler leaves it in the .o files and the linker records a debug map; lldb follows that map back to the objects. Delete the build directory and your executable becomes undebuggable. dsymutil prog gathers it into a prog.dSYM bundle you can archive — do that for every release, and keep it beside the binary.

lldb, the commands that cover most sessions

lldb -- ./prog a bstart with argv; everything after -- is the program'slldb -p PID / lldb -c core ./progattach; post-mortemr / c / n / s / finishrun, continue, over, into, out — the gdb spellings all workb main.c:42 / b func / b -n func -c 'x==3'breakpointsbr l / br del 1 / br dis 1list, delete, disablebt / bt allthis thread's stack; every thread'sfr vframe variable — locals AND arguments, without evaluating anythingp expr / po objevaluate; print the descriptionv -T xshow types alongside valuesparray 10 ptrten elements from a pointer (gdb's p *p@10 also works)me read -c16 -fx -s1 pexamine memory; x/16xb works tooim li / im loo -a 0x1000image list; address to symbol and lineguithe curses interfacelldb -o bt -b ./progscripted, batch
fr v beats p for looking around. frame variable reads the variables out of the debug info directly; p compiles and runs an expression in the inferior, which can have side effects, can fail, and is far slower. Use p when you need to compute something, fr v when you just want to look.

The session

~/.lldbinitstartup file; a project one must be allowed with settings set target.load-cwd-lldbinit truesettings set target.run-args a bargv, without restartingtype summary add -s "${var.x}, ${var.y}" Pointa custom formatter for your own typescript print(lldb.frame.FindVariable("x"))lldb embeds Python; the whole API is exposedcommand script import ~/fmt.pyload your own commandsexpr -- (void)fflush(0)call a function in the inferiorwatchpoint set variable xbreak on writethread backtrace allthe deadlock command

Cores and crash logs

ulimit -c unlimitedLinux. On macOS also /cores must exist and be writable~/Library/Logs/DiagnosticReports/macOS keeps .ips crash reports here insteadatos -o prog.dSYM/Contents/Resources/DWARF/prog -l 0x100000000 0x100001234symbolicate an address by handllvm-symbolizer < addresses.txtthe cross-platform versionlldb -c /cores/core.1234 ./progopen a core

Sanitizers

the most complete set anywhere

The sanitizers are LLVM's, and Clang has all of them — including two GCC does not implement. They are instrumentation plus a runtime, they cost 1.2–20×, and they belong in the test suite.

FlagCatchesCost
-fsanitize=addressoverflow, use-after-free, double free, leaks~2×, ~3× memory
-fsanitize=undefinedsigned overflow, bad shifts, null deref, misalignment, bad enum~1.2×
-fsanitize=threaddata races — real ones, with both stacks~10×, ~7× memory
-fsanitize=memoryreads of uninitialised memory Clang only~3×
-fsanitize=cfiindirect calls through the wrong function type Clang onlysmall; needs LTO
-fsanitize=safe-stackstack-smashing, by splitting the stack Clang only~1%
-fsanitize=fuzzer— it is libFuzzer, the coverage-guided fuzzer Clang only
MemorySanitizer needs the whole program instrumented, libc included. An uninstrumented library that writes to a buffer looks to MSan like memory that was never initialised, so you get a flood of false positives. In practice it means building against an instrumented libc++/musl, which is why MSan is the least-used of the four despite catching the bug class valgrind is famous for.

Driving them

-fsanitize=address,undefined -fno-omit-frame-pointer -gthe everyday pair, with usable stacks-fno-sanitize-recover=allabort on the first finding rather than logging and carrying on-fsanitize-trap=undefinedtrap instead of calling a runtime — no runtime library needed. For firmware-fsanitize-minimal-runtimetiny UBSan runtime, for shipping it enabled-fsanitize-ignorelist=ign.txtexclude functions, files or types by pattern Clang only__attribute__((no_sanitize("address")))exclude one function-fsanitize=integerincluding UNSIGNED overflow, which is defined but usually still a bug Clang only-fsanitize=implicit-conversionsilent narrowing at run time Clang only-fsanitize=local-boundsbounds checks on local arrays
ASAN_OPTIONS=detect_leaks=1:abort_on_error=1leaks on; stop where a debugger can catch itASAN_OPTIONS=detect_stack_use_after_return=1off by default; catches a whole familyUBSAN_OPTIONS=print_stacktrace=1UBSan prints no stack without it. Set it in the harnessTSAN_OPTIONS=second_deadlock_stack=1both sides of a lock-order inversionMSAN_OPTIONS=poison_in_dtor=1ASAN_SYMBOLIZER_PATH=$(which llvm-symbolizer)when the report is all hex
ASan and TSan cannot be combined — both own the allocator and the shadow mapping. UBSan combines with either. Every sanitizer must appear on the link line too, or the runtime is missing.

Fuzzing, which is nearly free here

int LLVMFuzzerTestOneInput(const uint8_t *d, size_t n){…}write this one functionclang -fsanitize=fuzzer,address,undefined fuzz.c -o fuzzbuild it./fuzz corpus/ -max_total_time=60run it. It grows the corpus itself./fuzz -runs=0 corpus/replay the corpus as a regression test in CI-fsanitize-coverage=trace-pc-guardthe coverage instrumentation, if you drive your own fuzzer

Any function that takes a buffer and a length is a candidate. Parsers, decoders and format readers usually yield a crash within minutes the first time they are fuzzed — and libFuzzer minimises the input for you.

The Tooling Around It

tidy, format, clangd, scan-build

This is the part of the toolchain GCC has no answer to, because Clang is a library. All four tools read the same compile_commands.json, so the first job on any project is to produce one.

cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ONthe easy pathninja -C build -t compdb > compile_commands.jsonfrom any Ninja buildbear -- makeintercept an arbitrary build; works with hand-written Makefilesclang -MJ f.o.json …emit fragments and concatenate them yourselfln -s build/compile_commands.json .the tools look upward from the source file; put it at the root

clang-tidy

clang-tidy f.c -- -std=c17the args after -- are the compile line, when there is no databaseclang-tidy -p build f.cuse the database in build/clang-tidy -checks='-*,bugprone-*,cert-*' f.cstart from nothing and add. The default set is broadclang-tidy -list-checks -checks='*'every check there is (about 400)clang-tidy -fix f.capply the fixes it is confident aboutrun-clang-tidy -p build -j8the whole project, in parallel.clang-tidya YAML file at the project root; what CI should read// NOLINT(bugprone-x)suppress one, on one line, by name// NOLINTBEGIN / NOLINTENDa region

The checks worth starting with in C: bugprone-*, cert-*, clang-analyzer-* (the static analyzer, run as a tidy check), and misc-*. Skip readability-* until the codebase is clean, or the signal drowns.

The static analyzer

scan-build makewrap any build; it substitutes itself for the compilerscan-build -V makeopen the HTML report when it finishesscan-build -enable-checker alpha.security.ArrayBound makethe experimental checkersclang --analyze f.cone file, straight from the driverclang --analyze -Xanalyzer -analyzer-output=text f.cthe path, in the terminal rather than HTML
The analyzer's HTML report is the feature. It draws the exact path through your functions that reaches the bug, arrow by arrow, with the value of each variable at each step. For a null-dereference eight calls deep it is the difference between a warning you dismiss and a bug you fix in five minutes.

clang-format and clangd

clang-format -i f.cformat in placeclang-format --style=llvm --dump-config > .clang-formatstart a config from a presetgit clang-formatformat ONLY the lines you changed. The way to adopt it on an old codebaseclang-format --style=file:.clang-format-c f.ca named config// clang-format off / onprotect a hand-aligned tableclangd --check=f.cdebug why the editor's language server is confused about a fileclangd --query-driver=/usr/bin/arm-none-eabi-gcclet it learn a cross compiler's system headers.clangdper-project language-server config: added flags, removed flags, index settings

clangd is the same parser as the compiler, which is the whole point: go-to-definition, find-references and rename are exact rather than heuristic, and the diagnostics in the editor are the ones the build will produce. It is what every editor's C support is built on now, Zed and VS Code included.

Linking

archives, dylibs and the two linkers

Clang forks a linker; which one it forks decides both the speed and the spelling of half these flags. On Linux it is ld.bfd unless you say otherwise; on macOS it is always Apple's ld64.

clang -fuse-ld=lld …LLVM's linker: ELF, COFF and Mach-O, roughly 4x ld.bfdclang -fuse-ld=mold …faster still on ELFclang -Wl,--versionwhich linker actually ran (ELF)ld64: -Wl,-vthe macOS equivalent-B/path/to/dirthe old way: point at a directory holding an `ld`

Static archives — the rule that explains every failure

The linker walks the command line left to right holding a set of undefined symbols, and pulls a member out of an archive only if it defines something currently undefined.

clang main.o -L. -lx -o proglibraries AFTER the objects that need them. Alwaysllvm-ar rcs libx.a a.o b.ocreate with an index; llvm-ar handles bitcode too-Wl,--whole-archive -lplugin -Wl,--no-whole-archivetake every member (ELF)-Wl,-force_load,libplugin.athe same on macOS-Wl,-u,symbolpretend it is undefined, to pull its member in-Wl,--start-group … -Wl,--end-groupbreak a cycle between archives (ELF only; ld64 rescans anyway)

Shared libraries, both spellings

ELF (Linux)Mach-O (macOS)
build-shared -fPIC-dynamiclib (PIC is mandatory)
filelibx.so.1.2.3libx.1.dylib
recorded name-Wl,-soname,libx.so.1-Wl,-install_name,@rpath/libx.1.dylib
search at run time-Wl,-rpath,'$ORIGIN/../lib'-Wl,-rpath,@loader_path/../lib
list dependenciesreadelf -dotool -L / dyld_info
no undefined symbols-Wl,-z,defsthe default
drop dead code-Wl,--gc-sections-Wl,-dead_strip
@rpath is not optional on macOS. A dylib records its own install name inside itself, and every program that links it copies that string. Build one with a plain relative name and every consumer records a path that only resolves from the directory you happened to build in. Set -install_name @rpath/libx.1.dylib when you build the library, and -rpath when you link the program.

Visibility, which should be a default

-fvisibility=hiddennothing exported unless it says so. Put it in CFLAGS on day one__attribute__((visibility("default")))this one is public-Wl,--version-script=x.mapELF: versioned symbols-Wl,-exported_symbols_list,x.txtthe macOS equivalentllvm-nm -D --defined-only libx.socheck what you actually export-Wl,-dead_strip -Wl,-map,out.mapmacOS: strip and then read the map

The LLVM Tools

the binutils replacements, plus some

LLVM ships a drop-in for every binutils program and several with no counterpart. They read ELF, Mach-O, COFF and WebAssembly from one binary, which is the reason to prefer them: the same command works on every target you cross-compile for.

LLVMReplacesNotes
llvm-nmnmon macOS, nm already is this
llvm-objdumpobjdump--x86-asm-syntax=intel for Intel syntax
llvm-readobj / llvm-readelfreadelfreadelf mode takes the GNU flags
llvm-strip, llvm-objcopystrip, objcopysame options, all formats
llvm-ar, llvm-ranlibar, ranlibrequired under LTO
llvm-symbolizeraddr2linewhat the sanitizers call to make a stack readable
llvm-cxxfiltc++filt
llvm-size, llvm-stringssize, strings
llvm-mcaa machine-code analyser: throughput and port pressure, statically
llvm-boltpost-link layout optimisation from a perf profile

The questions, and the commands

file progformat, arch, PIE, stripped. Still the first questionllvm-size -A progper-section sizesllvm-nm --size-sort -S prog | tailwhere the size wentllvm-nm -u progwhat is still undefinedllvm-objdump -d --no-show-raw-insn progreadable disassemblyllvm-objdump -dS progwith source interleavedllvm-readelf -d progNEEDED, SONAME, RPATH, RUNPATHllvm-readobj --macho-dysymtab progMach-O metadata, structuredllvm-symbolizer --obj=prog 0x1234address to file:line, following inlinesllvm-strip --strip-unneeded libx.sothe right strip for a shared objectllvm-objcopy -O binary f.elf f.bina raw image, for flashing

The two with no GNU counterpart

llvm-mca -mcpu=skylake f.ssimulate a block: cycles, IPC, which port is saturatedllvm-mca -timeline f.sa per-instruction timeline. Where a hot loop's stall actually isllvm-bolt prog -o prog.bolt -data=perf.fdatareorder the binary from a profile; 5-15% on large programsperf2bolt -p perf.data -o perf.fdata progconvert the profile first

llvm-mca answers "why is this loop slow?" without running it, at instruction granularity. Paste in the output of clang -S -O2 for one function and it tells you the bottleneck resource. Nothing in binutils does this.

Coverage & Profiling

source-based coverage, done properly

Clang's coverage is source-based rather than line-table-based, which means it is accurate at -O2 and it counts regions — so it can tell you that a condition was evaluated 40 times and was true 40 of them.

clang -fprofile-instr-generate -fcoverage-mapping -O2 -g …build; note BOTH flagsLLVM_PROFILE_FILE=p.profraw ./progrunllvm-profdata merge -sparse p.profraw -o p.profdatamerge, always — even one filellvm-cov show ./prog -instr-profile=p.profdataannotated source in the terminalllvm-cov show -format=html -output-dir=cov ./prog -instr-profile=p.profdatathe browsable reportllvm-cov report ./prog -instr-profile=p.profdatathe summary tablellvm-cov export -format=lcov …for a CI service that wants lcov-fcoverage-mapping -mllvm -runtime-counter-relocationneeded for coverage of a shared library on some targets
Region coverage is a better question than line coverage. llvm-cov show --show-branches=count marks the branch conditions your tests never took the other way — which is where the bugs are. A file at 100% line coverage routinely sits at 60% branch coverage.

gcov compatibility, when a tool demands it

clang --coverage …the GCC-compatible path: .gcno / .gcda filesllvm-cov gcov f.gcdaread them with the gcov interfacegcovr / lcovwork unchanged against those

Profiling

perf record -g ./prog && perf reportLinux. Build with -g -fno-omit-frame-pointersamply record ./progcross-platform, opens the Firefox profiler UI. Works on macOSxcrun xctrace record --template 'Time Profiler' --launch ./progInstruments from the command linesample PID 10macOS: ten seconds of stacks, no setup at allleaks --atExit -- ./progmacOS leak check with no rebuildDYLD_PRINT_STATISTICS=1 ./progwhere launch time went, on macOShyperfine './a' './b'proper statistics on wall-clockvalgrind --tool=callgrind ./progexact instruction counts; ~50x, but reproducible

On Apple silicon perf does not exist. samply, sample and Instruments are the replacements, and Instruments is the only one that sees the efficiency/performance core split.

Seeing the IR

the compiler you can take apart

LLVM IR is a real, documented, textual language, and every stage of the pipeline will hand it to you. This is the single biggest practical difference from GCC: when the optimiser does something surprising, you can watch it happen.

clang -S -emit-llvm -O0 f.c -o -the IR as CodeGen produced it, unoptimisedclang -S -emit-llvm -O2 f.c -o -after the full pipelineclang -c -emit-llvm f.c -o f.bcbitcode, the binary formllvm-dis f.bc -o -bitcode back to textllvm-as f.ll -o f.bcand back againopt -O2 -S f.ll -o -run the middle end by handopt -passes='mem2reg,instcombine' -S f.llrun named passes, in your orderopt -print-before-all -print-after-all -S f.llthe IR at every step. Enormous, and occasionally the only wayllc -O2 f.ll -o -the back end alone: IR to assemblyllc -march=aarch64 f.ll -o -the same IR, a different machine

What to look for

allocaa stack slot. If your local still has one after -O2, it did not get promoted to a registerload / storecounting these is how you see whether a struct copy was elided!tbaatype-based alias analysis metadata — strict aliasing, made visiblensw / nuw"no signed/unsigned wrap": signed overflow being undefined, written down!llvm.loop.isvectorizedthe loop was vectorisedllvm.memcpy.p0.p0.i64your loop became a memcpy callunreachablethe optimiser proved a path cannot be taken — usually because of undefined behaviour@llvm.assumea fact the front end guaranteed the optimiser
An unreachable where you did not expect one is the signature of undefined behaviour. It means the optimiser proved this path is only reached by a program that already had UB, so it deleted the path — including, sometimes, your null check. Build the same file with -fsanitize=undefined and it will name what it found.

The assembly, when that is what you want

clang -S -O2 -masm=intel f.c -o -Intel syntax on x86clang -S -O2 -fverbose-asm f.cwith source-level commentsclang -O2 -c f.c && llvm-objdump -d --no-show-raw-insn f.owhat really got emitted, after the assemblerclang -Xclang -ast-dump -fsyntax-only f.cthe AST, before any of thisclang -cc1 -ast-print f.cthe source, reprinted from the AST — macros expanded

Cross-Compiling

one binary, every target

A single clang contains every backend LLVM was built with, so a cross build is a flag rather than a second toolchain. What you still have to supply is the target's headers, libraries and linker.

clang -print-targetsevery architecture this build can emitclang --target=aarch64-linux-gnu -c f.ccompile for it. That is the whole compiler side--sysroot=/path/to/rootfswhere the target's headers and libraries are--gcc-toolchain=/usr/aarch64-linux-gnufind the target's crt files and libgcc-fuse-ld=lldlld cross-links natively; the host `ld` cannot--target=thumbv7em-none-eabi -mcpu=cortex-m4 -mfpu=fpv4-sp-d16bare-metal Cortex-M4--target=riscv32-unknown-elf -march=rv32imac -mabi=ilp32RISC-V--target=wasm32-wasi --sysroot=/opt/wasi-sysrootWebAssembly
The compiler is never the hard part of cross-compiling. It is the sysroot, the linker and pkg-config. Set --sysroot, use -fuse-ld=lld, and set PKG_CONFIG_LIBDIR/PKG_CONFIG_SYSROOT_DIR so pkg-config stops answering with host paths — those three cover almost every failure.

Bare metal

-nostdlib -ffreestandingno libc, no startup files, no assumptions-nostdlibincand none of its headers either--rtlib=compiler-rtLLVM's runtime instead of libgcc — the division and 64-bit helpers-print-libgcc-file-namewhich runtime you actually got-Wl,-T,link.ldthe linker script. lld implements most, not all, of GNU ld's syntax-Wl,--gc-sections -ffunction-sections -fdata-sectionsthe flash-size triollvm-size f.elfdoes it fit?llvm-objcopy -O binary f.elf f.binthe image to flash

Universal binaries, which are the macOS version of this

clang -arch arm64 -arch x86_64 f.c -o progboth slices, one command, one file-Xarch_x86_64 -mavx2a flag for one slice onlylipo -info progwhich architectures are in therelipo prog -thin arm64 -output prog.arm64pull one outlipo -create a.arm64 b.x86_64 -output progglue two separately built ones together-target arm64-apple-macos12architecture and minimum OS in one

Testing 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 firmwarearch -x86_64 ./progrun the Intel slice under Rosetta on Apple siliconopenocd + lldb, target remote :3333on-chip debugging; lldb speaks the gdb remote protocol

This Mac, Specifically

xcrun, the SDK, and Mach-O

On macOS the toolchain is Apple's fork, the headers come from an SDK that moves with Xcode, and several things you expect from Linux are simply absent. None of it is hard once the pieces are named.

xcode-select -pwhich developer directory is activexcrun -f clangthe clang that directory will usexcrun --show-sdk-paththe SDK; -isysroot wants thisxcrun --show-sdk-versionwhich SDK you are building againstxcode-select --installthe Command Line Tools, without all of Xcodesudo xcode-select -s /Applications/Xcode.appswitch from the CLT to the full Xcodeclang -mmacosx-version-min=13.0the deployment targetMACOSX_DEPLOYMENT_TARGET=13.0the same, from the environment
Building against a new SDK does not mean running on an old OS. The SDK version sets what exists; -mmacosx-version-min sets what you promise to run on. Get the second wrong and your binary uses a symbol that is missing on the machine it ships to, and dies at launch with Symbol not found. Use __builtin_available(macOS 14, *) for anything newer than your minimum.

What is different, concretely

LinuxmacOS
lddotool -L, or dyld_info -dependents
LD_LIBRARY_PATHDYLD_LIBRARY_PATH — stripped by SIP for system binaries
LD_PRELOADDYLD_INSERT_LIBRARIES — blocked by SIP and the hardened runtime
$ORIGIN@loader_path (and @executable_path, @rpath)
--gc-sections-dead_strip
-soname-install_name
debug info in the binarya debug map into the .o files; dsymutil to collect
static libcnot supported at all — you must link libSystem dynamically
perfInstruments, sample, samply

Mach-O tools

otool -L progdylib dependenciesotool -l progevery load command: LC_RPATH, LC_ID_DYLIB, LC_UUIDotool -hv progthe header, symbolically — PIE, TWOLEVEL and the restotool -tV progdisassemblyinstall_name_tool -id @rpath/libx.dylib libx.dylibfix an install name after the factinstall_name_tool -change old new progrepoint one dependencyinstall_name_tool -add_rpath @loader_path/../lib progadd a search pathdsymutil progbuild prog.dSYM. Do this for every releasevtool -show progplatform and minimum OSdyld_info -dependents -platform progthe modern replacement for most of otool
Every install_name_tool edit invalidates the code signature. On Apple silicon an unsigned or badly signed binary will not run at all. Re-sign afterwards with codesign -f -s - prog (ad-hoc) — and check with codesign -dv --verbose=4 prog.

Signing, the minimum you need to know

codesign -f -s - progad-hoc sign. Enough to run locallycodesign -dv --verbose=4 proginspect a signaturecodesign --entitlements e.plist -f -s - progentitlements, e.g. get-task-allow for debuggingspctl -a -vv progwhat Gatekeeper thinksxattr -d com.apple.quarantine fclear the quarantine flag on a downloaded file

Getting Clang

three different compilers with one name

"clang" on a given machine is one of three things, and they differ in ways that matter.

WhichGet it withNotes
Apple clangxcode-select --installthe only one that fully knows the SDK, frameworks and availability attributes
Upstream LLVM, macOSbrew install llvmkeg-only; add $(brew --prefix llvm)/bin to PATH deliberately
Upstream LLVM, Linuxapt install clang-21, dnf install clang, apt.llvm.orgversioned binaries: clang-21, clang-tidy-21
brew install llvmgets clang, clang-tidy, clang-format, lld, lldb and every llvm-* toolexport PATH="$(brew --prefix llvm)/bin:$PATH"keg-only means it is NOT on PATH by default. That is deliberate$(brew --prefix llvm)/bin/clang --versionuse it without changing PATH at allclang-format --versionApple ships clang-format; it does NOT ship clang-tidybash llvm.sh 21apt.llvm.org's installer, for a version Debian does not carry
Apple's clang does not include clang-tidy, scan-build, opt, llc or most llvm-* tools. That is the main practical reason to install Homebrew's LLVM alongside it — not to replace the compiler, but to get the tooling. Use Apple's clang to build, and Homebrew's tools to analyse.

Which one is running

clang --version"Apple clang version …" or "clang version …" — the distinction is the whole answerecho | clang -dM -E - | grep __apple_build_version__defined only by Apple'sclang -print-resource-dirthe builtin headers and sanitizer runtimes in useclang -print-target-triplearm64-apple-darwin25.6.0 on this Macclang -### f.c 2>&1 | head -3the full path to the cc1 that would run

Using it with a project that expects GCC

make CC=clangusually all it takes; Clang accepts the GCC driver flagsCC=clang CFLAGS='-Wno-unknown-warning-option'for a Makefile full of GCC-only -W flags-Qunused-argumentssilence "argument unused during compilation", which build systems generate constantly-fgnuc-version=15.1lie about __GNUC__, for a header that gates on it Clang only-Wno-gnuthe inverse of -Wgnu, when you have decided the extensions are fine

Building the same project under both compilers is worth the afternoon it costs: each finds real bugs the other misses, and the differences in their warnings are complementary rather than redundant. That is the actual argument for keeping both installed.

Flag, Attribute & Tool Index

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

Driver & Output

33

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-target-triplenormalised triple
-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
-Xclang ARGstraight to cc1
-mllvm ARGan LLVM backend cl::opt
--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
--target=TRIPLEcross-compile target
-isysroot DIRheaders-only sysroot

Language & Dialect

32

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

34

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
-ivfsoverlay fa virtual file system map

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
-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
__has_feature(x)a language or sanitizer feature
__has_extension(x)available even outside its standard
__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
-Weverythingliterally every Clang warning
-wsuppress all warnings
-Werrorwarnings become errors
-Werror=NAMEpromote one warning
-Wno-error=NAMEdemote one under -Werror
-Wfatal-errorsstop at the first error
-ferror-limit=Ncap the error count

Correctness

-Wuninitializeduse before set
-Wconditional-uninitializedthe Clang analogue
-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
-Wloop-analysisa loop variable that never changes
-Wcommasuspicious use of the comma operator

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
-Wshorten-64-to-32narrowing a 64-bit value to 32
-Wimplicit-int-conversionnarrowing integer conversion
-Wassign-enumassigning a value outside the enum range
-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
-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
-Wdocumentationdoc comment does not match the declaration
-Wextra-semia stray semicolon
-Wnewline-eofno newline at end of file
-Wpoison-system-directoriesa host include dir in a cross build

Concurrency and analysis

-Wthread-safetylock discipline, statically checked
-Wthread-safety-analysisthe core of the above

Suppressing precisely

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

Optimisation

47

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
-ftrivial-auto-var-init=zerozero uninitialised locals

Loops and vectorisation

-ftree-vectorizeenable the vectoriser
-fno-vectorizeturn it off
-fvect-cost-model=dynamicvectoriser cost model
-Rpass=loop-vectorizewhat was vectorised
-Rpass-missed=loop-vectorizewhat was not
-Rpass-analysis=loop-vectorizethe cost model reasoning
#pragma GCC ivdepassert no loop-carried dependence
#pragma clang loop vectorize(enable)per-loop control

Link-time and profile-guided

-fltolink-time optimisation
-flto=thinThinLTO: parallel, incremental
-fprofile-instr-generateinstrument for PGO
-fprofile-instr-use=f.profdataconsume a merged profile
-fprofile-sample-use=fsampling PGO
-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

-fsave-optimization-recordYAML optimisation remarks
-emit-llvm -Sdump LLVM IR
-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

24

Levels and formats

-gdebug info at the default level
-g0 / -g1 / -g2 / -g3increasing detail
-gdwarf-4 / -gdwarf-5pin the DWARF version
-gsplit-dwarfdebug info into .dwo files
-gzcompress debug sections
-g -O2debug info for an optimised build
-gmodulesdebug info in module form
-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
-fcoverage-mappingsource-based coverage
-pggprof instrumentation
-finstrument-functionscall your hooks on entry and exit
-fxray-instrumentXRay: patchable instrumentation
-fstack-usagewrite a .su file per function
-fcallgraph-infoemit a call graph

Sanitizers & Hardening

44

The sanitizers

-fsanitize=addressASan: overflow and use-after-free
-fsanitize=undefinedUBSan: the UB catalogue
-fsanitize=threadTSan: data races
-fsanitize=memoryMSan: uninitialised reads
-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
-fsanitize-ignorelist=fsuppress by regex
__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
-fsanitize=implicit-conversionsilent narrowing at run time
-fsanitize=unsigned-integer-overflowunsigned wraparound
-fsanitize=cficontrol-flow integrity
-fsanitize=safe-stacksplit the unsafe stack out
-fsanitize=shadow-call-stacka shadow return stack

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)

28

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
-rtlib=compiler-rtuse compiler-rt not libgcc
-unwindlib=libunwindwhich unwinder
-fuse-ld=lldchoose the linker
--ld-path=/usr/bin/moldan exact linker path

macOS spellings

-dynamiclibbuild a .dylib
-bundlebuild a loadable bundle
-Wl,-install_name,@rpath/libx.dylibthe soname equivalent
-Wl,-rpath,@loader_path/../libthe $ORIGIN equivalent
-Wl,-dead_stripthe --gc-sections equivalent
-Wl,-map,out.mapthe -Map equivalent
-Wl,-exported_symbols_list,fthe version-script equivalent
-arch arm64 -arch x86_64universal binary
-mmacosx-version-min=13.0deployment target

Linker Options

39

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
-Wl,--icf=allmerge identical functions

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,--statstime and memory per phase
-Wl,--warn-commonwarn on common symbols
-Wl,--warn-backrefswarn on order-dependent resolution

Analysis & Diagnostics

22

Static analysis

clang --analyzeClang Static Analyzer, one file
scan-build make -j8analyse a whole build
clang-tidy -p build src/*.cAST lint plus analyser checks
clang-tidy --checks=... -fixand rewrite the code
clang-tidy --list-checkswhat is available
cppcheck --enable=all src/independent analyser
include-what-you-useheader hygiene

Diagnostic presentation

-fdiagnostics-color=alwayskeep colour through a pipe
-fcolor-diagnosticsthe older Clang spelling
-fdiagnostics-format=sarifSARIF for CI ingestion
-fno-diagnostics-show-caretone line per diagnostic
-fdiagnostics-show-optionname the -W flag responsible
-fdiagnostics-show-hotnessannotate remarks with profile weight
-fdiagnostics-print-source-range-infomachine-readable ranges

Formatting and tooling

clang-format -i f.cformat in place
clang-format --style=fileread .clang-format
clang-format --dump-config --style=GNUa starting config
git clang-formatformat the staged hunks only
clangdthe language server
bear -- makegenerate compile_commands.json
ninja -t compdb > compile_commands.jsonstraight from ninja
-DCMAKE_EXPORT_COMPILE_COMMANDS=ONCMake writes it for you

Attributes

48

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
optnonedo not optimise this function
musttailguarantee a tail call
target("avx2")compile one function for another ISA

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
diagnose_if(cond, "msg", "error")a conditional diagnostic
guarded_by(mutex)thread-safety annotation

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
ext_vector_type(4)OpenCL-style vectors with .xyzw
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

Builtins

40

Branches and constants

__builtin_expect(x, 1)branch probability hint
__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_assume(cond)assert a condition
__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

37

Which compiler

__GNUC__GNU-compatible compiler
__GNUC_MINOR__ __GNUC_PATCHLEVEL__the rest of the version
__clang__this is Clang
__clang_major__ __clang_minor__Clang version
__apple_build_version__Apple toolchain build
__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

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

17

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 clang diagnostic ...the Clang spelling
#pragma message("text")print at compile time

Optimisation

#pragma clang loop vectorize(enable)per-loop control
#pragma clang optimize offstop optimising from here
#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

lldb

55

Starting

lldb -- ./prog a bstart with argv set
lldb -p PIDattach to a running process
lldb -c core ./progpost-mortem on a core file
lldb -o bt -b ./progbatch: run commands, then quit
lldb -s script.txt ./proga command file
r / runstart the program
process launch --stop-at-entryrun and stop before main
settings set target.run-args a bchange argv without restarting
settings set target.env-vars K=Vset an environment variable
process kill / detachstop, or let it go on running

Breakpoints and watchpoints

b main.c:42by file and line
b funcby function name
b -n func -c 'x==3'conditional
br s -r ^parse_a regex over function names
br s -S selectorby Objective-C selector
br l / br del 1 / br dis 1list, delete, disable
br mod -i 100 1ignore the next 100 hits of #1
br com add 1run commands when it hits
w s v xwatchpoint: stop when x is written
w s e -- 0x1000watch an address rather than a variable
br s -E c++ / -E objcbreak on a thrown exception

Stepping

ccontinue
n / s / finishover, into, out
ni / sione instruction
thread step-inst-overthe explicit form of ni
thread until 50run to line 50 in this frame
thread return 0force a return now
thread jump --line 50move the PC

The stack and the data

btbacktrace this thread
bt allevery thread’s stack
f 2 / up / downselect a frame
fr vevery local and argument
fr v -T xwith types shown
p exprevaluate a C expression
p/x nformat the result
parray 10 ptrten elements from a pointer
me read -c16 -fx -s1 pexamine raw memory
me write -s4 &x 42write to memory
expr x = 5change a variable
ta l / target variableglobals and statics
type lookup Foothe definition of a type
register read / p $spregisters

Modules, threads and the session

im liloaded modules and their addresses
im loo -a 0x100003f2caddress to symbol, file and line
im loo -n mallocfind a symbol across every module
im du sectevery section of every module
th l / th sel 2list threads; switch
th bt allevery stack
di -fdisassemble the current function
guithe curses interface
type summary add -s "${var.x}" Pointa formatter for your own type
script print(lldb.frame)drop into embedded Python
command script import ~/f.pyload your own commands
~/.lldbinitstartup file
apropos rpathsearch the command help

LLVM Tools

41

First questions

file progformat, arch, PIE, stripped
llvm-size -A progper-section sizes
llvm-nm --size-sort -S prog | tailthe biggest symbols
llvm-nm -u progstill-undefined symbols
llvm-nm -D --defined-only libx.sowhat a shared object exports
llvm-strings -n 8 progprintable runs
llvm-cxxfilt _Z3fooidemangle

Metadata

llvm-readelf -d progNEEDED SONAME RPATH RUNPATH
llvm-readelf -h / -S / -l progheader, sections, segments
llvm-readobj --all progeverything, structured
llvm-readobj --macho-segment progMach-O segments and sections
otool -L progdylib dependencies (macOS)
otool -l progevery Mach-O load command
dyld_info -dependents progthe modern otool -L
vtool -show progplatform and minimum OS
lipo -info progwhich architectures are in a fat file

Disassembly and addresses

llvm-objdump -d progdisassemble
llvm-objdump -dS progwith source interleaved
llvm-objdump -d --x86-asm-syntax=intelIntel syntax
llvm-objdump --macho -d progMach-O mode
llvm-symbolizer --obj=prog 0x1234address to function and file:line
atos -o prog.dSYM/…/prog -l BASE ADDRthe macOS symbolicator
llvm-mca -mcpu=skylake f.sstatic throughput analysis
llvm-mca -timeline f.sa per-instruction timeline

Rewriting and archives

llvm-ar rcs libx.a a.o b.ocreate an archive
llvm-ranlib libx.arebuild the index
llvm-strip -s progremove symbols
llvm-strip --strip-unneeded libx.sokeep what the loader needs
llvm-objcopy -O binary f.elf f.bina raw image, for flashing
llvm-objcopy --only-keep-debug prog prog.dbgsplit the debug info out
install_name_tool -id @rpath/libx.dylib libx.dylibfix a dylib’s install name
install_name_tool -add_rpath @loader_path/../lib progadd a run-time search path
dsymutil progcollect debug info into prog.dSYM
codesign -f -s - progad-hoc re-sign

The IR, by hand

clang -S -emit-llvm f.c -o -the IR, readable
llvm-as f.ll / llvm-dis f.bctext to bitcode, and back
opt -O2 -S f.ll -o -run the middle end
opt -passes='mem2reg,instcombine' -S f.llnamed passes, your order
llc -O2 f.ll -o -the back end alone: IR to assembly
llvm-bcanalyzer f.bcwhat is in a bitcode file
llvm-link a.bc b.bc -o all.bcmerge bitcode modules

tidy, format & clangd

33

The compilation database

cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ONthe easy path to compile_commands.json
ninja -t compdb > compile_commands.jsonfrom any Ninja build
bear -- makeintercept an arbitrary build
clang -MJ f.o.jsonemit one fragment per file
ln -s build/compile_commands.json .put it at the project root

clang-tidy

clang-tidy f.c -- -std=c17no database: the compile line after --
clang-tidy -p build f.cuse the database in build/
clang-tidy -checks='-*,bugprone-*' f.cstart from nothing, add groups
clang-tidy -list-checks -checks='*'every check there is
clang-tidy -fix f.capply the confident fixes
clang-tidy -export-fixes=f.yamlthe fixes as data, for review
run-clang-tidy -p build -j8the whole project in parallel
.clang-tidyper-project YAML config
// NOLINT(bugprone-x)suppress one check on one line
// NOLINTBEGIN … // NOLINTENDsuppress over a region
bugprone-* cert-* clang-analyzer-*the groups worth enabling in C

The static analyzer

scan-build makewrap any build at all
scan-build -V makeand open the HTML report
clang --analyze f.cone file, from the driver
clang --analyze -Xanalyzer -analyzer-output=text f.cthe path in the terminal
scan-build -enable-checker alpha.security.ArrayBoundthe experimental checkers

clang-format

clang-format -i f.cformat in place
clang-format --style=llvm --dump-config > .clang-formatstart a config from a preset
git clang-formatformat only the lines you changed
clang-format --dry-run --Werror f.ca CI check that changes nothing
// clang-format off / onprotect a hand-aligned block
ColumnLimit / IndentWidth / PointerAlignmentthe three settings arguments are about

clangd

clangd --check=f.cwhy the language server is confused
clangd --query-driver=/usr/bin/arm-none-eabi-gcclearn a cross compiler’s system headers
.clangdper-project config
--background-indexindex the project in the background
--header-insertion=neverstop it adding #includes for you
--clang-tidyrun tidy checks live in the editor

Environment

27

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

macOS

SDKROOTwhich SDK to build against
MACOSX_DEPLOYMENT_TARGETminimum OS version
DYLD_LIBRARY_PATHrun-time search
DYLD_INSERT_LIBRARIESthe LD_PRELOAD equivalent
DYLD_PRINT_LIBRARIES=1trace dylib loading
MallocStackLogging=1record allocation stacks
MallocScribble=1fill freed memory with 0x55
NSUnbufferedIO=YESunbuffered stdio

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