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:
- It saves the address of the instruction in the EPC (exception program counter).
- It writes a code for the reason into the Cause register.
- It jumps to the exception handler at address
0x80000180and switches to kernel mode.
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:
| Cause | Code in Cause | Comes from | What the handler does |
|---|---|---|---|
| hardware interrupt | 0x00 | a device (here the timer) | serves the device, returns to EPC |
| system call | 0x20 | the syscall instruction | does the service, returns to EPC + 4 |
| undefined instruction | 0x28 | an opcode the decoder does not know | kills the program |
| arithmetic overflow | 0x30 | add, sub, addi | kills 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 byPCSrc = 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.
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
mfc0instruction. 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.
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.