; ============================================================
; CROSS-LEVEL LESSON 3: CRASH SITE vs ROOT CAUSE — trace backward.
; ------------------------------------------------------------
; The instruction that SIGSEGVs is innocent. The real defect is upstream.
; Here a valid 64-bit pointer is destroyed long before it is used:
;
; ROOT CAUSE (here): %chopped = trunc i64 %addr to i32 ; keep only low 32 bits
; store i32 %chopped, ptr %slot ; write 4 bytes into slot
; ...
; CRASH SITE (here): store i32 456, ptr %wild ; deref the wrecked pointer
;
; %slot is a 4-byte (i32) alloca, but we reload it with `load ptr` -- an 8-BYTE
; load. That reads the 4 bytes we wrote PLUS 4 bytes of ADJACENT stack, so the
; high half of the rebuilt pointer is garbage. The pointer is wild -> the final
; store faults (SIGSEGV, deterministically on this box).
;
; To debug this you START at the faulting store and walk the SSA def-use chain
; BACKWARD (every value names the instruction that produced it):
;
; store i32 456, ptr %wild <- crash site; who made %wild?
; %wild = load ptr, ptr %slot <- 8-byte load from a 4-byte slot!
; store i32 %chopped, ptr %slot <- only 4 bytes were ever written
; %chopped = trunc i64 %addr to i32 <- *** ROOT CAUSE ***
; %addr = ptrtoint ptr %target to i64
;
; TWO things this proves:
; * `opt -passes=verify` calls this VALID. Opaque `ptr` carries no width, so
; "trunc i64 to i32" then "load i64" is structurally fine. Verified != correct.
; * `valgrind --track-origins=yes` (on the native exe) reports
; "Use of uninitialised value of size 8" and traces the origin back to the
; stack slot whose high 4 bytes we never wrote — i.e. straight to the trunc.
;
; Build & run (from cross_level/, local LLVM on PATH):
; opt -passes=verify truncptr_rootcause.ll -disable-output && echo VALID
; llc -filetype=obj truncptr_rootcause.ll -o tr.o && clang tr.o -o truncptr_ir
; ./truncptr_ir ; echo exit=$?
; (REAL output for every step is in cross_level/README.md.)
; ============================================================
target triple = "x86_64-unknown-linux-gnu"
define i32 @main() {
entry:
; a real 4-byte object we intend to write to
%target = alloca i32, align 4
store i32 123, ptr %target, align 4
; a 4-byte (i32) slot -- too small to hold a 64-bit pointer
%slot = alloca i32, align 4
; address-of target as a 64-bit integer
%addr = ptrtoint ptr %target to i64
; *** ROOT CAUSE: keep only the low 32 bits of the address ***
%chopped = trunc i64 %addr to i32
; write those 4 bytes into the 4-byte slot (well-typed; semantically lossy)
store i32 %chopped, ptr %slot, align 4
; reload as an 8-BYTE pointer from the 4-byte slot: the high 4 bytes come from
; ADJACENT stack -> uninitialised garbage
%wild = load ptr, ptr %slot, align 8
; CRASH SITE: store through the wrecked pointer -> SIGSEGV
store i32 456, ptr %wild, align 4
ret i32 0
}A lab is done when you can say what it showed on your machine — not when you have read it.
It decides which problems come first, which drills open, which labs you see and how the Learn surface is organised. Change it any time from the switch in the top bar, or press Escape to look around first.