Skip to content

Prep: The Assembly Bridge — 20a

Session: Thursday Oct 1, 1h45 · Exercises: 20a_asm_bridge · Prep time: ~30 min · Lecture: Week 6 · Below 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
Week 6 · This week, then Thursday · 20a The assembly bridge from Registers have jobs through The calling convention 3 min
Week 6 · Loads, stores, and local labels and The bridge 3 min
Week 6 · A ret that lands somewhere else and Running under QEMU 2 min
Week 6 · For the exam: lb versus lbu, stack frames, register traces. Not needed today; on Midterm 1 2 min
RISC-V guide: Registers, Calling convention, Local numeric labels 7 min
Dev Setup: oslings doctor, and run it · QEMU guide: how to get out of QEMU 6 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

  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 — the tests that pass earn their share — then finish it at a make-up session (office hours, on the class network) for three quarters, or anywhere else for half, and submit again.

If you finish early

Work Week 6 · Problem 1: Sign extension by hand, Problem 3: Offsets by hand and Problem 4: Trace a resume, then start reading Friday's prep page. Chapter 2 of the xv6 book shows where the assembly lives in a real kernel.