Exam Preparation¶
This page is for the two weeks before each exam. It says what is examinable, what the questions actually look like, and how to study a kernel on paper when every habit you have built this semester involves a running QEMU. Read it once early — it changes how you take notes while you work — and again while you revise. For the material itself, see Key Concepts; for the constants, the Cheatsheet.
The three exams¶
| Exam | When | Covers | Weight |
|---|---|---|---|
| Midterm 1 | Thu Oct 15, in class, full period | Module 1 (00r–21r) plus 30k–33k |
15% |
| Midterm 2 | Thu Nov 19, in class, full period | 34k–48k — processes through user mode |
15% |
| Final | Dec 11–17, registrar's assigned slot | cumulative, weighted toward 49k–53k (pipes at the design level only) |
20% |
The final's exact date, time, and room are set by the registrar, not by the course — check the official final exam schedule. Midterms fall on Thursdays, and there is no Friday exercise session in an exam week. Later exams are cumulative in concepts: anything earlier may reappear as a building block, but no question rests only on old material. You must average C or better across the three to pass the course.
Format¶
- On paper. No laptop, so no guess-and-check: an answer you cannot justify is an answer you cannot fix.
- Closed book, with exactly one permitted reference: the Cheatsheet, which you may print and annotate. Nothing else — no notes, no printed source listings.
- No electronic devices, including phones, watches, and calculators. All the arithmetic is hex shifting and masking, done by hand.
- You will not write a long program from memory. You will read code, trace it, decode bit layouts, order steps, and explain why something is arranged the way it is.
Because the cheatsheet is permitted, no question rewards memorizing a constant that is printed on it. Questions reward knowing what to do with the constant.
Shape 1 — trace the registers¶
Worked example.
proc_yieldcallsswtch(&p.context, &SCHED_CTX)(usermode.rs:365). Givea0,a1,ra, andspat each stage, and say where the finalretgoes.
| Stage | a0 |
a1 |
ra |
sp |
|---|---|---|---|---|
entry to swtch |
&p.context |
&SCHED_CTX |
into proc_yield, just after the call |
p's kernel stack |
after the 14 sds |
unchanged | unchanged | unchanged | unchanged — 14 words have been stored into p.context |
after the 14 lds |
unchanged | unchanged | SCHED_CTX.ra |
SCHED_CTX.sp — the scheduler's stack |
at ret |
— | — | — | jumps to SCHED_CTX.ra |
The ret lands inside scheduler, on the instruction after its call to
swtch (usermode.rs:297) — a different function from the one that called
this swtch. That is the whole trick of a context switch, and it is genuinely
disorienting the first time: a function returns to a caller it never had.
Notice what is not saved. a0 and a1 are read as pointers and never
modified; no t or a register is saved or restored. Context has exactly fourteen fields — ra, sp,
s0–s11 (swtch.rs:5) — because the calling convention already forced the
caller to spill anything it cared about in the caller-saved registers before
making the call. #[repr(C)] is on the struct because the assembly hardcodes
those byte offsets.
A syscall trace is the same shape with more moving parts. write(1, buf, 5)
leaves ulib with a7 = 16, a0 = 1, a1 = &buf, a2 = 5, then ecall:
| Step | Where | What holds what |
|---|---|---|
the ecall |
hardware | sepc ← address of the ecall; scause ← 8; sstatus.SPP ← 0 (the trap came from U-mode); pc ← stvec = uservec |
csrrw a0, sscratch, a0 |
uservec |
a0 ← TRAPFRAME (0x3F_FFFF_E000); sscratch ← the user's a0 |
31 sds |
uservec |
every user register parked at a fixed offset: a0 at 112, a7 at 168 (usermode.rs:33) |
ld sp, 8(a0) / ld t0, 16(a0) / ld t1, 0(a0) |
uservec |
kernel stack top, usertrap's address, the kernel's satp |
csrw satp, t1 … jr t0 |
uservec |
page table swapped mid-instruction-stream; only works because the trampoline is mapped at the same virtual address in both tables |
scause == 8 |
usertrap |
tf.epc = sepc + 4, so the ecall is not re-executed |
dispatch(tf.a7, tf.a0, tf.a1, tf.a2) |
syscall.rs:33 |
the syscall number and three arguments, read from the trapframe |
tf.a0 = ret |
usermode.rs:408 |
the return value, planted in the a0 the user will wake up holding |
csrrw a0, sscratch, a0 … sret |
userret |
user a0 restored; pc ← sepc |
Full credit is naming the register, giving its value, and saying what forces it.
"a0 is the trapframe" is half an answer; "a0 is the trapframe, because
sscratch was loaded with TRAPFRAME before entering user mode and csrrw
swaps them" is the whole one.
Shape 2 — decode the bits¶
An Sv39 page table entry:
63 54 53 10 9 8 7 6 5 4 3 2 1 0
+------------+------------------------------------+---+-+-+-+-+-+-+-+-+
| reserved | PPN (44 bits) |RSW|D|A|G|U|X|W|R|V|
+------------+------------------------------------+---+-+-+-+-+-+-+-+-+
Worked example. What is
0x2008_041B?
- Flags = low 10 bits =
0x41B & 0x3FF=0x01B=0b0001_1011→V(1),R(2),X(8),U(16).Wis clear. - PPN =
0x2008_041B >> 10=0x8_0201. - Physical address = PPN
<< 12=0x8020_1000. - Verdict: a user text page — readable, executable, not writable,
reachable from user mode. Exactly what
load_segmentinstalls (vm.rs:228:PTE_R | PTE_X | PTE_U).
Two things trip people up every year. First, a PTE holds a page number, not
an address: the >> 12 / << 10 pair in Pte::new (vm.rs:31) is the entire
encoding, and the ten low bits are why they do not cancel. Second, a PTE with
V set and R, W, X all clear is not a leaf — it is a pointer to the
next level, which is what makes the walk loop terminate correctly
(vm.rs:56).
The same decoding applies to satp. 0x8000_0000_0008_0005: mode field (bits
63:60) = 8 = Sv39; the low 44 bits are the root table's PPN = 0x8_0005, so
the root page table sits at 0x8000_5000 (make_satp, vm.rs:106). Mode 0
means paging off — which is what start.rs:37 writes before mret.
And to scause, where the top bit separates interrupts from exceptions:
scause |
Meaning | Handled at |
|---|---|---|
8 |
ecall from U-mode — a system call |
usermode.rs:399 |
3 |
breakpoint | trap.rs:75 |
0x8000_…_0001 |
supervisor software interrupt — the forwarded timer tick | trap.rs:58 |
0x8000_…_0009 |
supervisor external interrupt — a device via the PLIC | trap.rs:66 |
12 / 13 / 15 |
instruction / load / store page fault | usermode.rs:428, kills the process |
Worked example. Translate virtual address
0x0001_0FF8.
The index formula is px(level, va) = (va >> (12 + 9 * level)) & 0x1FF
(vm.rs:44):
| Field | Bits | Value |
|---|---|---|
| level-2 index | 38:30 | 0 |
| level-1 index | 29:21 | 0 |
| level-0 index | 20:12 | 0x10 = 16 |
| page offset | 11:0 | 0xFF8 |
So the walk reads entry 0 of the root, entry 0 of the middle table, and entry
16 of the leaf table; the physical address is that leaf's PPN << 12, plus
0xFF8. Sanity check your answer against the map: USER_STACK is
16 * 4096 = 0x1_0000 and USER_STACK_TOP is 0x1_1000
(memlayout.rs:72–75), so index 16 is the stack page and 0xFF8 is its top word.
Write the arithmetic down — the index and the offset each earn credit, so a
dropped carry costs a line, not the question.
Shape 3 — order the steps¶
Worked example. Here are the six calls in
kinit(main.rs:87), scrambled. Put them in order and name the constraint that fixes each.
| # | Step | What forces its position |
|---|---|---|
| 1 | uart::init() |
printing must work before anything can fail; it is plain MMIO, needing no allocator and no MMU |
| 2 | kalloc::init() |
builds the free list from the linker's end symbol up to PHYSTOP (kalloc.rs:21); everything below allocates |
| 3 | vm::kvminithart(vm::kvmmake()) |
kvmmake allocates every page-table page through walk (vm.rs:62), so it must follow kalloc; the csrw satp is followed immediately by sfence.vma (vm.rs:177), and the kernel identity-maps itself so the next instruction fetch still resolves |
| 4 | proc::init() |
clears the fixed process table; free to move, but must precede any process creation |
| 5 | trap::init() |
stvec must hold kernelvec before any trap is possible — and definitely before interrupts are enabled |
| 6 | fs::FS.lock().init() |
a fixed inode table (fs.rs:79); its only real constraint is "before anything opens a file" |
Part of the answer is admitting which steps are genuinely interchangeable. Steps 4 and 6 could swap; steps 2 and 3 could not. Say so, and say why.
The interesting constraints live just after kinit, at main.rs:119:
console::init() enables the UART's receive interrupt, programs the PLIC, and
sets sie.SEIE; only then does trap::intr_on() set the global sstatus.SIE.
Reverse those two and the first keystroke traps before the PLIC can say which
device caused it.
How to prepare¶
- Reread your own code. You wrote it, so you will recover it faster than
anything you merely read. Midterm 1:
02r,04r,20a,32k,33k. Midterm 2:35k,37k,39k,43k,48k. Final:49k,50k,51k,52k. - Redraw the diagrams from memory, then check them: the Sv39 address split
and PTE layout; the free list threaded through the free pages themselves;
_entry→start→mret→kmain; the double switch between a process andSCHED_CTX;ecall→uservec→usertrap→usertrapret→userret→sret; the PLIC's claim/complete handshake. If you can draw it blank, you know it. - Do the practice set on paper, before looking at the solutions. Reading a worked solution feels like learning and mostly is not. Sit with a blank page and a timer, get it wrong, then read.
- Practice with the printed cheatsheet in front of you, so that on the day you already know where the PTE table is rather than hunting for it.
- Narrate one path out loud, end to end — a keystroke to a character on
screen, or
forkthroughexectowait. If you can tell the story without stalling, you can answer the long question whatever it turns out to be.
What is not examinable¶
Line numbers, OSlings CLI flags, cargo invocations, QEMU command lines, and
exact Rust API signatures. Structure is what is tested: "the trapframe parks
a7 at a fixed offset, so the kernel can read the syscall number after every
user register is saved" is an answer. The number 168 is on the cheatsheet.