Skip to content

Level 5 · Chapter 5.5

Traps, interrupts and exceptions

The three ways control leaves a running program without a jump: exceptions raised by an instruction (faults, traps, aborts), deliberate traps into the kernel (system calls), and interrupts from devices and timers — what the CPU does in hardware, how x86, ARM64 and RISC-V differ, and measurements of what traps and system calls cost on real machines.

Every chapter so far followed a program as it jumps and calls its way through its own code. But control can also leave the program without any jump in it: an instruction divides by zero, touches a page that isn't mapped, or asks the operating system for a service; or a network card finishes receiving a packet. Each time, the CPU stops what it's doing, saves just enough to come back, switches to kernel mode and runs a handler chosen from a table. This mechanism is what makes an operating system possible: it's the only way into the kernel, and the only way the kernel gets the CPU back from a program that never gives it up.

Tanenbaum splits these events in two:

  • traps, caused by the program itself: synchronous, and reproducible — run the same program on the same input and they happen at the same instruction every time;
  • interrupts, caused by something outside the program, usually I/O: asynchronous, arriving between any two instructions.

Each ISA has its own vocabulary on top. Intel calls the synchronous events exceptions and splits them into faults, traps and aborts; ARM calls everything an exception, interrupts included; RISC-V calls everything a trap, divided into exceptions and interrupts. The ideas are the same.

What the hardware does

When an exception or interrupt is taken, the CPU performs a short fixed sequence in hardware, with no instructions from the program:

  1. Save the return point: the address of the interrupted or faulting instruction, and the flags.
  2. Switch privilege: from user mode to kernel mode, and, on x86, to the kernel's own stack.
  3. Find the handler: look up the event's number in a table the kernel set up in advance.
  4. Jump to the handler.

The handler saves whatever registers it uses, deals with the event, restores them, and executes a return-from-exception instruction that undoes steps 1 and 2 at once. If it's done right, the interrupted program never notices; Tanenbaum calls that transparency.

x86-64ARM64RISC-V
handler tableIDT: 256 entries of 16 bytes, located by the IDTR registera table of 16 entry points, 128 bytes each, at VBAR_EL1one entry point in stvec (or a table in vectored mode)
where the cause is foundthe vector number (0–255), plus an error code on the stackthe ESR_EL1 syndrome registerthe scause register
return address saved inthe kernel stack (with flags, stack pointer, code segment)ELR_EL1 (flags in SPSR_EL1)sepc (status in sstatus)
return instructioniretqeretsret

x86 pushes the saved state onto memory; ARM64 and RISC-V put it in special registers and leave it to the handler to save them if it needs to. The book describes the x86 table as 8-byte segment descriptors; in 64-bit mode each entry is 16 bytes, to hold a 64-bit handler address.

Exceptions: faults, traps and aborts

Intel sorts exceptions by where execution resumes:

  • a fault is reported before the instruction completes, and the saved address points to the faulting instruction itself, so the handler can fix the problem and run it again. The key example is the page fault: the program touches a page that isn't in memory, the kernel loads it from disk, and the same instruction runs again, successfully. The program never knows. That restart is what makes virtual memory possible.
  • a trap is reported after the instruction, and execution resumes at the next one. Breakpoints (int3) and single-stepping work this way.
  • an abort is unrecoverable, like a machine check reporting a hardware error.

Restarting a faulting instruction requires precise exceptions: when the handler runs, every earlier instruction must have completed and no later one must have changed anything. That's harder than it sounds in an out-of-order core, which has dozens of instructions in flight; the reorder buffer exists largely to guarantee it, by only raising an exception when the faulting instruction reaches the head.

The common x86 exceptions, and what Linux turns them into for the program:

VectorNameRaised byLinux signal
0#DE divide errorinteger division by zero, or a quotient too largeSIGFPE
1#DB debugsingle-step, hardware breakpointsSIGTRAP
3#BP breakpointint3 (the byte 0xCC)SIGTRAP
6#UD invalid opcodean undefined instruction, or ud2 (0f 0b)SIGILL
13#GP general protectiona privileged instruction in user mode, a non-canonical addressSIGSEGV
14#PF page faultan access to an unmapped or protected pagehandled silently, or SIGSEGV
18#MC machine checka hardware errorusually a kernel panic

The simulator raises the same divide error:

Live · Dividing by zero

Try it: Press Step to run one instruction, Run to animate or Continue to finish; the L2–L7 buttons zoom in and out one level at a time.

program— ▸ is the next instruction
  1. mov eax, 7
  2. xor edx, edx
  3. xor ecx, ecx
  4. div ecx ; edx:eax / ecx, with ecx = 0
  5. mov eax, 1 ; never reached
step 0
Loading emulator…
The instruction that just ran, as the bytes the CPU actually fetched and decoded.

Stepping onto the div stops the machine with #DE divide error. On a real system, the kernel's handler for vector 0 would send the process SIGFPE, and without a signal handler, the process dies.

Not every ISA traps

The book lists overflow and division by zero among the classic causes of traps. That's where ISAs differ most. Measured on an Apple M2, with the same C program built for ARM64 and for x86-64 (run under Rosetta 2):

volatile int zero = 0;ARM64x86-64
7 / zero (integer)returns 0, no trapSIGFPE
7.0 / 0.0 (floating point)inf, no trapinf, no trap
undefined instruction (udf / ud2)SIGILLSIGILL
breakpoint (brk / int3)SIGTRAPSIGTRAP
read through a null pointerSIGSEGVSIGSEGV

ARM64's divide instruction sdiv simply returns 0 for a zero divisor, and RISC-V's returns −1 (all ones); neither traps. Designers of those ISAs chose to keep the divider simple and let software check if it cares, which C compilers don't: division by zero is undefined behavior. Floating-point division by zero doesn't trap anywhere by default. IEEE 754 defines a result (infinity) and just sets a status flag, and the traps it allows are disabled unless a program enables them.

Integer overflow, the book's first example, doesn't trap on today's mainstream ISAs either. x86 had an instruction for it, INTO (the byte 0xCE), which trapped if the overflow flag was set; it was removed from 64-bit mode — nasm refuses it with instruction not supported in 64-bit mode. MIPS has an add that traps on overflow, but compilers emit addu, which doesn't. Languages that check overflow, like Rust in debug builds or Swift, do it in software, with a conditional jump on the overflow flag after each operation.

System calls: traps on purpose

A program can't jump into the kernel: kernel memory isn't mapped for it, and jumping there would fault. The only way in is a deliberate trap, an instruction whose whole purpose is to raise one. The kernel reads the requested service's number and arguments from registers, does the work, and returns.

x86-64ARM64RISC-V
instructionsyscall (0f 05)svc #0ecall
call number inraxx8 (Linux)a7
arguments inrdi, rsi, rdx, r10, r8, r9x0–x5a0–a5

32-bit Linux programs used int 0x80, a software interrupt through the IDT. syscall is a faster, dedicated path that skips the table lookup. The simulator implements Linux's write (number 1) and exit (number 60):

Live · Two system calls: write, then exit

Try it: Press Step to run one instruction, Run to animate or Continue to finish; the L2–L7 buttons zoom in and out one level at a time.

program— ▸ is the next instruction
  1. .data
  2. msg: .asciz "hello\n"
  3. .text
  4. mov eax, 1 ; write(
  5. mov edi, 1 ; fd 1 = standard output,
  6. lea rsi, [rip+msg] ; buffer,
  7. mov edx, 6 ; 6 bytes)
  8. syscall
  9. mov eax, 60 ; exit(
  10. mov edi, 3 ; status 3)
  11. syscall
step 0
Loading emulator…
The instruction that just ran, as the bytes the CPU actually fetched and decoded.

It prints hello and exits with status 3. In a real program, the C library's write and exit functions are thin wrappers around exactly these instructions; the operating-system level looks at what the kernel does on the other side.

Entering and leaving the kernel costs far more than a call. On the M2 under macOS, a loop of getppid() — about the cheapest system call there is — took 92 ns per call, against 0.9 ns for an ordinary function call: a factor of about 100. In a Linux virtual machine on the same computer, it was 152 ns. The instruction itself is fast; the cost is in switching stacks, saving registers, and the security mitigations modern kernels apply on every entry. That's why system calls are batched when possible, and why Linux answers some of the most frequent ones, like reading the clock, from a page of kernel data mapped into every process (the vDSO), with no trap at all.

Interrupts

An interrupt comes from outside the instruction stream: a disk finishing a transfer, a network packet arriving, a key being pressed, a timer running out. The CPU checks for pending interrupts between instructions, and when one is pending and interrupts are enabled, it performs the same save-switch-jump sequence as for an exception, with the vector number supplied by the device or its controller.

Tanenbaum describes the classic design: the device raises an interrupt line on the bus, the CPU acknowledges, and the device puts its vector number on the data bus. PCs of that era used an 8259A interrupt controller to arbitrate priorities between devices. Modern machines work differently:

  • Each core has a local APIC (on ARM, a GIC redistributor) that receives interrupts, prioritizes them and delivers them to its core.
  • PCIe devices don't have interrupt wires at all. They signal an interrupt with MSI (message-signaled interrupts): an ordinary memory write to a special address, carrying the vector number as data. A network card with many queues can target a different vector — and a different core — for each.
  • Cores interrupt each other with inter-processor interrupts (IPIs), for example to make another core flush its TLB or reschedule.
  • A timer interrupt on each core is what lets the kernel take the CPU back from a program that loops forever, and switch to another: it's the heartbeat of preemptive multitasking.
  • Non-maskable interrupts (NMIs) still exist, as the book says, for emergencies — and for profilers and watchdogs that must work even while interrupts are disabled.

The book's priority scheme survives too. A handler for a high-priority interrupt can itself be interrupted by an even higher one, but lower ones wait. The local APIC implements it by ranking interrupts by vector number.

How busy is this? On an idle Linux virtual machine with 24 cores, /proc/stat counted about 1,500 interrupts and 2,200 context switches per second, with no program running. Each one was a trap into the kernel and a transparent return — invisible to every program it interrupted.

Takeaways

  • Exceptions (Tanenbaum's traps) are synchronous: caused by an instruction, at the same place every run. Interrupts are asynchronous: caused by devices and timers, between any two instructions.
  • The hardware saves the return address and flags, switches to kernel mode, and jumps through a table (x86's IDT, ARM64's VBAR_EL1, RISC-V's stvec); iretq, eret and sret return.
  • A fault restarts the faulting instruction (page faults make virtual memory work); a trap resumes after it; an abort can't resume. Restarting needs precise exceptions.
  • What traps depends on the ISA: x86 traps on integer division by zero, ARM64 returns 0 and RISC-V −1. Floating-point errors and integer overflow don't trap by default anywhere; x86 removed INTO in 64-bit mode.
  • System calls are deliberate traps (syscall, svc, ecall), about 100 times the cost of a function call — measured at 92 ns on an M2.
  • Modern interrupts are delivered by per-core APICs, as MSI memory writes from PCIe devices, and as IPIs between cores; a timer interrupt drives the scheduler.

In this level

  1. 5.1What the ISA promises: memory model, alignment and ordering
  2. 5.2Instruction formats and encoding
  3. 5.3Addressing modes
  4. 5.4x86, ARM and RISC-V side by side
  5. 5.5Traps, interrupts and exceptions
  6. 5.6Data types the hardware understands
  7. 5.7VLIW, EPIC and the Itanium