5.3.13 · D1Build Systems & Toolchain

Foundations — Undefined Behavior Sanitizer (UBSan)

2,299 words10 min readBack to topic

Before you can understand the parent note, you need to own every symbol it throws at you. We build each one from nothing — plain words first, then a picture, then why the topic needs it.


1. A bit, and a fixed-width box

An int is not an infinite number line. It is a fixed row of 32 bits — a box with exactly 32 slots. Because the box has a fixed size, there is a largest number it can hold and a smallest one. This single fact — the box is finite — is the root of almost every bug UBSan catches.

Figure 1 below draws that box: 32 numbered slots, with the leftmost slot (bit 31) picked out in red because — as Section 3 shows — it behaves specially.

Figure — Undefined Behavior Sanitizer (UBSan)

Why the topic needs this: every "overflow" the parent note talks about is just "the answer needed more slots than the box has." If you don't picture the box in Figure 1, overflow sounds mystical. It isn't.


2. Reading the switches: powers of two

To turn a row of bits into a number, each slot has a weight. The rightmost slot is worth , the next , then , , and so on — each slot doubles the one to its right. We write these weights as

To read a number, add up the weights of every slot that is on (1).

Why the topic needs this: the parent writes and everywhere. Those aren't scary — they're just "the biggest thing a 31-slot pile of weights can add up to."


3. The largest and smallest int: INT_MAX and INT_MIN

Here is the twist that makes signed numbers work. In a 32-bit int, the top (leftmost) bit does not add — it subtracts. Its weight is instead of . This is called Two's Complement Representation, and it is why one bit can encode negative numbers.

Figure 2 shows all the weights in a row so you can see that only the top box is negative while every other box adds.

Figure — Undefined Behavior Sanitizer (UBSan)
  • If the top bit is 0: the number is built only from positive weights, up to
  • If the top bit is 1: you start at and add positive weights back, so the smallest possible is

The comparison and membership symbols <, >, , , : these compare numbers or ask "is it inside a range?".

  • means " is to the left of on the number line" (a is less than b).
  • means " is to the right of " (a is greater than b) — the mirror image of .
  • means " is left of, or equal to, " (less than or equal).
  • means " is right of, or equal to, " (greater than or equal) — the mirror image of .
  • means " is a member of that range" — the square bracket includes its endpoint, the round bracket excludes it. So is up to , not .

4. The shift operator <<, and why n << 32 is out of range

The standard demands the shift amount k be — that is, . There are only 32 slots, so shifting by 32 would push the bit clean out of the box, which has no defined meaning. That round bracket in excluding 32 is exactly why n << 32 in the parent is illegal — it asks for a slide the box cannot represent.


5. What "wraparound" looks like — the odometer

Add to the biggest number and the box has no slot for the answer, so all the on-bits roll to and the top bit flips on — turning a huge positive into a huge negative. Just like a car odometer rolling from 99999 back to 00000.

Figure 3 draws this as a curved red arrow leaping from just past INT_MAX all the way back to INT_MIN on the number line — the visual of "one step past the edge becomes the far negative side."

Figure — Undefined Behavior Sanitizer (UBSan)

For unsigned integers this rollover is defined by the standard — see Integer Overflow & Wraparound — it simply counts modulo . For signed integers the standard refuses to define it: that is the UB the whole topic is about.


6. The sign bit and the symbol

The parent uses . It just means "the top bit of " — for non-negative, for negative. Reading the top bit tells you a number's sign in one glance.

The symbols (and) and (or) and (not equal) are plain logic words:

  • — both true.
  • — at least one true.
  • is different from .

So the parent's overflow rule reads in English: "if a and b had the same sign, but the answer's sign came out different, we wrapped." No calculus needed — just watching one bit.

Why the topic needs this: this single-bit test is the cheap check UBSan inserts. Understanding "top bit = sign" is the entire trick.


7. Alignment, the alignof operator, and the % symbol

Example 3 of the parent uses addr % alignof(int) == 0. The symbol % is remainder after division: because . If the remainder is , the address sits exactly on a -slot boundary — that is what "aligned" means. So addr % alignof(int) == 0 reads "is the address a clean multiple of 4?" The odd address 1 fails because .


8. Compile time vs run time

  • Compile time = when the compiler builds your program (before it runs). Here UBSan inserts its checks. This is why UBSan is a form of instrumentation added at compile time.
  • Run time = when the finished program actually executes on data. Here the inserted check fires and calls libubsan.

This split is the difference between a static analyzer (reasons about code without running it) and UBSan (a dynamic tool that only sees the paths you actually run). It also connects to Compiler Optimization Levels (-O0 vs -O2 change how much the compiler assumes UB never happens) and to Debugging with -g and Debug Info (-g is what lets the whistle point at a line number).


How the foundations feed the topic

Bit = one 0 or 1 switch

Fixed 32-slot box

Powers of two 2^n as weights

Twos complement top bit subtracts

Range INT_MIN to INT_MAX

Wraparound odometer rollover

Sign bit and sgnbit

Shift operator slides bits

Shift by 32 out of range UB

Signed overflow is UB

Remainder percent and alignof

Pointer alignment UB

Compile time vs run time

UBSan inserts checks then fires

Undefined Behavior Sanitizer

Related tooling you will meet later: AddressSanitizer (ASan) for memory bugs, and the GCC and Clang Toolchains that host -fsanitize=undefined.


Equipment checklist

Cover the right side and answer each. If you can, you're ready for the parent note.

What is a bit?
A single switch that is either 0 (off) or 1 (on).
What does mean?
Multiply 2 by itself times; counts the doublings.
Why does a 32-bit int have a limited range?
It has only 32 slots, so it can hold at most distinct patterns.
What are INT_MAX and INT_MIN for 32 bits?
and .
Why does turning on all 31 positive slots give ?
Each weight is one more than the whole stack below it, so the total is always one short of the next power of two.
Why is there one more negative than positive?
The value 0 uses one of the non-negative patterns, so the negatives get one extra.
What is special about the top bit in two's complement?
Its weight is negative (), so it encodes the sign.
What does mean?
The top bit of : 0 if non-negative, 1 if negative.
What does the operator n << k do?
Slides every bit of n left by k slots, equal to .
Why is n << 32 illegal for a 32-bit int?
The shift amount must be in , i.e. 0 to 31; 32 pushes the bit out of the box.
What does alignof(int) return and mean?
4 — an int must start at an address that is a multiple of 4.
What do , , , , mean?
less than, greater than, less-or-equal, greater-or-equal, not equal.
What does include?
0 through 31 — the square bracket includes 0, the round bracket excludes 32.
What does % compute, and what is ?
Remainder after division; .
What does "aligned" mean for an int?
Its address is a multiple of 4, i.e. addr % 4 == 0.
Difference between compile time and run time?
Compile time builds the program (UBSan inserts checks); run time executes it (the checks fire).
Is signed overflow UB? Is unsigned?
Signed = UB; unsigned = defined, wraps mod .