Undefined Behavior
Undefined behavior (UB) is C’s most dangerous feature. The compiler can do literally anything when UB occurs — including appearing to work perfectly for years until a compiler upgrade, optimization flag change, or different input triggers a spectacular failure. The insidious part: UB does not mean “your program crashes.” It means the C standard places no requirements whatsoever on what happens. Your program might print the right answer, format your hard drive, send your browsing history to your boss, or (most commonly) work fine in debug builds and break silently in release builds. The only guarantee is that there are no guarantees.Why Does C Have Undefined Behavior?
The Design Philosophy
Before looking at the dangers, understand why C was designed this way: The goal: Performance and portability above all else. Why UB exists:-
Performance: Checking for errors adds overhead.
- Example: Checking array bounds on every access makes code 10-30% slower.
- C’s philosophy: “Trust the programmer” - if you say
arr[100], C assumes you know what you’re doing.
-
Portability: Different hardware behaves differently.
- Example: What happens when you right-shift a negative number?
- x86: Arithmetic shift (preserves sign)
- Some ARM/PowerPC: Logical shift (fills with zeros)
- C’s solution: Make it “Implementation Defined” or “Undefined” so compilers can use the fastest native instruction.
-
Optimization: The compiler assumes UB never happens.
- Example: If you write
x + 1, the compiler assumes no overflow. - This allows it to optimize loops and algebraic simplifications that wouldn’t be valid if it had to handle overflow wrapping.
- Example: If you write
What is Undefined Behavior?
The C standard defines three categories of problematic code:Signed Integer Overflow
Null Pointer Dereference
Buffer Overflows
Use After Free
Uninitialized Variables
Strict Aliasing Violations
Sequence Point Violations
Shift Operators
Pointer Arithmetic Violations
Data Races
Division and Modulo
How Compilers Exploit UB
This is where undefined behavior goes from “academic concern” to “production nightmare.” Modern optimizing compilers do not just ignore UB — they actively exploit the assumption that UB never happens to generate faster code. When your code does trigger UB, the optimizer’s assumptions become wrong, and the generated code does something completely unrelated to what you wrote. Think of it like a contract: you promised the compiler your code would never have UB, and in exchange the compiler promised to make your code fast. When you break your side of the contract, the compiler’s optimizations — which were valid under the contract — produce nonsensical results.Tools for Finding UB
Compiler Sanitizers
Valgrind
Static Analysis
Defensive Coding Practices
These are not optional “nice to have” habits — they are the minimum viable discipline for writing C code that works across compilers, optimization levels, and platforms. Every item on this list prevents a real class of bugs that has caused real security vulnerabilities in real production systems.Exercises
1
UB Hunter
Find all undefined behavior in a provided code sample (at least 5 instances).
2
Safe Integer Library
Implement safe add, subtract, multiply, divide functions that detect overflow.
3
Sanitizer Setup
Set up a project with all sanitizers enabled in debug mode and run your data structures code through them.
4
Aliasing Exploration
Write code that behaves differently with -O0 vs -O2 due to strict aliasing, then fix it.
Next Up
System Calls & POSIX
Interface with the operating system