c · memo
In one line: UB = the standard places no requirements on the
result — and the optimiser assumes it never happens, so it deletes
checks and rewrites code around it; the bug shows up far away, often only at
-O2. Building = per translation unit preprocess
→ compile → assemble to a .o, then link every
.o + library into one image with real addresses.
Download PDF Print view LaTeX source
The UB catalogue
| UB | example · the defined alternative |
|---|---|
| signed overflow | INT_MAX + 1 · unsigned wraps (defined) |
| shift ≥ width | x << 32, 1 << 31, count < 0 · 1u << 31 |
| out of bounds | a[5] of int a[5], reads too |
| null deref | *p with p == NULL · check before use |
| uninitialised read | int x; return x; · initialise it |
| use-after-free | free(p); *p · p = NULL after free |
| strict aliasing | *(uint32_t *)&f · memcpy, union |
| unsequenced | i = i++; a[i] = i++; · split them |
| data race | 2 threads, 1 writes, no sync · _Atomic, mutex |
| literal write | char *s = "hi"; s[0]='H'; · char s[] |
| div by zero | x / 0, INT_MIN / -1 · (float: inf/NaN) |
| misaligned | cast to uint32_t * of buf+1 · memcpy |
Why the optimiser “breaks” it
int read_dev(struct dev *d) {
int v = d->val; // deref => compiler infers d != NULL
if (!d) return -1; // ...so this check is DELETED
return v;
}
bool will_wrap(int x) { return x + 1 < x; } // folded: false
- Not “a garbage value”: a promise you made. Debug (
-O0) works,-O2breaks → latent UB. Remove the UB; never “fix” it by lowering-O. - Tests can’t prove absence: a path may look right today.
Catch it
-Wall -Wextra -Werror(+-Wshadow -Wconversion).-fsanitize=address: OOB, use-after-free/return, double free, leaks.-fsanitize=undefined: overflow, shifts, null, misaligned.-fsanitize=thread: races (not with ASan). Uninitialised reads: clang MSan / Valgrind. Run on host unit tests.- On the ESP32: heap poisoning, stack-smashing protection, FreeRTOS stack canaries (menuconfig);
cppcheck,clang --analyze.
Static vs dynamic libraries
.a = archive of .o; only needed objects are copied in at link; order matters (gcc main.o -lfoo). .so/.dylib = loaded at run time, shared, updatable, needs PIC. MCU firmware is all static (each ESP-IDF component is a .a).
Picture — the pipeline
Linkage and the one-definition rule
- External (default for functions/globals): one name across TUs. Internal:
staticat file scope = private to the.c. None: locals. (staticin a function = storage, not linkage.) - Header declares (
extern int count;), exactly one.cdefines (int count = 0;).int count;in a header → multiple definition (GCC ≥ 10:-fno-common). Declared, never defined → undefined reference. - Linker: resolves symbols, relocates to final addresses, places sections by the linker script (
.text/.data/.bss/IRAM → flash/RAM), drops dead code (-ffunction-sections -fdata-sections -Wl,--gc-sections). __attribute__((weak)): default, overridden by any strong definition.-Wl,-Map=app.map,size,nm,idf.py size-componentsshow who uses the flash.
Headers and macros
#ifndef UART_H /* or: #pragma once */
#define UART_H
#define SQ(x) ((x) * (x)) // parens: arg AND body
#define SWAP(a,b) do{ int t_=(a); (a)=(b); (b)=t_; }while(0)
static inline int sq(int x) { return x * x; } // prefer
#endif
SQ(i++)evaluatesi++twice (UB);do{}while(0)makesif (c) SWAP(x,y); elseparse;#if FOOwithFOOundefined is silently 0.#xstringifies,a##bpastes.
Interview traps
- Your Q22: UB is not “may be 0 or -1” — no requirements; name 3 kinds.
- Your Q23: the linker’s jobs: symbols, addresses, linker script,
--gc-sections— not vague “optimisations”. -O0debug ·-OgESP-IDF default ·-Ossize ·-O2speed.
Remember
UB is a promise you made to the optimiser. cpp → cc → as per TU,
then ld once. Declare in the .h, define once in a .c.
Likely questions
- Undefined reference vs multiple definition? — never defined vs defined twice.
staticon a global? — internal linkage: invisible to other.cfiles.- Why
memcpyfor type punning? — aliasing-safe; compiles to one load.