Prep: The Assembly Bridge — 20a¶
Session: Thursday Oct 1, 1h45 · Exercises: 20a_asm_bridge · Prep time: ~45 min · Lecture: RISC-V Registers and Calling Assembly from Rust
QEMU deadline is today
First exercise that boots on the emulated machine; no host-side fallback.
Run oslings doctor before class. Anything red: see the
troubleshooting table
or get help in office hours now.
What you will build¶
Three short routines in RISC-V assembly, called from Rust through
global_asm! and extern "C", running bare-metal under QEMU:
a three-operand add that proves the calling convention works as you expect,
a byte-copy loop built from loads, stores, and local numeric labels,
and a baby context switch that saves four registers into one #[repr(C)]
struct, loads them from another, and returns into a different thread of
execution on a different stack. A given harness boots, calls each routine with
its edge cases (zero bytes, a context switched to itself), and the test
passes when the serial console prints OSLINGS:PASS.
Concepts you need¶
- ABI names and the register file — Lecture §2 · RISC-V guide: Registers
- Caller-saved vs. callee-saved — Lecture §3, §3.1 · RISC-V guide: caller/callee split
- The calling convention:
a0–a7,ra,sp— Lecture §4 · RISC-V guide: Calling convention - Loads, stores, and
1b/2flabels — Lecture §5.1, §5.2 · RISC-V guide: Local numeric labels global_asm!,extern "C", and#[repr(C)]— Lecture §6.1, §6.2, §6.3 · RISC-V guide: Assembly inside Rust- A
retthat lands somewhere else — Lecture §8.3, §8.4
Read before class¶
| What | Time |
|---|---|
| Lecture §2–§4 | 15 min |
| Lecture §5–§6 | 12 min |
| Lecture §8.3–§8.4 | 5 min |
| RISC-V guide: Registers, Calling convention, Local labels | 5 min |
Dev Setup §7: run oslings doctor · QEMU guide: how to get out of QEMU |
8 min |
Mental model¶
The whole bridge fits in one leaf function that fills a two-field struct:
#[repr(C)]
pub struct Pair { pub lo: u64, pub hi: u64 } // lo at offset 0, hi at 8
core::arch::global_asm!(r#"
.globl fill_pair
fill_pair: # a0 = *mut Pair, a1 = lo, a2 = hi
sd a1, 0(a0)
sd a2, 8(a0)
ret # jalr zero, 0(ra)
"#);
extern "C" { pub fn fill_pair(p: *mut Pair, lo: u64, hi: u64); }
The arguments arrive in a0–a2 because extern "C" promised they would.
A leaf touches no callee-saved register and never overwrites ra, so nothing
is saved. The 0 and 8 are literals welded into the sd instructions; they
match the struct only because #[repr(C)] forbids reordering fields. A
kernel's context switch is this pattern scaled up: stores at hard-coded
offsets, loads from another struct, and a ret to whatever ra was just
loaded. Get one offset wrong, or load before you store, and nothing faults;
the machine runs perfectly, in the wrong place.
Check yourself¶
- A function you call uses
s3andt2as scratch. Which must it save and restore, and why?Answer
Onlys3: it is callee-saved, so the caller trusts it to survive.t2is caller-saved; the caller already assumed it was gone. retis a pseudo-instruction. What does it expand to, and what does that say about a function that overwritesrabefore returning?Answer
jalr zero, 0(ra). It jumps to whateverraholds now, so a function that loads a newrareturns somewhere else. That is the whole mechanism of a context switch.- Assembly does
sd a1, 8(a0)into a struct that is not#[repr(C)]. What can go wrong?Answer
The compiler may reorder fields, so offset 8 may be a different field. The 8 is baked into the instruction; the store silently corrupts the wrong field and no tool complains.
What "done" looks like¶
oslings run is green, then oslings submit before you leave. Not green? Submit anyway (substantial credit), then finish by Thursday 11:59 pm and submit again.
If you finish early¶
Work lecture Practice Problem 3 and Problem 4, then start reading Friday's prep page. Chapter 2 of the xv6 book shows where the assembly lives in a real kernel.