The idea: an unplanned call into the OS

An exception is like a procedure call that the program did not write. Something happens that the program can't or shouldn't handle itself, and the processor hands control to the operating system. Afterwards the OS either goes back to the program or ends it.

Whatever the cause, the hardware does the same three things:

  1. It saves the address of the instruction in the EPC (exception program counter).
  2. It writes a code for the reason into the Cause register.
  3. It jumps to the exception handler at address 0x80000180 and switches to kernel mode.
A user program hits an exception at 0x00400008. The hardware sets EPC to 0x00400008, sets Cause to the code and loads PC with 0x80000180 in kernel mode. The handler reads Cause with mfc0. A syscall returns to EPC + 4, an interrupt returns to EPC, an overflow or undefined instruction kills the program
Every exception takes the same three hardware steps; only the handler looks at the cause and decides where, or whether, to go back.

Causes and the Cause register

Some exceptions are caused by the instruction that is running. Others come from outside. An exception from outside, such as a timer or a keyboard, is called an interrupt. The page shows four causes:

CauseCode in CauseComes fromWhat the handler does
hardware interrupt0x00a device (here the timer)serves the device, returns to EPC
system call0x20the syscall instructiondoes the service, returns to EPC + 4
undefined instruction0x28an opcode the decoder does not knowkills the program
arithmetic overflow0x30add, sub, addikills the program

A syscall is an exception the program asks for on purpose. It is how a program asks the OS to print, read a file or exit. Here $v0 = 1 means "print the integer in $a0", as in the SPIM and MARS simulators. (Demo: syscall prints 42.)

EPC: where to go back

EPC holds the address of the instruction where the exception happened, here 0x00400008. The handler decides where the program continues:

  • After a syscall the instruction has done its job. Going back to EPC would run it again, so the handler adds 4 and returns to the next instruction.
  • After an interrupt the instruction at EPC was never fetched. The handler returns to EPC itself, and the program does not notice anything. (Demo: timer interrupt, then the add runs.)
  • After an overflow or an undefined instruction there is nothing sensible to go back to. The OS ends the program. On Linux the program gets the signal SIGFPE or SIGILL. (Demo: add overflows, Demo: undefined opcode.)

The exception handler

There is only one handler address, so the handler first finds out why it was called. It copies Cause into a normal register with mfc0 (move from coprocessor 0) and branches on the code. EPC and Cause are not in the normal register file. They live in coprocessor 0, the part of the processor that controls the system: EPC is register 14 and Cause is register 13. The handler on the page:

0x80000180  mfc0 $k0, Cause     # why are we here?
0x80000184  (branch on $k0)     # 0x00, 0x20, 0x28 or 0x30
0x80000188  jal  service        # print, kill or count a timer tick
0x8000018C  mfc0 $k1, EPC       # where did it happen?
0x80000190  addi $k1, $k1, 4    # syscall only
0x80000194  mtc0 $k1, EPC
0x80000198  eret                # PC ← EPC, back to user mode

The handler may only use $k0 and $k1 without saving them first. The MIPS register convention keeps these two registers for the OS, so a program must never count on them. Any other register the handler uses must be saved and restored, because the program can be stopped between any two instructions. This is the same idea as the preserved registers of a procedure call; see MIPS Procedure Calls.

Kernel mode and user mode

A program runs in user mode. It may not use coprocessor 0 or the OS's memory. Taking an exception sets the EXL bit in the Status register (coprocessor 0 register 12), which puts the processor in kernel mode. eret (exception return) clears EXL and loads PC from EPC in one instruction, so the program gets control back only in user mode. That is why a program can't just jump to the OS's code: the only way in is through an exception, to the one address the OS controls.

The hardware: EPC, Cause and a new mux input

To support exceptions the datapath needs very little:

  • two registers, EPC and Cause, each with its own write enable (EPCWrite, CauseWrite);
  • a small mux in front of Cause that picks the code (0x28 for undefined, 0x30 for overflow), driven by a control signal (IntCause in Harris & Harris);
  • a fourth input on the PCSrc mux: the constant 0x80000180, selected by PCSrc = 11. The other inputs are PC + 4 (00), the branch target (01) and the jump target (10);
  • a path from coprocessor 0 to the register file for mfc0.
The PCSrc mux with four inputs: 00 PC + 4, 01 branch target, 10 jump target, and the new 11 input, the constant 0x80000180, feeding the PC. Below it, the PC is copied into EPC and a small mux picks the code 0x28 or 0x30 for the Cause register, both in coprocessor 0
Exceptions add a fourth PCSrc input and two coprocessor 0 registers; the rest of the datapath is unchanged.

For the single-cycle processor this is the whole change. For the multicycle processor the controller also needs new states.

New states in the controller FSM

The multicycle controller is a finite state machine. Every instruction starts with S0 Fetch and S1 Decode. Exceptions add a few states:

  • S12 Undefined: S1 finds an opcode it does not know. Cause ← 0x28, EPC ← PC.
  • S13 Overflow: S6 Execute finds that the ALU overflowed. S7 ALU Writeback is skipped, so the wrong answer is never written to $t2. Cause ← 0x30, EPC ← PC.
  • S14 Exception: PCSrc = 11 and PCWrite, so PC ← 0x80000180. Then back to S0, which fetches the first instruction of the handler.
  • S15 MFC0: for the mfc0 instruction. It writes a coprocessor 0 register into the register file.

The page adds two states the same way: Syscall (Cause ← 0x20) after S1, and Interrupt (Cause ← 0x00), which the controller enters instead of S0 when an interrupt is pending.

Controller FSM: S0 Fetch to S1 Decode to S6 Execute to S7 ALU Writeback. S1 goes to Syscall or S12 Undefined, S6 goes to S13 Overflow, S0 goes to Interrupt when one is pending. All four cause states go to S14 Exception, which loads PC with 0x80000180 and goes back to S0
Each cause state writes its code into Cause; they all meet in S14, which sends the PC to the handler.

The cause states come after Fetch, which has already set PC to PC + 4. The MIPS architecture defines EPC as the address of the instruction itself, and that is what the page shows.

add vs addu

add, sub and addi treat their operands as signed numbers and raise an exception on overflow. Adding two positive numbers and getting a negative result means the answer does not fit in 32 bits. addu, subu and addiu do exactly the same arithmetic but never raise an exception: 0x7FFFFFFF + 1 simply wraps to 0x80000000 (−2147483648). C compilers use addu, because C does not trap on overflow; the "u" does not mean the numbers are unsigned. (Demo: addu, same sum, no exception.) With $t0 = 0x80000000, the smallest int, sub overflows the other way.

What the page leaves out

Real MIPS processors have more causes: TLB misses and page faults (see Virtual Memory in Hardware), address errors from unaligned loads, bus errors, and breakpoints. The Cause register also holds pending-interrupt bits, and Status holds an interrupt mask, so the OS can turn interrupts off. TLB refills use a faster vector at 0x80000000. An exception on an instruction in a branch delay slot sets EPC to the branch and a BD bit in Cause. The page leaves out pipelined processors too, which must flush the instructions behind the one that caused the exception, so that exceptions stay precise. Finally, a real handler saves more registers on a kernel stack and may switch to another program, as on Kinds of Interrupts in Linux. The Interrupt-Driven I/O Cycle follows one device interrupt from start to end.