A trap is any event that makes the processor stop what it is doing and jump to a handler. It might be an illegal instruction, a page fault, a system call or a timer interrupt. For software to recover and continue, the processor must stop at a clean point, with every earlier instruction finished and every later one having left no trace. That property is called precise exceptions, and in a pipeline with several instructions in flight it takes careful design. This article explains the kinds of trap, what the hardware does step by step, and how a pipeline keeps exceptions precise.

12 min read | Intermediate

What You'll Learn

  • Distinguish exceptions from interrupts, and list common causes
  • Describe what the hardware does on trap entry and on mret
  • Write the outline of a trap handler that saves and restores state
  • Explain how a pipeline makes exceptions precise

Exceptions and Interrupts

RISC-V uses the word trap for both kinds of event:

  • An exception is caused by the instruction being run. It is synchronous: rerun the same program with the same data and it happens at the same instruction.
  • An interrupt comes from outside the instruction stream, such as a timer or a device. It is asynchronous: it can arrive between any two instructions. Interrupts and Interrupt Latency covers those.

The CSR mcause records which trap happened. Its top bit says whether it was an interrupt, and the rest is a code:

Code (exceptions)Meaning
0, 1instruction address misaligned, instruction access fault
2illegal instruction
3breakpoint (the ebreak instruction)
4, 5load address misaligned, load access fault
6, 7store address misaligned, store access fault
8, 9, 11environment call (ecall) from U, S or M mode
12, 13, 15instruction, load and store page faults

Many of these are not errors. A page fault often means only that the page is not in memory yet, and the OS brings it in and restarts the instruction. An illegal instruction may be one the hardware does not implement, and a handler can emulate it in software and continue. A system call is requested on purpose. The whole system relies on being able to stop, fix something, and carry on as if nothing had happened.

What the Hardware Does

When a trap is taken in machine mode, the hardware does a short list of things, all in one step:

Six steps: hardware saves the PC in mepc and sets mcause and mtval; hardware turns interrupts off and sets privilege to M; hardware sets the PC to mtvec; software saves registers and handles the cause; software restores registers and runs mret; hardware restores the PC from mepc and the old mode and interrupt state
  1. mepc takes the PC of the instruction that trapped, or for an interrupt, the next instruction to run.
  2. mcause takes the reason, and mtval takes extra information, such as the bad address, or zero if there is none to give.
  3. mstatus changes: the interrupt enable MIE is copied to MPIE and then cleared, and the old privilege mode is saved in MPP.
  4. The privilege mode becomes machine mode.
  5. The PC is loaded from mtvec.

That is all. The hardware does not save any general registers. RISC-V follows the usual RISC approach and leaves that to software, so the handler saves only what it uses.

Interrupts are off on entry for a good reason. A second trap at this moment would overwrite mepc and mcause, and the first trap's information would be lost. The handler can turn interrupts back on after it has saved those registers.

The instruction mret reverses it: the PC comes from mepc, the mode comes from MPP, and MIE is restored from MPIE.

Exception or Interrupt: Which PC?

Whether mepc points to the instruction itself or to the next one depends on the trap. A page fault saves the faulting instruction's own address, so that mret reruns it after the handler has fixed the problem. An ecall also saves its own address, so the handler must add 4 to mepc, or the program would execute the same ecall forever. This is a common beginner's bug.

A Trap Handler

A handler's first problem is that it needs registers to work with, but every register holds a value that the interrupted program expects to see again. mscratch solves this. It usually holds a pointer to a private save area, and it is swapped with a general register on entry:

trap_entry:
    csrrw t0, mscratch, t0     # swap: t0 = save area, mscratch = old t0
    sw    a0, 0(t0)            # save the registers the handler will use
    sw    a1, 4(t0)
    csrr  a0, mcause           # find out why
    csrr  a1, mepc
    ...                        # handle it
    addi  a1, a1, 4            # for an ecall: return past the instruction
    csrw  mepc, a1
    lw    a1, 4(t0)            # restore the registers
    lw    a0, 0(t0)
    csrrw t0, mscratch, t0     # restore t0, put the pointer back
    mret

The swap with mscratch is why the CSR instructions are atomic. After the first line, the old t0 is safely stored, and the handler has a register to work with. In C, GCC can do this for you with __attribute__((interrupt("machine"))), which saves whatever the function uses and returns with mret. For a small handler that increments a counter, it produces:

timer_isr:
    addi sp,sp,-16
    sw   a4,12(sp)        # save only the two registers it uses
    lui  a4,0x0
    sw   a5,8(sp)
    lw   a5,0(a4)         # ticks = ticks + 1
    addi a5,a5,1
    sw   a5,0(a4)
    lw   a4,12(sp)
    lw   a5,8(sp)
    addi sp,sp,16
    mret

Eleven instructions, of which four move registers to and from the stack and two adjust the stack pointer: over half of the handler is overhead.

Quick Check

Q: A handler for ecall returns with mret without changing mepc. What happens?

Show Answer

mepc still holds the address of the ecall, so the program reruns it and traps again, forever. The handler must add 4 to mepc.

Precise Exceptions

An exception is precise if, when the handler starts, the machine looks as if the program ran one instruction at a time and stopped at a clean point. Every instruction before the trapping one has fully finished, and that instruction and all later ones have had no effect. Without this property, a handler could not fix the problem and restart, because it could not know what state the machine is in.

In a pipeline, this is not automatic. Several instructions are in flight, and faults can be detected in different stages: an illegal instruction in decode, a misaligned load in execute, a page fault in memory. Worse, a later instruction can fault before an earlier one. Imagine a load in the memory stage that page-faults, in the same cycle as an illegal instruction in decode. Both are real, but only the older one should be reported, since the younger one might never have run.

The standard solution is simple:

  1. When a stage detects a fault, it does not trap at once. It records the fault in the pipeline register and turns the instruction into a marked one, which travels on with the rest.
  2. The trap is taken only when a marked instruction reaches a fixed point near the end of the pipeline, called the commit point. Since instructions reach that point in order, the oldest fault is always found first.
  3. Everything younger than it, in the earlier stages, is flushed.
Pipeline grid: an add finishes normally; a load faults in MEM in cycle 5 and is flushed; the sub and the or behind it are flushed; and the handler is fetched from cycle 6

In the example, the add has written its result, while the load, the sub and the or leave no mark. Since register writes occur in WB and memory writes in MEM, the design must take care with stores. A store in MEM must not write if an older instruction is about to trap. In a five-stage pipeline, older instructions are already past that point, except for faults in the MEM stage itself. A store after a faulting load is still in EX, so it cannot get through.

Precise Exceptions in Large Cores

An out-of-order core runs instructions in whatever order their data becomes ready, so a young instruction may finish long before an old one. How can exceptions be precise there? It keeps a reorder buffer, a queue of all instructions in program order. Instructions may execute out of order, but they commit in order, from the front of the queue, and only then do their results become official. A fault is noted in the entry, and it acts only when that instruction reaches the front. Out-of-Order Execution and Register Renaming explains it. The idea is the same as in the simple pipeline, on a larger scale. This same structure also recovers from branch mispredictions.

Not all exceptions have been precise. Some older high-performance designs gave up precision for floating-point errors, because keeping it was too expensive. Software could only be told that something went wrong "somewhere near here". That is now rare, except for some asynchronous memory errors, which often can't be tied to one instruction.

Where Is This Used?

Precise exceptions make much of a modern system possible. Virtual memory depends on restarting a load after a page fault. A debugger sets a breakpoint by writing an ebreak over an instruction. A hypervisor runs a guest OS by trapping its privileged instructions. Emulating a missing instruction in software, as some small processors do for floating point, needs exactly the same thing: stop cleanly, fix it, continue.

Nested Traps

A handler can itself trap. Suppose a page fault occurs inside a handler before it has saved mepc: the old value is lost, and the system cannot return. So the first part of every handler must be written so that it cannot trap: it runs from memory that is always mapped, on a known stack, with interrupts off. Once it has saved the CSRs, the handler can turn interrupts on and allow nesting. Machine-level handlers are the last line of defense, and a fault there usually means a system crash.

Quick Check

Q: In one cycle, a load in MEM reports a page fault and an illegal instruction is found in ID. Which is reported, and what happens to the other?

Show Answer

The load's page fault, because it is the older. The illegal instruction is flushed. If the handler fixes the page and returns, the load reruns, and the illegal instruction will be found again later, this time reported correctly.

Costs of a Trap

A trap is not free, and part of its cost is hidden. There are the direct cycles: flushing the pipeline, fetching the handler, saving and restoring state. Then there are indirect ones. The handler pushes the program's data out of the caches and branch predictors, so that the program runs more slowly for a while after it returns. On a large core, these after-effects can cost more than the trap itself. That is why operating systems try to reduce how often programs have to enter the kernel, and why fast paths exist for the most common system calls.

Some cores cut the direct cost with extra registers set aside for the handler, so that a very short handler can run without touching memory at all.

Common Mistakes

Mistake #1: Taking a trap as soon as a stage detects it

If an early stage traps at once, an older instruction further along may be canceled by mistake, or a younger fault may hide an older one. Carry the fault down the pipeline and act on it at the commit point.

Mistake #2: Letting a store through on a flush

Once memory is written, it cannot be undone. Make sure a store cannot write if any older instruction is trapping, and test stores directly after faulting loads.

Mistake #3: Clobbering registers in a handler

Every register that the handler changes must be restored. A handler that leaves one wrong makes a random failure in whatever code it interrupted, usually far from the cause.

Practice Exercise

Challenge: What Should the Hardware Do?

For each case, say which instruction is reported, what mepc holds and what must be flushed, on a five-stage pipeline where traps are taken from the MEM stage. Addresses are shown in the listing. 0x100: add a0,a1,a2. 0x104: lw a3,0(a0), which faults. 0x108: sw a3,4(sp). 0x10C: .word 0, an illegal instruction. (a) Which fault is reported? (b) What is in mepc and in mcause? (c) Which instructions are flushed? (d) Does the add's result survive?

Hint

Instructions reach MEM in order. A load access fault has code 5.

Solution

(a) The load's fault: it reaches MEM first, and the illegal instruction behind it is only in decode or fetch. (b) mepc = 0x104, mcause = 5 (or 13 for a page fault). (c) The load itself, the store and the illegal word. (d) Yes. The add has already passed MEM and goes on to write a0 in WB, so it finishes normally.

Summary

  • A trap is an exception, caused by an instruction, or an interrupt, from outside.
  • On a trap, hardware saves the PC, cause and status, disables interrupts, raises privilege and jumps to the handler; software saves registers.
  • An ecall handler must add 4 to mepc; a page fault handler must not.
  • A precise exception means all older instructions are finished, and the faulting one and all younger ones have left no trace.
  • A pipeline carries faults to a commit point, and out-of-order cores use a reorder buffer for the same purpose.
Did you find what you were looking for?

Quiz

No quiz questions available for this article.