User Mode vs Kernel Mode
The CPU runs in one of two privilege levels; user mode cannot touch devices, page tables or other processes, and the only ways into kernel mode are a trap, an interrupt or an exception — which is why a segfault is a fault delivered to you as a signal, not a crash of the machine.
The problem
Two modes, one CPU
The CPU keeps a current privilege level in a control register. In the lower privilege (user mode, ring 3 on x86, EL0 on ARM) a set of instructions is forbidden — loading page-table registers, disabling interrupts, talking to I/O ports, halting the core — and every memory access is checked against page-table permission bits that mark kernel pages as supervisor-only. In the higher privilege (kernel mode, ring 0, EL1) all of it is allowed. x86 defines four rings but every mainstream OS uses two; virtualisation adds a level below ring 0 for the hypervisor.
Everything that runs in user mode is "the application" from the kernel’s point of view: your code, the language runtime (V8, CPython, the JVM), and the libraries it links, including libc. Everything that runs in kernel mode is the operating system proper: the scheduler, the memory manager, the file systems, the network stack, device drivers. The boundary between them is the trap described in System Calls — and the only difference between user and kernel code is which side of that boundary it executes on, not what language it is written in.
Switching is asymmetric. Going *up* into kernel mode is only possible through a small number of hardware-defined doors, each of which lands at an address the kernel chose at boot. Coming *down* is a single instruction (sysret/iret/eret) that the kernel executes when it is done. User code cannot jump into the kernel at an address of its choosing, which is what makes the boundary a security boundary and not a convention.
| User mode (ring 3 / EL0) | Kernel mode (ring 0 / EL1) |
|---|---|
| Application code | Scheduler and run queues |
| Language runtime: V8, CPython, JVM, Go runtime | Memory manager: page tables, page cache, reclaim |
| Libraries incl. libc, OpenSSL, libuv | File systems and the VFS |
| Shells, daemons, databases, browsers | TCP/IP stack, socket buffers |
| User-space drivers (DPDK, FUSE handlers, Windows UMDF) | Device drivers, interrupt handlers |
| Cannot: touch devices, page tables, other processes | Can: everything, including crash the machine |
The only three doors in
There are exactly three ways the CPU leaves user mode, and they share one mechanism: the CPU saves where it was, switches to the kernel stack, raises privilege and jumps to an entry the kernel registered in a table (the IDT on x86, the exception vector table on ARM). A trap is deliberate — the syscall/svc instruction, executed by the program to request a service. An interrupt is external and asynchronous — a timer, a NIC with a packet, a keyboard, a disk completion — and arrives regardless of what the program was doing. An exception (or fault) is synchronous and unintended — a division by zero, an access to an unmapped page, a privileged instruction in user mode.
All three land in kernel mode with the user state saved, so all three are potential scheduling points: after handling a timer interrupt the kernel may switch tasks (Context Switching); after a page fault it may put the task to sleep while a page loads (Page Faults); after a syscall that blocks it certainly does. The user program sees none of it; from its perspective, one instruction simply took longer than usual.
The kernel side of an interrupt is split in two on most systems: a short top half that acknowledges the device and queues work, run with further interrupts masked, and a bottom half (softirq, tasklet, threaded IRQ on Linux; DPC on Windows) that does the real work later with interrupts enabled. A NIC receiving 1 M packets/s cannot be handled in the top half; the network stack runs in bottom halves, and under heavy load in a dedicated ksoftirqd thread that shows up in top as system time no user process asked for.
What a segfault actually is
Dereference a null pointer in C++ and the CPU tries to read address 0. The page table has no valid entry for that page, so the memory unit raises a page fault exception; the CPU saves the faulting address (in CR2 on x86, FAR_EL1 on ARM) and enters the kernel’s page-fault handler. The handler asks: is this address inside any mapping of the process? Could it be lazily allocated, copy-on-write, or swapped out? (See Page Faults.) If yes, it fixes the page table and returns, and the instruction re-executes. If no — address 0 is in no mapping — the access is invalid, and the kernel has to tell the process.
It tells it with a signal: SIGSEGV, with the faulting address attached. Signals are the kernel’s mechanism for delivering asynchronous events to a process (Signals: Asynchronous Notifications From the Kernel); the default action for SIGSEGV is to terminate the process and write a core dump, which is where "Segmentation fault (core dumped)" comes from. A process may install a handler instead, and some runtimes do: the JVM and Go catch SIGSEGV to turn null dereferences into a NullPointerException or a panic with a stack trace, and garbage collectors use deliberately protected pages plus a SIGSEGV handler as write barriers.
So a segfault is not the machine failing and not the kernel panicking; it is the kernel refusing an access on the process’s behalf and reporting it. A bus error (SIGBUS) is the same story for a different reason — an access that is mapped but cannot be satisfied, such as a misaligned access on strict architectures or a page of a memory-mapped file that has been truncated away. And SIGILL is the exception raised when user code executes a privileged or undefined instruction — the CPU enforcing the privilege boundary at the instruction level.
1#include <cstdio>2int main() {3 int* p = nullptr;4 std::printf("about to fault\n");5 return *p; // load from VA 0 → #PF → kernel: no mapping → SIGSEGV → "Segmentation fault (core dumped)", exit status 1396}Monolithic, micro, hybrid — and the Windows note
How much runs in kernel mode is a design choice. A monolithic kernel puts the scheduler, memory manager, file systems, network stack and drivers in one privileged address space: fast (a file read is one trap and function calls thereafter) and fragile (a driver bug can corrupt anything). Linux and the BSDs are monolithic, with loadable modules that still run at ring 0. A microkernel keeps only scheduling, address spaces and message passing in kernel mode and runs file systems, drivers and network stacks as user-mode servers: robust (a crashed driver is restarted) and historically slower, because each service is a message and two mode switches away. seL4, QNX (in every car’s infotainment system) and MINIX (inside Intel’s Management Engine) are microkernels; Fuchsia’s Zircon is close to one.
Most production kernels are hybrid. Windows NT has a microkernel-shaped core (the executive, HAL, kernel) but runs graphics, file systems and most drivers in kernel mode for speed; macOS’s XNU fuses the Mach microkernel with a BSD kernel in one address space. On Linux, user-space drivers are the exception (FUSE file systems, DPDK network drivers, io_uring-based storage engines) and exist precisely where the trap cost or the crash isolation matters more than the default.
Windows exposes the boundary differently. Applications call the Win32 API (kernel32.dll, user32.dll), which calls ntdll.dll, which contains the actual syscall stubs into ntoskrnl.exe. The syscall numbers change between Windows versions and are deliberately undocumented; the stable contract is the DLL layer. Windows also separates user-mode driver frameworks (UMDF) from kernel-mode ones (KMDF), and since Windows 10 runs parts of the kernel under a hypervisor (VBS/HVCI) so that even ring 0 cannot modify certain memory — an extra privilege level on top of the two.
Key points
- The CPU enforces two privilege levels; in user mode privileged instructions fault and kernel pages are unreadable.
- User mode: application, runtime, libraries (including libc). Kernel mode: scheduler, memory manager, file systems, network stack, drivers.
- Only three doors lead into kernel mode: trap (syscall), interrupt (device/timer), exception (fault). All land in kernel-chosen handlers.
- A segfault is a page-fault exception the kernel cannot resolve, reported to the process as
SIGSEGV; the default action terminates it. - Runtimes exploit this: the JVM and Go turn
SIGSEGVinto exceptions; GCs use protected pages as barriers. - Monolithic (Linux, BSD), microkernel (seL4, QNX), hybrid (Windows NT, XNU): a trade of speed against isolation. Windows stabilises the DLL layer, not the syscall numbers.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why two modes instead of trusting programs?
Because any bug or malicious program in a single-mode system can corrupt any state. The privilege boundary makes the damage a program can do proportional to what the kernel lets it request, and the kernel checks every request.
▸Why can’t a program jump into the kernel wherever it likes?
The entry addresses are fixed in a table the kernel wrote at boot and user mode cannot modify. If user code could pick the target, it could skip the permission checks; fixed entries make the checks mandatory.
▸Why does a null dereference kill the process instead of returning an error?
The CPU raised an exception mid-instruction; there is no return value to deliver. The kernel’s only channel to the process is a signal, and the default action for an invalid memory access is termination because the process’s state is presumed corrupt.
▸Why do microkernels exist if they are slower?
Because in a car, a plane or a phone baseband a crashed driver must not take the system down. When isolation is worth a few microseconds per operation, message-passing servers are the right trade.
User mode / kernel mode
How it fails
What the failure looks like from inside real software.
- "Segmentation fault (core dumped)", exit status 139: an invalid access; the core file and
dmesg(segfault at 0 ip … sp … error 4) tell you the address and whether it was a read or write. SIGBUSwhile reading a memory-mapped file: another process truncated it; the page is mapped but has no backing.- High
%syor%siwith idle user processes: the kernel is busy in interrupt bottom halves (network storm, disk completions) or in reclaim — no user process asked for that CPU. - A kernel-mode driver bug on Linux or Windows does not produce a signal; it produces a kernel panic or a blue screen, because there is nothing above ring 0 to catch it.
- A program that installs a
SIGSEGVhandler and returns from it without fixing the mapping loops forever re-executing the faulting instruction.