06 — verifyModule and Debugging JIT Crashes
A JIT failure mode looks like this: lookup("main") returns a function
pointer. You call it. The process crashes with SIGBUS, SIGSEGV, or — if
you're unlucky — silently produces wrong output. There is no stack trace
pointing back at the IR that did it. Debugging requires reproducing the bug
in tools designed for native code (lldb, instruments) against memory pages
that didn't exist a moment ago.
The single best defense is to verify before you JIT. cp-17 does this
at the end of emitModule:
std::string verr;
raw_string_ostream os(verr);
if (verifyModule(*r.module, &os)) {
r.error = "verifyModule failed:\n" + os.str();
r.module.reset();
}
verifyModule catches:
- Basic blocks without terminators (the #1 emitter bug).
- Multiple terminators in a basic block.
- Branches to blocks in the wrong function.
- Type mismatches in operands (e.g., passing
i32wherei64is expected). - PHI nodes with the wrong number of incoming values.
- Use-before-def on
Values outside their dominator scope. - Function signature mismatches between caller and callee.
ret voidfrom ani64-returning function (and vice versa).
Every one of those will at best cause a JIT crash and at worst quietly miscompile. Catch them at IR emission and you save days.
Reading verifier output
The verifier names the broken instruction and prints surrounding context:
Terminator found in the middle of a basic block!
label %then
When you see this, the cause is almost always:
- You emitted a
retorbr, then forgot to switch insertion point and emitted more instructions into the same now-closed block, or - You forgot to check
GetInsertBlock()->getTerminator()before appending a fall-through branch after anif/whilebody that already returned.
The cure in ir_emit.cpp is the if (!b.GetInsertBlock()->getTerminator())
guard before any CreateBr/CreateRet.
When the verifier passes but the JIT still crashes
Common remaining causes:
- Wrong calling convention between a
declared extern and the host function (e.g., declaringvoid f(i64)but the C function takesi32). The verifier can't catch this — both sides look internally consistent. Treatment: keep signatures in one place (a header) and reference them from both the emitter and the runtime. - Mutated
Moduleafter JIT addModule. ORC takes ownership; subsequent edits via the stale pointer are undefined. - Forgetting
InitializeNativeTarget()—LLJITBuilder().create()will return an error like "no available targets are compatible with this triple".jit.cppcalls these once viastd::call_once.
Dumping IR while debugging
mod.print(errs(), nullptr) will dump the module as text to stderr. Add it
right before the verifyModule call and you can copy-paste into llc or
opt to reproduce a bug outside the JIT. The text and the in-memory form
round-trip exactly, so behaviour is identical.