Interrupts, Exceptions, and Trap Handling
LESSON
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:
- The syscall row identifies saved arguments and advances past the request before returning a result.
- The page-fault row uses the cause, fault address when supplied, and mapping policy; it either repairs and retries or ends the process, rather than always advancing the PC.
- The timer row treats timing as external, acknowledges or accounts for the event, and preserves a valid continuation even if the scheduler later selects another process.
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
- [REFERENCE] RISC-V Supervisor-Level ISA — Focus:
stvec,sepc,scause,stval, interrupt state, andsret. - [BOOK] xv6 RISC-V textbook — Focus: Chapter 4's four-stage trap handling path and user/kernel transitions.
- [COURSE] MIT 6.1810 traps lab — Focus: reading a small kernel's trap handling code and observing the saved state.
Key Takeaways
- Syscalls, exceptions, and interrupts have different causes and policies, but all use a controlled trap entry path.
- RISC-V records key trap evidence in supervisor state; kernel entry code must save general registers into a trap frame.
- A trap handler must classify the cause before deciding whether to dispatch, repair, schedule, terminate, or return.
- Returning safely requires the correct privilege state, registers, and program counter; the right PC policy depends on the cause.
- Short, auditable entry code improves correctness, while interrupt re-enablement introduces a responsiveness-versus-reentrancy trade-off.