When an instruction cannot be carried out
The overview of this topic is in Assembly basics. The same topic in M68K, MIPS, Z80, x86.
ecall is a deliberate trap: your program runs it to ask the Playground to do something. This
lesson handles a different kind of event, a synchronous fault: an instruction cannot be carried
out, so the processor transfers control to a handler.
For example, lw needs a word-aligned address. An address ending in binary ...01 is not a
multiple of four, so a word load from it faults.
What happens on a fault
Before the handler's first instruction, the hardware does these things in order:
- It abandons the instruction that cannot run.
- It writes that instruction's address to
uepc. - It writes a cause code to
ucause, and the related address or value toutval. - It jumps to the address in
utvec, if user exceptions are enabled inustatus.
The handler finishes with uret. uret resumes execution at the address currently in uepc.
That is why a handler can either retry a faulting instruction or arrange to skip it.
utvec, ustatus, uepc, ucause, and utval are control and status registers (CSRs), separate
from the 32 general-purpose registers. These are the CSR instruction forms used here:
| instruction | meaning |
|---|---|
csrr rd, csr | read a CSR into general-purpose register rd |
csrw rs, csr | write general-purpose register rs into a CSR |
csrsi csr, mask | OR the immediate mask into a CSR |
So csrsi ustatus, 1 ORs in the mask 1 (...0001 in binary), setting bit 0. Without enabling
bit 0, writing a handler address to utvec alone has no effect. When user exceptions are disabled,
this Playground reports the fault and ends the run.
Installing a handler
This program deliberately places w at a misaligned address. The byte odd occupies one byte;
.align 0 prevents the usual padding before the word, so w's address ends in ...01. A normal
word alignment directive would insert padding before w.
The handler uses t0 and t1, so it saves and restores both. It makes two word-sized stack slots,
saves those registers, does its work, restores them, then restores sp. The Playground provides a
valid stack for this program, and every stack access below is word-aligned.
Here j end is ordinary control flow. end names the address after the last assembled instruction,
so reaching it completes this Playground run.
After the run, s0 is 5. last_cause holds 4, the code for a misaligned load, and last_value
holds the misaligned address of w.
Choosing where to return
uepc contains the address of the lw that faulted, not the address after it. If the handler
executes uret without changing uepc, the processor retries that same lw; it faults again and
the program repeats the handler forever.
This handler chooses not to perform the load, so it adds 4 to uepc. Four is correct for the
32-bit, four-byte instructions this Playground assembles. A handler that repaired the problem
instead would leave uepc unchanged and let the instruction run again.
For these memory faults, ucause and utval help identify the problem:
| code | what happened | utval |
|---|---|---|
| 4 | load address misaligned | the load address |
| 5 | load access fault | the inaccessible address |
| 6 | store address misaligned | the store address |
| 7 | store access fault | the inaccessible address |
Arithmetic instructions do not raise these faults. That includes div: as in the earlier arithmetic
work, this Playground produces -1 for division by zero.
Polling still handles input
An interrupt is a device asking for attention between instructions; it is different from the instruction-caused faults above. This Playground does not deliver device interrupts. Its keyboard model remains polling: read the ready status, then read a character when it is ready.
Your turn
The lw below faults. Fill the handler so it advances uepc past that lw and returns with
uret. Preserve t0 and restore sp before returning.
Automated state checks cover reaching post-fault main, preserved t0, unchanged t2, and restored
sp; stepping verifies the uepc update and uret control path.