Skip to content

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

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 a0a2 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

  1. A function you call uses s3 and t2 as scratch. Which must it save and restore, and why?
    AnswerOnly s3: it is callee-saved, so the caller trusts it to survive. t2 is caller-saved; the caller already assumed it was gone.
  2. ret is a pseudo-instruction. What does it expand to, and what does that say about a function that overwrites ra before returning?
    Answerjalr zero, 0(ra). It jumps to whatever ra holds now, so a function that loads a new ra returns somewhere else. That is the whole mechanism of a context switch.
  3. Assembly does sd a1, 8(a0) into a struct that is not #[repr(C)]. What can go wrong?
    AnswerThe 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.