Exceptions, CSRs and interrupts

RISC-V keeps the address of its handler in a control register instead of a table, and this editor really runs the handler you write, once one bit of ustatus is set. The CSR instructions, ucause and uepc, uret, and why the handler has to step past the instruction that faulted.

A lw at an address that is not a multiple of four and a lw at an address that is in no segment do the same thing to the program counter: they take it away from your program and give it to a handler. On RISC-V the address of that handler is in a register, and this editor runs the one you write there.

Three words for the machinery, which Assembly basics set out. An exception is the CPU refusing to carry out the instruction it is on, at an instruction your program chose. An interrupt comes from a device between two instructions and has nothing to do with which two they were. A trap is an instruction you ran on purpose to hand control over, which on RISC-V is ecall.

No table, and no fixed address either

The M68K has 256 vectors at the bottom of memory, one per cause, and the CPU reads the address of the handler out of the table. MIPS has one handler at one fixed address, 0x80000180, in a segment of its own. RISC-V has neither: the handler is ordinary code in .text, at whatever address it happens to land, and your program writes that address into a control register called utvec before anything can fault.

Nothing is reserved. A program with no handler installed leaves utvec at 0, and then a fault ends the run and puts a message under the editor, which is every program in this course before this page.

Control and status registers

The registers that describe an exception are not among the 32. They are CSRs, control and status registers, a separate file of up to 4096 numbered registers, and instructions of their own reach them. RISC-V calls that group the Zicsr extension.

The user level ones this editor has:

numbernamewhat it holds
0x000ustatusbit 0 switches the handler on
0x004uiewhich interrupts are enabled
0x005utvecthe address of the handler
0x040uscratchone word that belongs to the handler
0x041uepcthe address of the instruction that faulted
0x042ucausewhy it faulted
0x043utvalthe address or value that went wrong
0x044uipwhich interrupts are pending
0xC00cyclea cycle counter, with time and instret next to it

Every CSR instruction does a read and a write at the same time, which is what makes them atomic: the old value comes out into the register you named while the new one goes in.

instructionwhat it does
csrrw rd, csr, rsread the CSR into rd and write rs into it
csrrs rd, csr, rsread it into rd and set the bits rs has set
csrrc rd, csr, rsread it into rd and clear the bits rs has set
csrrwi, csrrsi, csrrcithe same three with a 5 bit constant instead of a register

Six is more than anyone wants to write, so there are short names, and each of them is one of the six with zero in an operand:

you writewhat it becomes
csrr t0, ucausecsrrs t0, ucause, zero
csrw t0, utveccsrrw zero, utvec, t0
csrs t0, ustatuscsrrs zero, ustatus, t0
csrsi ustatus, 1csrrsi zero, ustatus, 1

The register comes first in csrr and in csrw, so csrw t0, utvec writes t0 into utvec and reads nothing back.

The bit that switches it on

Writing utvec is not enough. Bit 0 of ustatus has to be set, and until it is, a fault ends the run whatever utvec holds. That is the line people leave out.

s0 comes out at 5, so the program survived the fault and ran to its ecall. t2 is 0, because a load that faults writes nothing. s1 is 4, the cause code for a misaligned load, and s2 is 0040001C, the address the handler sent the program back to.

Delete the csrsi ustatus, 1 line and press Run: the handler is still installed and it is not entered, and the run ends with Runtime exception at 0x00400018: Load address not aligned to word boundary 0x10010001. Then put it back.

uret, return from a user trap, puts uepc into the program counter. Step through the program with the pc register in view and you can watch it jump into the handler and come back.

The addi s2, s2, 4 is the line that matters. uepc is the address of the instruction that faulted, not the one after it, so a handler that returns without moving it runs the same lw again, faults again, and goes round for ever. Delete that line and the csrw under it and press Run: the program stops when the Playground's two million instructions run out, with the pc parked on the lw.

Adding 4 is right here because the handler decided to skip the instruction. A handler that has actually fixed the problem, by loading a page or emulating a missing instruction, returns to the same address so the instruction runs again, and leaves uepc alone.

ucause and utval

ucause holds a number saying what happened, and utval holds the address or the value the fault was about.

codewhat happened
4load address misaligned
5load access fault, an address in no segment
6store address misaligned
7store access fault

Those four are the ones a program you write here can raise. The specification numbers more of them, 2 for an illegal instruction and 3 for a breakpoint among others, and this assembler will not let you write an instruction that raises them.

code comes out at 4, epc at 00400018, the address of the lw, and bad at 1001000D, the address that was not a multiple of four. Change the lw to a sw and code becomes 6, and point it at address 4 instead and code becomes 5 with bad at 00000004.

The registers a handler may use

MIPS reserves two registers, $k0 and $k1, that a program is told never to keep anything in, so its handler always has two registers to work with. RISC-V reserves none. A handler here runs between two instructions of a program that is using all 32, and every register it writes is one it has taken.

What it gets instead is uscratch, one control register that belongs to it. The usual idiom is csrrw sp, uscratch, sp, which swaps the handler's own stack pointer in and the program's out in one instruction, and then the handler has a stack and can save whatever it likes on it.

The program below causes three faults, two loads at an address in no segment and one unaligned load, and the handler counts them in uscratch and touches nothing else the program cares about.

s0 comes out at 3, and none of the three faults stopped the program. t2 and t4 are 0, because every one of the three loads was skipped without writing anything, which is what a handler that only steps past the problem leaves behind. The handler still destroyed t5 and t6, which is fine here because main was not using them and would have been a bug if it had been.

What this editor does not do

No device raises an interrupt here. The bitmap display never interrupts, and the keyboard's interrupt enable bit, bit 1 of the receiver control register at 0xffff0000, ends the run with a message saying so and telling you to poll the Ready bit instead. So uie and uip are registers nothing will ever set, and the handler above only ever runs for exceptions the program caused itself.

Polling is what replaces an interrupt, and the previous lecture's keyboard loop is what it looks like: read a status register, and when nothing is ready, let some program time pass with ecall service 32 and go and look again.

ebreak pauses instead of trapping. The instruction that a debugger uses to plant a breakpoint stops the run here, and it does that whether or not a handler is installed, so ebreak is a way of stopping a program and not something a handler can catch.

A fault with no handler ends the run. These are the messages you will meet, each naming the line:

what you didthe message
lw at an address not a multiple of 4Load address not aligned to word boundary 0x...
sw at oneStore address not aligned to word boundary 0x...
lh at an odd addressLoad address not aligned on halfword boundary 0x...
read or write outside every segmentaddress out of range 0x...
read the instructions as dataCannot read directly from text segment!0x...
an ecall number nothing answersinvalid or unimplemented syscall service: 99
ret or jr to an address with no codeInstruction load access error

The first four raise an exception with a code from the table above, so a handler catches them. Without one they end the program, which for a program you are debugging is usually what you want.

There is one more thing RISC-V does not raise: arithmetic never faults. There is no overflow exception, and a division by zero answers -1 instead of trapping, which "Arithmetic, logic and bits" went through. MIPS raises an exception for the first and executes a break for the second, so a handler written for that course has two causes to deal with that cannot happen here.

Your turn

The program below faults on an unaligned load and the handler is empty, so the run stops. Fill the handler in so the program carries on and s0 comes out at 5.

Show solution

The second one faults on the same unaligned load, and this time the handler has to say what happened. Store the cause from ucause into code and the faulting address from utval into bad, then step past the instruction. The code for a misaligned load is 4, and w lands at 0x10010009.

Show solution