06 · A Trap, With Nothing Else in the Machine¶
Chapter header
The question¶
What is a trap, when nothing else is going on?
A trap is a jump the processor makes without being asked, to an address it was given in advance. Part II said why a kernel is a hard place to learn that: almost nothing on the page is the mechanism. Here there is nothing else — no kernel, no library, no loader, and no operating system. One handler, one deliberate trap, and every question about it answered by the machine.
The material¶
The whole machine¶
Three files, and between them they are the entire runtime. The linker script says where the
program goes, because with -bios none — no firmware — QEMU releases the processor at a fixed
address and expects to find instructions there.
SECTIONS
{
. = 0x80000000;
.text : {
*(.text.entry) /* first, because execution begins at the lowest address */
*(.text .text.*)
}
The entry code makes a stack and zeroes the memory C is entitled to assume is zero — every
variable outside a function that was declared without a value. Nothing else will: there is no
loader to set memory up, and no runtime to call before main.
_entry:
csrr t0, mhartid # QEMU releases every hart here, at the same instant
# sp = bare_stacks + (hartid + 1) * BARE_STACK_BYTES. Each hart gets its own and they
# grow down into it. Two harts sharing one stack looks exactly like memory corruption.
la sp, bare_stacks
li t1, 4096
addi t2, t0, 1
mul t1, t1, t2
add sp, sp, t1
And the console is two device registers. Writing a byte to one sends it; the other says whether the device is ready for the next. That is the whole driver. The kernel’s version in ch19 is these same two registers underneath a great deal of machinery about sleeping and waking.
void bare_putc(char c) {
while ((uart[UART_LSR] & UART_LSR_THRE) == 0) {
/* The device has a byte in hand. Spin: there is nothing else to do and nobody to
* yield to. A kernel would sleep here, which is the interrupts-and-drivers chapter's subject. */
}
uart[UART_THR] = (uint8)c;
}
Where the processor goes¶
One register decides where a trap lands: mtvec, the machine trap vector, holds the address to
go to. It is one of the processor’s control and status registers — CSRs, a set apart from a0
and its neighbours, read and written with instructions of their own, which is what
bare_csr_write wraps. The m is for machine mode, the most privileged state the processor
has and the one it comes out of reset in; ch07 is where the other
modes appear.
bare_csr_write(mtvec, (uint64)handler);
Build it and boot it on a machine with nothing on it:
./run trapThat is the installation, complete. There is no table of handlers, no registration, and nothing to
tell the processor which kinds of trap this handler wants — it gets all of them. Sorting out which
is which is the handler’s job, and it reads mcause to do it —
ch07’s subject.
Where it was, and the mistake that loops for ever¶
The processor also records where it was when the trap happened, in mepc — the saved program
counter, the processor’s record of which instruction it is on. Exactly which instruction that
means is the first thing about traps that surprises people:
/* mepc holds the address *of* the instruction that trapped, not the one after it. Returning
* without moving it re-executes the ecall, which traps again, for ever. The machine does not
* consider this an error; it is doing exactly what it was told. */
bare_csr_write(mepc, seen_epc + 4);Three registers and the program counter. Everything a reader associates with a trap — a saved frame, a process, a dispatch — is software somebody wrote, and none of it is here yet.
mepc points at the instruction that trapped, not past it. A handler that returns without
moving it re-executes the same instruction, which traps again, into the same handler, for ever.
The machine does not consider this an error. It is doing exactly what it was told, and the symptom
is a program that prints nothing and never finishes.
Whether four is the right number to add is not obvious either. ch02
pointed out that instructions on this architecture are not all the same length, so adding a fixed
four ought to look reckless. It is safe here only because the programs in this part are built
for rv64g, without the compressed extension — the short forms ch02
mentioned — so every instruction in them really is four bytes. That is a property of the build,
not of the processor, and the first problem below is about what happens when it stops holding.
What a trap does not do¶
It does not save anything. Not the registers, not the stack, not a frame — the processor changes
mepc, mcause and a couple of bits of mstatus, its status register, then jumps, and that is
all. Every register still
holds what the interrupted code put there, and the instant the handler uses one, that value is
gone.
Here the compiler covers for us:
/* `interrupt("machine")` makes the compiler do two things a normal function does not: save every
* register it touches, including the caller-saved ones an ordinary function may clobber, and end
* with `mret` rather than `ret`. That is the whole difference between a function and a handler,
* and the bare-metal system-call chapter is where you write it out by hand, because a handler that
* has to read the caller's registers and change one needs them somewhere it can reach. */
__attribute__((interrupt("machine"), aligned(4))) static void handler(void) {What the compiler saves is a private matter between the handler’s first instructions and its
last — the mret that jumps back to mepc: the C in between cannot name those slots, cannot
read what the interrupted code had in a0, and cannot change what it will get back. That is fine here, because this handler has
nothing to say to the code it interrupted. A trap that is told something and has to answer —
a system call, which takes its arguments from the caller’s registers and puts its result back
into one — needs the saved registers laid out where the handler can reach them, and that is a
frame written by hand. It is ch09, and it is why that handler
saves all thirty-one itself.
Proving it¶
The program sets a register, traps — with ecall, the instruction whose only job is to trap —
and reads the register back afterwards. asm volatile is how C embeds instructions of your own;
the volatile is ch05’s, and stops the compiler
removing them.
asm volatile("li s2, 0x5eed\n"
"la %0, 1f\n"
"1:\n"
"ecall\n"
"mv %1, s2\n"
: "=&r"(ecall_at), "=r"(witness_after)
:
: "s2", "memory");
What we measured¶
Run it yourself before reading the table — the rows below are what you should see, and a figure you have reproduced is worth more than one you have been shown:
./run trap| One trap, start to finish | Observed |
|---|---|
mtvec holds the handler’s address | yes |
mcause | 11 |
| …which is environment call from machine mode | yes |
mepc is the address of the ecall, not after it | yes |
| Bytes the handler must add before returning | 4 |
| A register set before the trap, still set after it | yes |
| Traps taken | 1 |
Conditions: target bare · qemu virt (-machine virt -bios none) · QEMU emulator version 8.2.2 (Debian 1:8.2.2+ds-0ubuntu1.18) · no firmware, no kernel · riscv64-linux-gnu-gcc · -march=rv64g -mabi=lp64 -mcmodel=medany -ffreestanding -nostdlib -fno-pic -fno-stack-protector -fno-builtin -O1 -g -Wall -Werror · 2026-09-17 · Source: bench/results/trap-bare.json, code hash 4d71347a1a9cb4fd.
Every row is the machine answering rather than the chapter asserting, and bench/run_bare.py
refuses the run if any of them stops being true — including the two that look like formalities:
that mtvec reads back what was written, and that the program resumed, which it proves by
reaching the line that prints it. If mepc_is_the_ecall ever came back no, the paragraph above
about the infinite loop would be describing a different processor.
What this cannot tell you¶
How long a trap takes. Nothing on this target may be timed, for the reason Part I gives: a duration measured under QEMU is a fact about the laptop running the emulator. What a trap costs is ch29, on hardware.
What a real board does before your code runs. QEMU with -bios none hands the processor over
at the reset address with the machine in a defined state. Part II says what a physical
board does instead, and why this program will not boot one unchanged.
Whether the compiler will keep covering for you. interrupt("machine") saved the registers
because this handler had nothing to hand back. The moment a trap has to answer the code it
interrupted, the saving is yours to write.
Problems¶
Three, and the second one is the one that catches people.
6.1 — Why four, and when is it not?
The handler adds four to mepc. Say what that four actually is, and give a case in this
instruction set where adding four would resume in the wrong place. The test asks for both, and
checks the second against an assembled instruction rather than against a claim.
python3 -m pytest tests/a_trap_with_nothing_else/test_problem_1_advance.py6.2 — Make it loop, then explain it.
Remove the line that advances mepc and run the program. Record how the harness reports what
happens, and say precisely which instruction executes how many times. The test checks that your
description matches a run it performs itself, so “it hangs” does not pass.
python3 -m pytest tests/a_trap_with_nothing_else/test_problem_2_forever.py6.3 — Take the compiler’s help away.
Rewrite the handler without interrupt("machine"), as an ordinary function, and make the program
still print register_survived yes. You will need to say what mret is and where it has to go.
The test compiles your handler, boots it, and checks both the output and that the attribute is
genuinely absent.
python3 -m pytest tests/a_trap_with_nothing_else/test_problem_3_by_hand.pyWhere to go next¶
The privileged specification RISC-V International (n.d.) is the source for mtvec, mepc, mcause and mret,
and is worth opening at the machine-mode trap chapter now rather than later: it is short, and
everything in it is something this program did.
ch07 adds the thing that arrives without being asked for. ch16 is this mechanism again with a kernel around it, which is a good deal easier to read having built the middle of it.
- RISC-V International. (n.d.). The RISC-V Instruction Set Manual, Volume II: Privileged Architecture. https://riscv.org/technical/specifications/