Interrupts, Exceptions, and Trap Handling

LESSON

Operating Systems Implementation

003 30 min intermediate

Interrupts, Exceptions, and Trap Handling

By the end of this lesson, you will be able to...

  • Classify an event as a syscall, exception, or interrupt from its source and timing.

  • Trace how a RISC-V kernel saves, dispatches, handles, and returns from a trap.

  • Predict why a missing saved register or wrong return PC corrupts a process after an otherwise successful handler.

Idea in one sentence: A trap is the processor's controlled handoff from ordinary execution to kernel code, carrying enough evidence for the kernel to decide what happened and how to resume safely.

calc has reached the protection boundary from lesson 002. It now executes write(1, "42\n", 3). The program cannot call a C function inside the kernel directly: it runs in U-mode and the kernel owns the console driver. It needs a doorway into the kernel.

At almost the same moment, a timer may expire. Or calc may use an invalid pointer. All three events interrupt the comfortable story that the CPU just fetches the next instruction. The kernel must regain control without losing the process's registers, return address, or privilege state. It must also know why it regained control.

That job is trap handling. A good trap path is not a vague jump to “the kernel.” It is a small protocol with saved state, a classification decision, a handler, and a carefully prepared return. A mistake in any stage can turn a harmless system call into a loop, leak one process's registers into another, or return to user mode with interrupts disabled forever.

Core Insight

The word trap names a controlled transfer of execution into a more privileged handler. It is the common mechanism behind three events that have different causes:

Event Who causes it? Is its timing tied to the current instruction? Typical kernel decision
System call the running program, intentionally yes perform or reject a requested service
Exception the running instruction yes repair, report, or terminate the process
Interrupt an external device or timer no acknowledge work and arrange the next action

A system call is a requested crossing. On RISC-V, U-mode code can execute ecall; it asks the execution environment for a service. An exception is a consequence of the instruction currently being executed, such as a page fault or illegal instruction. An interrupt is asynchronous: a timer or device becomes ready even if the process is doing ordinary arithmetic.

The shared mechanism matters because all three must preserve an invariant: after the kernel finishes, it must either return to a defined execution point with the process state intact, or make an explicit decision not to return.

Plain meaning:

The CPU pauses the program, leaves a compact record of why and where it paused, and starts trusted kernel code.

In this scenario:

calc asks for write; the kernel needs the request number, arguments, and the instruction address to which it should eventually return.

Technical name:

This controlled transfer is a trap, and the complete saved program state maintained by kernel entry code is commonly called a trap frame.

What Hardware Records—and What the Kernel Must Still Save

It is easy to picture the CPU placing every register into a perfect structure before entering the kernel. Most architectures do not do that automatically. On a RISC-V transition into S-mode, the architecture records essential control state in supervisor CSRs: sepc identifies the interrupted program counter, scause records the reason, stval may contain event-specific information such as a faulting virtual address, and sstatus retains prior-mode and interrupt-enable information.

The kernel has to save the general-purpose registers itself, usually in a small assembly entry routine. That distinction is important. A C trap handler cannot safely use registers belonging to calc until entry code has put their values somewhere stable. Nor can the kernel assume the user stack is safe or available at the instant of entry; a kernel stack and an entry convention are part of the handoff contract.

One simplified trap-frame record for a process might be:

Field Example value Why the handler needs it
saved general registers a0 = 1, a1 = 0x1000, a2 = 3 syscall arguments and later user-state restoration
sepc address of the ecall instruction return point or faulting instruction location
scause environment call from U-mode dispatch category
stval 0 for this syscall extra fault information when the event provides it
previous privilege / interrupt state U-mode, prior interrupts enabled safe restoration policy

The trap frame is not merely a debugging convenience. It is the boundary object that lets kernel code inspect a stopped computation without confusing it with the kernel's own current registers. Later lessons will add process switching and scheduling; for this lesson, think of the trap frame as the receipt for one interrupted execution context.

The Naive Doorway Breaks

Imagine a simplistic design:

user code calls kernel_write(buffer, length) as an ordinary function

It looks direct, but it fails in several ways. The application would need a valid kernel code address and permission to branch into it. The kernel would receive application registers without a defined saving convention. An invalid memory reference during the call would have no uniform way to identify the interrupted privilege level or return point. A timer interrupt arriving during the helper would be yet another special case.

The naive design makes every crossing a one-off agreement. A trap mechanism replaces it with one standard entry protocol:

event occurs
  -> hardware records control evidence and selects a vector
  -> small entry code saves general registers into a trap frame
  -> kernel dispatcher classifies the cause
  -> specific handler makes a policy decision
  -> return code restores state and executes the architectural return

The vector is the configured entry address. In RISC-V S-mode, stvec holds the trap-vector configuration. In direct mode, traps start at one common entry address; that entry code reads the cause and dispatches. Vectored mode can send asynchronous interrupts to cause-specific offsets, while synchronous exceptions still use the base. Small teaching kernels often make a common entry path explicit because it makes state-saving rules easier to audit.

Worked Trace: write from U-mode to the Console

Follow one deliberate syscall all the way through. This trace simplifies implementation details, but it keeps the ownership and state transitions visible.

Input: calc wants to write three bytes. Before the request it has placed a service number in one argument register and these values in others:

a0 = 1          file descriptor for standard output
a1 = 0x1000     user virtual address of "42\n"
a2 = 3          requested byte count
pc = 0x00000080 address of ecall
mode = U

1. The request instruction changes control, not ownership

calc executes ecall. It does not jump to an arbitrary kernel function. Hardware takes a trap into the configured supervisor environment. For this synchronous event, sepc identifies the instruction that caused the event, and scause says it was an environment call from U-mode. Trap entry also records the prior privilege and interrupt state in supervisor status state.

At this moment, the CPU has not “done the write.” It has only transferred authority to the kernel. The byte buffer still belongs to the user address space, and the kernel has not yet decided whether descriptor 1, pointer 0x1000, and length 3 are valid.

2. Entry assembly creates a stable trap frame

The entry routine uses its agreed kernel context to save user registers. It records a0, a1, and a2 along with the rest of the registers it must restore. It leaves the kernel with a stable pointer to this trap frame.

This intermediate state matters. If the handler overwrote a1 before saving it, it could lose the user buffer address. If it used the user stack before switching to known kernel state, a malformed user stack could destabilize the kernel entry path.

3. The dispatcher classifies before acting

The common trap handler reads scause and follows a narrow decision tree:

is it an interrupt?      -> device/timer handling path
is it a U-mode ecall?    -> syscall dispatcher
is it a page fault?      -> memory-fault policy
otherwise                -> report and terminate or panic, by origin and policy

For this trace, it selects the syscall dispatcher. The dispatcher reads the saved service number and arguments from the trap frame rather than trusting whatever values happen to be in live registers after kernel code has started running.

4. The syscall handler validates and performs the service

The write handler checks that descriptor 1 names a writable console endpoint and that the complete user buffer range is valid. It copies or safely reads the three bytes through the user–kernel boundary, then asks the console driver to emit them. The handler chooses a return value, for example a0 = 3, in the saved user context.

The important boundary is unchanged from lesson 002: a trap gives the kernel control, not permission to trust user input. The handler has more authority, so it has more responsibility to validate.

5. Return code prepares the exact continuation

There is one subtle syscall-specific step. Because sepc points to the ecall instruction, return code must arrange to continue after it. If it returned to the same address without advancing the saved program counter, calc would execute ecall again and repeatedly write the same bytes.

Finally, the return path restores saved registers, restores the intended U-mode and interrupt state, and executes sret. On RISC-V, sret uses the saved supervisor state to select the prior privilege level and sets the program counter from sepc.

Output: calc resumes at the instruction after ecall, sees a0 = 3, and continues as an ordinary U-mode program. Its user registers are exactly its own values plus the documented syscall result.

Naive failure contrast: If the kernel forgets to advance sepc, the handler can be perfectly correct about the console and still produce an infinite syscall loop. If it forgets to restore one register, calc may fail later with a value that looks mysteriously corrupted. Trap handling correctness includes the return path, not just dispatch.

Check: A trap handler receives scause indicating a timer interrupt while a user program is calculating. Should it treat the saved sepc as the address of a faulty user instruction and always advance it before returning?

Think first, then reveal.

Answer: No. A timer interrupt is asynchronous; it is not a syscall instruction that must be skipped. The handler may schedule work or choose another runnable task, but a return normally resumes the interrupted instruction stream at the saved continuation point. Advancing sepc blindly would skip user work.

One Entry Mechanism, Different Policies

The trap entry path should be small and predictable because it runs at a delicate moment. The later decision is policy, and it differs by event.

Cause Evidence the kernel inspects Typical policy Can execution resume?
ecall from U-mode service number and saved arguments validate, dispatch, return result usually yes
user page fault cause, sepc, possible fault address in stval allocate/repair if policy permits, otherwise kill process sometimes
illegal instruction cause, sepc, possible instruction information report or terminate process rarely at the same instruction
timer interrupt interrupt cause and timer state account time, wake work, request reschedule yes, perhaps after a context switch
device interrupt interrupt cause and device status acknowledge completion and wake a waiter yes

The word exception does not mean every event is fatal. A demand-paging kernel may resolve a valid page fault by installing a mapping and retrying the same instruction. An illegal privileged instruction from U-mode generally cannot be repaired in that way. The handler's job is to distinguish the cases rather than treating “trap” as one result.

Reentrancy, Interrupt State, and the Smallest Safe Entry

Trap code can itself be interrupted or fault. That makes entry code more sensitive than ordinary kernel functions. On RISC-V, entering S-mode records the old supervisor interrupt-enable state in SPIE and clears SIE; the kernel later decides when it is safe to allow nested interrupts again. This prevents a second interrupt from immediately reusing half-initialized entry state.

The trade-off is clear. Keeping interrupts disabled for longer makes a simple handler easier to reason about, but it delays timer and device response. Enabling them too early improves responsiveness but requires that the trap frame, kernel stack, and shared kernel state are already safe for reentry. There is no universal “turn interrupts on here” line; the correct point follows from the ownership of the state that entry code is still constructing.

This mechanism also has limits. A correct trap frame cannot fix a kernel bug in the handler. It cannot make a bad syscall ABI safe, and it does not decide whether a process should be scheduled next. It gives later code a reliable starting record. The boundary becomes visible in debugging when a panic or log contains sepc, scause, stval, and a trap-frame register dump: those values are evidence of where control changed and what the machine knew then.

Common Confusions

Confusion: A syscall is just a function call with a different name

Why it is tempting:

Both can pass arguments and return a value.

Better model:

A function call stays within one privilege and calling convention. A syscall deliberately changes execution authority, enters through a configured trap path, and must validate data across the protection boundary.

Confusion: Hardware saves the whole process automatically

Why it is tempting:

The processor saves visible trap CSRs, so it can appear that the complete context is preserved.

Better model:

Hardware records a small control record. Kernel entry code must save the general registers and establish stable kernel execution state before higher-level code can run safely.

Confusion: Every trap should advance the saved PC

Why it is tempting:

An ecall normally needs to continue after its request instruction.

Better model:

Only the handler policy for a specific cause determines the return PC. A repaired page fault may retry the same instruction; an interrupt normally resumes the saved continuation; a syscall must avoid re-executing its request.

Practice: Build a Trap Decision Ledger

For each row below, fill in four columns: event type, evidence to inspect, first kernel action, and safe return policy.

Event Event type Evidence First action Return policy
U-mode ecall for write ? ? ? ?
load from an unmapped user page ? ? ? ?
timer interrupt while a process is in U-mode ? ? ? ?

Use this rubric to check your table:

Check: A kernel log shows scause for a U-mode ecall, but the process always returns with an old value in a0 instead of the byte count. Which trap-frame step is the strongest first place to inspect?

Think first, then reveal.

Answer: Inspect whether the syscall handler writes its result into the saved user register slot that return code restores. Changing a live kernel register is not enough if the return path later reloads the old a0 from the trap frame.

Resources

Key Takeaways

PREVIOUS Privilege, Address Spaces, and Kernel Boundaries NEXT Physical Memory, Page Tables, and Allocation