Undefined behaviour · the build: preprocess, compile, link

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

UBexample · the defined alternative
signed overflowINT_MAX + 1 · unsigned wraps (defined)
shift ≥ widthx << 32, 1 << 31, count < 0 · 1u << 31
out of boundsa[5] of int a[5], reads too
null deref*p with p == NULL · check before use
uninitialised readint x; return x; · initialise it
use-after-freefree(p); *p · p = NULL after free
strict aliasing*(uint32_t *)&f · memcpy, union
unsequencedi = i++; a[i] = i++; · split them
data race2 threads, 1 writes, no sync · _Atomic, mutex
literal writechar *s = "hi"; s[0]='H'; · char s[]
div by zerox / 0, INT_MIN / -1 · (float: inf/NaN)
misalignedcast 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, -O2 breaks → 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

Undefined behaviour · the build: preprocess, compile, link — figure 1

Linkage and the one-definition rule

  • External (default for functions/globals): one name across TUs. Internal: static at file scope = private to the .c. None: locals. (static in a function = storage, not linkage.)
  • Header declares (extern int count;), exactly one .c defines (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-components show 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++) evaluates i++ twice (UB); do{}while(0) makes if (c) SWAP(x,y); else parse; #if FOO with FOO undefined is silently 0. #x stringifies, a##b pastes.

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”.
  • -O0 debug · -Og ESP-IDF default · -Os size · -O2 speed.

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

  1. Undefined reference vs multiple definition? — never defined vs defined twice.
  2. static on a global? — internal linkage: invisible to other .c files.
  3. Why memcpy for type punning? — aliasing-safe; compiles to one load.