5.3.11 · D4Build Systems & Toolchain

Exercises — GCC - Clang flags — optimization (-O0 to -O3, -Os), warnings (-Wall, -Wextra), sanitizers

2,395 words11 min readBack to topic

Level 1 — Recognition

Exercise 1.1

Match each flag to the one thing it primarily does: -O0, -O2, -Os, -Wall, -Werror, -fsanitize=address.

Recall Solution
  • -O0 — turn off optimization; every variable stays in memory so a debugger can read it. Default level.
  • -O2 — the production speed default: inlining, loop optimizations, register allocation, without size-blind tricks.
  • -Os — optimize for binary size (like -O2 minus passes that bloat code).
  • -Wall — enable the common "almost-always-a-bug" compile-time warnings (static, no run needed).
  • -Werror — turn every warning into a hard error so the build fails.
  • -fsanitize=address — inject runtime checks that catch buffer overflows / use-after-free.

The split to notice: -O* change how fast/small, -W* are compile-time hints, -fsanitize are runtime detectors.

Exercise 1.2

True or false: -Wall enables all the compiler's warnings.

Recall Solution

False. Despite the name, -Wall is a curated common set. Warnings like unused-parameter and sign-compare need -Wextra; shadowing needs -Wshadow; narrowing conversions need -Wconversion. Serious builds use -Wall -Wextra at minimum.

Exercise 1.3

You run gcc prog.c -o prog with no -O flag. Which optimization level is active?

Recall Solution

-O0 — the default. No optimization, easiest to debug, slowest binary.


Level 2 — Application

Exercise 2.1

You want a build for debugging in GDB that also catches memory bugs and undefined behaviour, but stays fast enough to run tests. Write the flag set and justify each flag. (See Debugging with GDB and -g.)

Recall Solution

  • -Og — "optimize for debugging": faster than -O0, but keeps line-to-code mapping sane.
  • -g — emit debug info so GDB knows variable names / line numbers.
  • -Wall -Wextra — free static bug-finding at compile time.
  • -fsanitize=address,undefined — ASan + UBSan runtime detectors.

You must pass -fsanitize=... at both compile and link steps (ASan needs its runtime library).

Exercise 2.2

This code compiles clean with gcc prog.c. Add flags so the bug below is reported at compile time, then state the fix.

unsigned n = 5;
if (n - 10 < 0) puts("negative");   // never prints
Recall Solution

Add -Wall -Wextra (specifically -Wtype-limits/-Wsign-compare fire here). Why it's a bug: n is unsigned, so n - 10 computes in unsigned arithmetic. wraps modulo to , which is never < 0. So the branch is dead code. Check: . Fix: compare in signed space, e.g. if ((int)n - 10 < 0).

Exercise 2.3

Write a release build for a program where binary size matters most (embedded flash chip). Then write the plain speed-release build. What single macro do both share, and why?

Recall Solution
  • Size-critical:
  • Speed release:

Both define NDEBUG, which makes the standard assert() macro expand to nothing — removing assertion checks (and their code/strings) from the shipped binary. -Os differs from -O2 only by disabling size-growing passes like aggressive inlining/unrolling.


Level 3 — Analysis

Exercise 3.1

A colleague says: "My program prints the right answer at -O0 but garbage at -O2 — the optimizer is buggy." Give the most likely real cause and the exact command to confirm it.

Recall Solution

The overwhelmingly likely cause is undefined behaviour in the program (uninitialized read, out-of-bounds access, or signed overflow), not a compiler bug. The optimizer is permitted to assume UB never happens; at -O0 the UB happened to produce a "correct-looking" result, at -O2 an optimization exploited the assumption differently. Confirm with:

clang -O1 -g -fsanitize=address,undefined prog.c -o prog && ./prog

It pinpoints the offending line. (Use -O1 so the report stays readable but the UB isn't optimized away.)

Exercise 3.2

Why is the recommended sanitizer level -O1, and specifically not -O0 and not -O3? Give one concrete failure mode for each extreme.

Recall Solution
  • Not -O0: it's the slowest; sanitized -O0 builds can be painfully slow on big test suites, and stack layouts are noisy — but the deeper reason is speed.
  • Not -O3: aggressive optimization can delete the very UB you want to catch. Example: a signed overflow that -O3 folds into a constant, or a dead uninitialized read that gets eliminated — the sanitizer never sees it fire.
  • -O1 is the sweet spot: fast enough, keeps line info, but light enough that UB usually still executes and gets caught.

Exercise 3.3

Consider the reduction loop for(int i=0;i<n;i++) s+=a[i];. Explain, in terms of the "as-if" rule, why -O2 is allowed to (a) keep s in a register, and (b) add 8 elements at once with SIMD (see SIMD and Auto-Vectorization) — yet is not allowed to change the final value of s for a well-defined program.

Recall Solution

The as-if rule says the compiler may emit any machine code whose observable behaviour matches the source for a correct (UB-free) program. Observable = I/O, volatile accesses, program termination — not whether a value momentarily sits in memory vs a register.

  • (a) Register-keeping changes where s lives, not its final value ⇒ legal. It only removes load/store traffic (see CPU Caches and Instruction Cache).
  • (b) For integer addition, associativity holds exactly, so summing lanes and combining gives the identical total ⇒ legal.
  • What's forbidden: changing the actual number computed. (For floating point, reassociation is not exact, which is why -ffast-math / -Ofast — which permit it — can change results and is dangerous.)

Level 4 — Synthesis

Exercise 4.1

Design the flag strategy for a project with three build profiles wired into a Makefile and a CI pipeline (see Makefiles and CFLAGS/LDFLAGS, Continuous Integration pipelines): dev, ci, release. Specify flags for each and one sentence of intent per profile.

Recall Solution
  • dev : fast edit-run loop, debuggable, catches obvious mistakes.
  • ci : zero-warning gate (-Werror fails the build) plus runtime UB/memory detection on every push; -O1 keeps sanitizer reports meaningful and fast.
  • release : production speed default, assertions stripped.

Key design point: -Werror belongs in CI, not dev (a dev shouldn't be blocked by a harmless warning mid-refactor, but nothing warning-y should ever merge).

Exercise 4.2

Explain why you would not ship -fsanitize=address in the release binary, quantifying the cost, and why -O3 is not a safe default upgrade over -O2 for release.

Recall Solution
  • ASan in release: ASan uses shadow memory (1 shadow byte per 8 app bytes) and instruments every load/store. Cost is roughly ~2× slower and ~3× memory. That's fine in tests, unacceptable in production — and it changes the memory layout your users would run.
  • -O3 not a safe default: each optimization is a bet. -O3's aggressive inlining/unrolling grows code; if it overflows the instruction cache the program runs slower despite "more" optimization. So -O3 beats -O2 only when benchmarks prove the bets pay off. Default to -O2; switch only with measured evidence.

Level 5 — Mastery

Exercise 5.1

A heap overflow: int *p = malloc(4*sizeof(int)); p[4] = 1;. Explain step by step how ASan turns this silent corruption into a precise report, and name the memory region p[4] lands in.

Recall Solution
  1. malloc(4*sizeof(int)) gives 16 valid bytes: indices p[0..3].
  2. ASan surrounds the allocation with poisoned redzones — bytes marked "not addressable" in shadow memory.
  3. The store p[4] = 1 is instrumented: before writing, ASan computes the shadow byte for that address and checks it.
  4. p[4] is the first int past the 16 valid bytes — it lands in the redzone (reported as "0 bytes after" the block).
  5. The shadow byte says "poisoned" ⇒ ASan aborts with heap-buffer-overflow, a stack trace, and the exact source line.

Byte offset of p[4] from the block start: bytes = exactly one past the last valid byte (15).

Exercise 5.2

Degenerate/edge cases. For the loop for(int i=0;i<n;i++) s+=a[i];, describe what a correct compiler must do when: (a) n == 0, (b) n is negative (say int n = -1), (c) the running sum overflows int. Which of these is UB?

Recall Solution
  • (a) n == 0: the loop condition 0 < 0 is false immediately; body runs zero times, s stays 0. Fully defined. Any vectorized version must also short-circuit to leave s unchanged.
  • (b) n == -1: with signed i and condition i < n, 0 < -1 is false ⇒ zero iterations, s == 0. Defined (no access happens). But if the loop were written i <= n with unsigned counters, a negative n converts to a huge unsigned value ⇒ massive out-of-bounds reads = UB.
  • (c) signed int sum overflow: signed integer overflow is undefined behaviour. This is the dangerous one: the optimizer may assume it never happens (e.g. that s only grows), so relying on wraparound is a bug. Detect with -fsanitize=undefined; fix with a wider/unsigned accumulator.

So: (a) and (b, signed) are fine; the unsigned-n variant and (c) are UB.

Exercise 5.3

You must pick one sanitizer to run in CI but only have budget for one. A program does heavy multithreading and pointer arithmetic. Justify choosing between ASan, UBSan, and TSan — and explain why you can't just enable all three at once.

Recall Solution
  • TSan targets data races (the multithreading risk). ASan targets spatial/temporal memory bugs (the pointer-arithmetic risk). UBSan catches arithmetic/pointer UB.
  • If forced to pick one: for a multithreaded program the races are the highest-severity, hardest-to-reproduce class — choose TSan. If the code were single-threaded and pointer-heavy, choose ASan.
  • Why not all three: ASan and TSan are incompatible — both remap memory / instrument accesses in conflicting ways and cannot be combined in one binary. UBSan can combine with either. Practical answer: run two CI jobs (ASan+UBSan build, and a separate TSan build), not one.

Recall Self-check summary (cloze)

Q :::

  • The production-default optimization level is -O2.
  • Signed integer overflow is undefined behaviour.
  • -fsanitize=address must be passed at both compile and link steps.
  • ASan and TSan cannot be combined in one binary.
  • Sanitizers are best run at -O1 (readable reports, UB not optimized away).