2026-07-12
When your program calls write(fd, buf, 1024), the kernel eventually does something like copy_from_user(kbuf, buf, 1024). But buf is a user-space pointer — the caller could pass 0xdeadbeef, a freed page, or a pointer into an unmapped region. In user space, dereferencing that address raises SIGSEGV. In the kernel, the same fault would be a catastrophic oops. So how does copy_from_user return -EFAULT instead of panicking?
The answer is the exception table (also called the fault fixup table): a static array, built at link time, that maps "faulting instruction address" → "recovery instruction address."
The core copy loop on x86-64 looks roughly like:
1: mov (%rsi), %rax — the load that might faultmov %rax, (%rdi) — store to kernel buffer2: ... — success path3: mov $-EFAULT, %rax; ret — fixup pathThe assembler emits a .fixup/__ex_table entry: { fault_addr: 1b, fixup_addr: 3b }. The linker collects all these into a sorted table between __start___ex_table and __stop___ex_table.
When the CPU takes a page fault at instruction 1b, the kernel's do_page_fault handler checks: was this fault in kernel mode? If yes, it binary-searches the exception table for the faulting RIP. If found, it rewrites the saved RIP on the fault frame to point at 3b, then irets. The CPU "returns" to the fixup code, which sets -EFAULT and unwinds. No oops, no panic — just a clean error return.
Real-world example: This is how /proc/self/mem can safely probe address validity, how readv partial reads work when only some iovecs are valid, and how eBPF's bpf_probe_read reads arbitrary kernel/user pointers without crashing the tracer. Every get_user(), put_user(), and strncpy_from_user() relies on this mechanism.
Rule of thumb: The exception table adds ~16 bytes per fixup site (fault addr + fixup addr, often as 32-bit relative offsets = 8 bytes plus a type). A modern kernel has roughly 10,000–30,000 entries, so ~200 KB of the kernel image is exception table. Lookup is O(log n) — about 14 comparisons for 16K entries — and happens only on fault, so the fast path pays zero cost.
The clever part: this turns page-fault handling into a userland-style try/catch for the kernel, without any language runtime, stack unwinding, or per-call setup.
-EFAULT returns with zero fast-path overhead.
