← In Class CS 326 · Week 06 · Code and output Space/j/k sections · click to pin · n/p example · l copy link

Week 06 · Below Rust: assembly, unsafe, and no_std

Every program in examples/src/bin/ beside the output it printed, and every file in examples/broken/ beside what rustc said about it. Each colored, numbered section of code sits in the same row as the output it produced. Click an output line to light up the println! that printed it, or click a println! to find its output. Click a section's number to copy a link straight to that section, to paste where students will follow it. These programs are built for riscv64gc-unknown-none-elf and run under qemu-system-riscv64, not in a browser, so this page shows the output they printed there and cannot run them itself. To run one yourself, cd week06/examples and cargo run --bin it. If one ever hangs, Ctrl-A then x quits QEMU. On a phone the two columns do not fit, so a Code/Output switch shows one at a time.

The programs

ProgramThe one ideaThe line to point atSections
01registerssp and ra are ordinary registers you can readra = main + 0x…: the address right after the call4
02calling_assemblyextern "C" is the whole bridge, and the signature is a promisefn find_byte(s: *const u8, b: u8) -> *const u83
03lb_vs_lbulb sign-extends, lbu zero-extends0xe9: lb … = -23 lbu … = 2333
04repr_crepr(C) is what makes the assembly's offsets truespan_end = 0x80000007 after a field was inserted3
05add_wrapsthe hardware add drops the carry; Rust's + checksspan_end(start 0xffffffffffffffff, len 0x1) = 0x03
06prologue_and_s0a caller saves ra; a user of s0 saves s0keeper(rude) -> s1 = 0x633
07a_ret_that_lands_elsewhereret goes wherever ra says, on whatever stack sp sayslanding: nobody called me2
08mmio_clocka device register is an address, read through a raw pointerMTIMECMP.add(3) = 0x20040184
09volatile_mattersplain device accesses get merged, dropped, or hoistedthe UART received: B4
10unsafe_does_not_turn_offunsafe unlocks five operations and turns nothing offpub fn end_of(s: &Span) -> u644
11what_core_still_hasno_std removes the OS, not the languagewrite! into a 48-byte buffer on the stack4
12who_calls_mainno_main: the program names its own entry point_entry 0x800000003
13the_panic_handlerevery no_std program says what a panic does, oncefn panic(info: &PanicInfo) -> !2

Programs that must not compile

ErrorWhat rustc saysThe fix
E0133dereference of raw pointer is unsafe and requires unsafe function or blockunsafe { read_volatile(MTIME) }, or an unsafe fn
E0152found duplicate lang item panic_impldelete one
E0308mismatched types expected u32, found u64take the register's type, or say as u32
E0433cannot find type Vec in this scopea fixed array and a count, until there is a heap
E0463can't find crate for std#![no_std], with the !
E0601main function not found in crate e0601_forgot_no_main#![no_main], not an empty fn main
panic_handler_requirederror: #[panic_handler] function required, but not foundone #[panic_handler] returning !

Keys

Space j ↓ →next section (dims the rest)
Shift+Space k ↑ ←previous section
0–9jump to that section
clickpin an output line and the code that printed it (click again to unpin)
click a numbercopy a link to that section, or to the whole example from the number beside its title
lcopy a link to the pinned line, else the section in focus, else this example
Escunpin, then leave section focus, and drop it from the link so a reload does not bring it back
n pnext or previous example
rnext run, for programs run more than once
eopen or close the explanation (broken files) or the build warnings
+ -larger or smaller text (remembered)
oon a narrow screen, switch between the code and the output column

01 sp and ra are ordinary registers, and you can read them.

sp and ra are ordinary registers you can read · point at ra = main + 0x…: the address right after the call

$ cargo run --bin 01_registers
//! 01 — `sp` and `ra` are ordinary registers, and you can read them.▸ 13 more lines
//!
//! This program has no OS under it. QEMU jumped to 0x8000_0000, the runtime
//! pointed `sp` at a stack, and `main` was called like any function. So
//! what `main` sees in its registers is the whole story:
//!
//! sp the stack pointer: grows down, a multiple of 16 at every call
//! ra the return address: `call f` writes it, `ret` jumps to it
//! zero always 0: writes to it vanish
//!
//! `where_from` below is a leaf in assembly that hands back its own `ra`,
//! which is the address of the instruction right after the `call` in main.
//!
//! Run: cargo run --bin 01_registers
#![no_std]
#![no_main]

use core::arch::{asm, global_asm};
use week06::println;

week06::entry!(main);

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
week06::report_panic(info)
}

global_asm!(
r#"
.globl where_from
where_from: # returns its own ra: where the call came from
mv a0, ra
ret

.globl long_way_back
long_way_back: # the same `ret`, spelled out
li a0, 326
jalr zero, 0(ra)
"#
);

extern "C" {
fn where_from() -> usize;
fn long_way_back() -> u64;
static __stack_top: u8;
}

fn main() {
1 println!("== sp: the stack ==");
let sp: usize;
unsafe { asm!("mv {}, sp", out(reg) sp) };
let top = core::ptr::addr_of!(__stack_top) as usize;
println!("sp = {sp:#x}");
println!("stack top = {top:#x} (from link.ld)");
println!("in use = {} bytes, by the runtime and main", top - sp);
println!("sp % 16 = {}", sp % 16);

1== sp: the stack ==
sp = 0x800055d0
stack top = 0x80005660 (from link.ld)
in use = 144 bytes, by the runtime and main
sp % 16 = 0
2 println!("\n== ra: where a ret will go ==");
let main_at = main as fn() as usize;
let ra = unsafe { where_from() };
println!("main = {main_at:#x}");
println!("ra = {ra:#x} = main + {:#x}", ra - main_at);
println!("the call wrote ra; the leaf's ret jumped back to it");

2== ra: where a ret will go ==
main = 0x8000001e
ra = 0x800000ee = main + 0xd0
the call wrote ra; the leaf's ret jumped back to it
3 println!("\n== zero: writes vanish ==");
let z: u64;
unsafe { asm!("li zero, 99", "mv {}, zero", out(reg) z) };
println!("li zero, 99 then mv from zero: {z}");

3== zero: writes vanish ==
li zero, 99 then mv from zero: 0
4 println!("\n== ret is jalr zero, 0(ra) ==");
println!("long_way_back() = {}", unsafe { long_way_back() });
println!("it returned: jalr jumped to ra and threw the link away in zero");
4== ret is jalr zero, 0(ra) ==
long_way_back() = 326
it returned: jalr jumped to ra and threw the link away in zero
}
exit status 0

02 Rust calls assembly through extern "C", and the signature is a promise.

extern "C" is the whole bridge, and the signature is a promise · point at fn find_byte(s: *const u8, b: u8) -> *const u8

$ cargo run --bin 02_calling_assembly
//! 02 — Rust calls assembly through `extern "C"`, and the signature is a promise.▸ 17 more lines
//!
//! `global_asm!` puts `find_byte` in the program, `.globl` makes its name
//! visible to the linker, and the `extern "C"` block tells Rust how to call
//! it: arguments in a0, a1, …, the result in a0.
//!
//! Rust find_byte
//! a0 = s ──────────────────► 1: lbu t0, 0(a0) load a byte
//! a1 = b ──────────────────► beq t0, a1, 2f found
//! beqz t0, 2f the NUL
//! addi a0, a0, 1
//! j 1b
//! a0 ◄────────────────────── 2: ret
//!
//! A leaf: it calls nothing, so `ra` survives, and it uses only `a` and `t`
//! registers, so it needs no stack frame.
//!
//! Run: cargo run --bin 02_calling_assembly
#![no_std]
#![no_main]

use core::arch::global_asm;
use week06::println;

week06::entry!(main);

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
week06::report_panic(info)
}

global_asm!(
r#"
.globl find_byte
find_byte: # a0 = string, a1 = byte wanted
1: lbu t0, 0(a0) # load a byte
beq t0, a1, 2f # found: exit
beqz t0, 2f # the NUL: exit too
addi a0, a0, 1 # next byte
j 1b # back to the top
2: ret # a0 = match or NUL
"#
);

extern "C" {
/// SAFETY, for the caller: `s` points at readable bytes that end in a 0.
fn find_byte(s: *const u8, b: u8) -> *const u8;
}

/// How far into `s` find_byte stopped.
fn stop(s: &[u8], b: u8) -> usize {
let at = unsafe { find_byte(s.as_ptr(), b) };
at as usize - s.as_ptr() as usize
}

fn main() {
let s = b"kernel, meet user\0";

1 println!("== found ==");
println!("find_byte(s, b',') stopped at {}", stop(s, b','));
println!("find_byte(s, b'u') stopped at {}", stop(s, b'u'));
println!("the first match wins: s[{}] is the 'e' in \"kernel\"", stop(s, b'e'));

1== found ==
find_byte(s, b',') stopped at 6
find_byte(s, b'u') stopped at 13
the first match wins: s[1] is the 'e' in "kernel"
2 println!("\n== not there: it stops at the NUL ==");
let at = stop(s, b'z');
println!("find_byte(s, b'z') stopped at {at}, and s[{at}] = {}", s[at]);
println!("the 0 is the only way it knows where the bytes end");

2== not there: it stops at the NUL ==
find_byte(s, b'z') stopped at 17, and s[17] = 0
the 0 is the only way it knows where the bytes end
3 println!("\n== what crossed the boundary ==");
println!("a0 in: {:p} the pointer, 8 bytes", s.as_ptr());
println!("a1 in: {:#x} the byte, zero-extended into a register", b'u');
println!("a0 out: a pointer again, and Rust trusts that it is one");
3== what crossed the boundary ==
a0 in: 0x800020a3 the pointer, 8 bytes
a1 in: 0x75 the byte, zero-extended into a register
a0 out: a pointer again, and Rust trusts that it is one
}
exit status 0

03 lb sign-extends, lbu zero-extends, and below 0x80 you cannot tell.

lb sign-extends, lbu zero-extends · point at 0xe9: lb … = -23 lbu … = 233

$ cargo run --bin 03_lb_vs_lbu
//! 03 — `lb` sign-extends, `lbu` zero-extends, and below 0x80 you cannot tell.▸ 11 more lines
//!
//! A register is 64 bits; a byte is 8. Loading one byte has to decide what
//! goes in the other 56:
//!
//! byte 0xE9 = 1110_1001
//! lb → 0xFFFF_FFFF_FFFF_FFE9 top bit copied up: −23
//! lbu → 0x0000_0000_0000_00E9 zeros: 233
//!
//! Characters, sizes and lengths are unsigned, so they load with `lbu`.
//!
//! Run: cargo run --bin 03_lb_vs_lbu
#![no_std]
#![no_main]

use core::arch::global_asm;
use week06::println;

week06::entry!(main);

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
week06::report_panic(info)
}

global_asm!(
r#"
.globl load_lb
load_lb: # a0 = address; returns the byte, sign-extended
lb a0, 0(a0)
ret

.globl load_lbu
load_lbu: # a0 = address; returns the byte, zero-extended
lbu a0, 0(a0)
ret
"#
);

extern "C" {
fn load_lb(p: *const u8) -> u64;
fn load_lbu(p: *const u8) -> u64;
}

fn main() {
let bytes: [u8; 4] = [0x05, 0x60, 0xE9, 0xFF];

1 println!("== the same byte, loaded two ways ==");
for b in &bytes {
let (s, u) = unsafe { (load_lb(b), load_lbu(b)) };
println!("{:#04x}: lb {:#018x} = {:>4} lbu {:#018x} = {:>3}", b, s, s as i64, u, u);
}
println!("0x05 and 0x60 agree; 0xE9 and 0xFF do not: their top bit is 1");

1== the same byte, loaded two ways ==
0x05: lb 0x0000000000000005 = 5 lbu 0x0000000000000005 = 5
0x60: lb 0x0000000000000060 = 96 lbu 0x0000000000000060 = 96
0xe9: lb 0xffffffffffffffe9 = -23 lbu 0x00000000000000e9 = 233
0xff: lb 0xffffffffffffffff = -1 lbu 0x00000000000000ff = 255
0x05 and 0x60 agree; 0xE9 and 0xFF do not: their top bit is 1
2 println!("\n== a length byte, read with the wrong one ==");
let len: u8 = 200;
let wrong = unsafe { load_lb(&len) };
let right = unsafe { load_lbu(&len) };
println!("lbu says {} bytes", right);
println!("lb says {} as i64, which is {} as a usize", wrong as i64, wrong as usize);
println!("every byte of 0x80 or more comes out 256 too small");

2== a length byte, read with the wrong one ==
lbu says 200 bytes
lb says -56 as i64, which is 18446744073709551560 as a usize
every byte of 0x80 or more comes out 256 too small
3 println!("\n== Rust makes the same choice, by type ==");
let b = 0xE9u8;
println!("{:#x} as u8 -> i8 -> i64 = {}", b, b as i8 as i64);
println!("{:#x} as u8 -> u64 = {}", b, b as u64);
3== Rust makes the same choice, by type ==
0xe9 as u8 -> i8 -> i64 = -23
0xe9 as u8 -> u64 = 233
}
exit status 0

04 #[repr(C)] is what makes the assembly's offsets true.

repr(C) is what makes the assembly's offsets true · point at span_end = 0x80000007 after a field was inserted

$ cargo run --bin 04_repr_c
//! 04 — `#[repr(C)]` is what makes the assembly's offsets true.▸ 13 more lines
//!
//! `span_end` is handed a pointer in a0 and reads two fields out of memory at
//! fixed offsets. Nothing checks those offsets against the struct:
//!
//! #[repr(C)] struct Span { start: u64, len: u64 }
//!
//! a0 ─► +0 start ld t0, 0(a0)
//! +8 len ld t1, 8(a0)
//!
//! `repr(C)` promises declaration order and C's padding rules. Rust's default
//! layout promises nothing and may reorder fields to save space.
//!
//! Run: cargo run --bin 04_repr_c
#![no_std]
#![no_main]

use core::arch::global_asm;
use core::mem::{offset_of, size_of};
use week06::println;

week06::entry!(main);

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
week06::report_panic(info)
}

global_asm!(
r#"
.globl span_end
span_end: # a0 = *const Span
ld t0, 0(a0) # t0 = start
ld t1, 8(a0) # t1 = len
add a0, t0, t1 # a0 = start + len
ret
"#
);

#[repr(C)]
pub struct Span {
pub start: u64,
pub len: u64,
}

extern "C" {
fn span_end(s: *const Span) -> u64;
}

/// Three fields that do not fit neatly: C order, then Rust's.
#[repr(C)]
struct SlotC {
busy: u8,
addr: u64,
owner: u16,
}

struct SlotRust {
busy: u8,
addr: u64,
owner: u16,
}

/// A field inserted ahead of `len`. Still 8-byte aligned, still compiles.
#[repr(C)]
struct Tagged {
start: u64,
tag: u8,
len: u64,
}

fn main() {
1 println!("== span_end reads offsets 0 and 8 ==");
let s = Span { start: 0x8000_0000, len: 0x1000 };
println!("span_end(&Span {{ start: {:#x}, len: {:#x} }}) = {:#x}", s.start, s.len,
unsafe { span_end(&s) });
println!("offset_of!(Span, start) = {}", offset_of!(Span, start));
println!("offset_of!(Span, len) = {}", offset_of!(Span, len));

1== span_end reads offsets 0 and 8 ==
span_end(&Span { start: 0x80000000, len: 0x1000 }) = 0x80001000
offset_of!(Span, start) = 0
offset_of!(Span, len) = 8
2 println!("\n== repr(C) keeps order; the default may not ==");
println!("repr(C): busy at {}, addr at {}, owner at {}, size {}",
offset_of!(SlotC, busy), offset_of!(SlotC, addr), offset_of!(SlotC, owner),
size_of::<SlotC>());
println!("default: busy at {}, addr at {}, owner at {}, size {}",
offset_of!(SlotRust, busy), offset_of!(SlotRust, addr), offset_of!(SlotRust, owner),
size_of::<SlotRust>());
println!("C pads after busy so addr lands on 8; Rust moved busy to the end instead");

2== repr(C) keeps order; the default may not ==
repr(C): busy at 0, addr at 8, owner at 16, size 24
default: busy at 10, addr at 0, owner at 8, size 16
C pads after busy so addr lands on 8; Rust moved busy to the end instead
3 println!("\n== insert a field ahead of len ==");
// Zeroed first, so the padding after `tag` is 0 and the result is the same every run.
let mut t: Tagged = unsafe { core::mem::zeroed() };
(t.start, t.tag, t.len) = (0x8000_0000, 7, 0x1000);
let wrong = unsafe { span_end(&t as *const Tagged as *const Span) };
println!("offset_of!(Tagged, len) = {}, but span_end still reads 8", offset_of!(Tagged, len));
println!("span_end = {wrong:#x}; it added the tag byte and its padding, not len");
println!("the cast compiled, because a raw-pointer cast checks nothing");
3== insert a field ahead of len ==
offset_of!(Tagged, len) = 16, but span_end still reads 8
span_end = 0x80000007; it added the tag byte and its padding, not len
the cast compiled, because a raw-pointer cast checks nothing
}
exit status 0

05 add wraps without a word; Rust's + checks.

the hardware add drops the carry; Rust's + checks · point at span_end(start 0xffffffffffffffff, len 0x1) = 0x0

$ cargo run --bin 05_add_wraps
//! 05 — `add` wraps without a word; Rust's `+` checks.▸ 13 more lines
//!
//! The `add` inside `span_end` (program 04) keeps the low 64 bits of the sum
//! and drops the carry. Nothing traps, and no flag is left behind:
//!
//! 0xFFFF_FFFF_FFFF_FFFF + 1 = 1_0000_0000_0000_0000
//! ^ the 65th bit: gone
//! → 0
//!
//! Rust in a debug build checks every `+` and panics on overflow (program 13
//! shows the panic). When wrapping is what you mean, say so:
//! `wrapping_add`, or `checked_add` to get `None` instead.
//!
//! Run: cargo run --bin 05_add_wraps
#![no_std]
#![no_main]

use core::arch::global_asm;
use core::hint::black_box;
use week06::println;

week06::entry!(main);

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
week06::report_panic(info)
}

global_asm!(
r#"
.globl span_end
span_end: # a0 = *const Span
ld t0, 0(a0) # t0 = start
ld t1, 8(a0) # t1 = len
add a0, t0, t1 # a0 = start + len
ret
"#
);

#[repr(C)]
pub struct Span {
pub start: u64,
pub len: u64,
}

extern "C" {
fn span_end(s: *const Span) -> u64;
}

fn main() {
let big = black_box(u64::MAX);

1 println!("== the hardware add ==");
let near = Span { start: big - 0x10, len: 0x10 };
let over = Span { start: big, len: 1 };
println!("span_end(start {:#x}, len {:#x}) = {:#x}", near.start, near.len, unsafe { span_end(&near) });
println!("span_end(start {:#x}, len {:#x}) = {:#x}", over.start, over.len, unsafe { span_end(&over) });
println!("no trap, no flag: the carry out of bit 63 is simply dropped");

1== the hardware add ==
span_end(start 0xffffffffffffffef, len 0x10) = 0xffffffffffffffff
span_end(start 0xffffffffffffffff, len 0x1) = 0x0
no trap, no flag: the carry out of bit 63 is simply dropped
2 println!("\n== Rust, saying what it means ==");
println!("big.wrapping_add(1) = {:#x}", big.wrapping_add(1));
println!("big.checked_add(1) = {:?}", big.checked_add(1));
println!("big.checked_add(0) = {:?}", big.checked_add(0));
println!("big + 1 would panic here: 'attempt to add with overflow'");

2== Rust, saying what it means ==
big.wrapping_add(1) = 0x0
big.checked_add(1) = None
big.checked_add(0) = Some(18446744073709551615)
big + 1 would panic here: 'attempt to add with overflow'
3 println!("\n== the same bits, signed ==");
let edge = Span { start: black_box(i64::MAX as u64), len: 1 };
let sum = unsafe { span_end(&edge) };
println!("span_end(start i64::MAX, len 1) = {:#x} = {} as i64", sum, sum as i64);
println!("one add for both: signed or not is how you read the bits");
3== the same bits, signed ==
span_end(start i64::MAX, len 1) = 0x8000000000000000 = -9223372036854775808 as i64
one add for both: signed or not is how you read the bits
}
exit status 0

06 A function that calls saves ra; a function that uses s0 saves it.

a caller saves ra; a user of s0 saves s0 · point at keeper(rude) -> s1 = 0x63

$ cargo run --bin 06_prologue_and_s0
//! 06 — A function that calls saves `ra`; a function that uses `s0` saves it.▸ 14 more lines
//!
//! `framed` keeps `x` in `s0` across a call, so it owes its caller two
//! things: the caller's `s0`, and its own way home in `ra`. It claims 16
//! bytes of stack for them and hands `visit` a pointer to see them:
//!
//! ┌──────────────┐ ← sp before the prologue
//! sp + 8 │ saved ra │ the way back into main
//! sp + 0 │ saved s0 │ main's s0, owed back
//! └──────────────┘ ← sp after `addi sp, sp, -16`
//!
//! The last section runs the rule's failure: a callee that borrows `s1`
//! without saving it works fine, until its caller keeps something there.
//!
//! Run: cargo run --bin 06_prologue_and_s0
#![no_std]
#![no_main]

use core::arch::global_asm;
use week06::println;

week06::entry!(main);

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
week06::report_panic(info)
}

global_asm!(
r#"
.globl framed
framed: # a0 = x, a1 = visit; returns visit(x, frame) + x
addi sp, sp, -16 # claim 16 bytes; sp stays 16-byte aligned
sd ra, 8(sp) # we call, so save ra
sd s0, 0(sp) # we use s0, so save the caller's s0
mv s0, a0 # x lives in s0 across the call
mv t0, a1
mv a1, sp # let visit see this frame
jalr t0 # visit(x, frame): ra = the next instruction
add a0, a0, s0 # visit's answer + x
ld s0, 0(sp) # the epilogue: the prologue, backwards
ld ra, 8(sp)
addi sp, sp, 16
ret

.globl keeper
keeper: # a0 = callee; keeps 0x5151 in s1 across calling it
addi sp, sp, -16
sd ra, 8(sp)
sd s1, 0(sp)
li s1, 0x5151
jalr a0
mv a0, s1 # what s1 holds after the call
ld s1, 0(sp)
ld ra, 8(sp)
addi sp, sp, 16
ret

.globl polite
polite: # borrows s1, and gives it back
addi sp, sp, -16
sd s1, 0(sp)
li s1, 99
ld s1, 0(sp)
addi sp, sp, 16
ret

.globl rude
rude: # borrows s1, and keeps it
li s1, 99
ret
"#
);

extern "C" {
fn framed(x: u64, visit: extern "C" fn(u64, *const u64) -> u64) -> u64;
fn keeper(callee: unsafe extern "C" fn()) -> u64;
fn polite();
fn rude();
}

/// Called from inside `framed`'s frame; reads the two saved words.
extern "C" fn visit(x: u64, frame: *const u64) -> u64 {
let main_at = main as fn() as u64;
let (s0, ra) = unsafe { (*frame, *frame.add(1)) };
println!("frame at {frame:p}");
println!(" [sp + 0] saved s0 = {s0:#x}");
println!(" [sp + 8] saved ra = {ra:#x} = main + {:#x}", ra - main_at);
x * 10
}

fn main() {
1 println!("== the frame framed built ==");
let sp: u64;
unsafe { core::arch::asm!("mv {}, sp", out(reg) sp) };
println!("main's sp = {sp:#x}");
let r = unsafe { framed(4, visit) };

1== the frame framed built ==
main's sp = 0x80006790
frame at 0x80006780
[sp + 0] saved s0 = 0x80000d72
[sp + 8] saved ra = 0x800000b4 = main + 0x52
2 println!("\n== what the saves bought ==");
println!("framed(4, visit) = {r} = visit's 40 + the 4 kept in s0");
println!("visit ran Rust code with its own frames, and s0 still held 4");

2== what the saves bought ==
framed(4, visit) = 44 = visit's 40 + the 4 kept in s0
visit ran Rust code with its own frames, and s0 still held 4
3 println!("\n== a callee that skips the save ==");
println!("keeper(polite) -> s1 = {:#x}", unsafe { keeper(polite) });
println!("keeper(rude) -> s1 = {:#x}", unsafe { keeper(rude) });
println!("rude returns fine on its own. Only a caller keeping something in s1 notices,");
println!("and it notices later, somewhere else");
3== a callee that skips the save ==
keeper(polite) -> s1 = 0x5151
keeper(rude) -> s1 = 0x63
rude returns fine on its own. Only a caller keeping something in s1 notices,
and it notices later, somewhere else
}
exit status 0

07 ret goes wherever ra says, on whatever stack sp says.

ret goes wherever ra says, on whatever stack sp says · point at landing: nobody called me

$ cargo run --bin 07_a_ret_that_lands_elsewhere
//! 07 — `ret` goes wherever `ra` says, on whatever stack `sp` says.▸ 12 more lines
//!
//! `resume` loads a new `sp` and a new `ra`, then returns. It returns into a
//! function that never called it, running on a stack main never used:
//!
//! main ── resume(&r) ──► ld sp, 0(a0) sp = top of the second stack
//! ld ra, 8(a0) ra = landing
//! ret ──► landing() on the second stack
//!
//! It is a one-way door: nothing recorded where main was, so there is no
//! coming back. `landing` powers the machine off itself.
//!
//! Run: cargo run --bin 07_a_ret_that_lands_elsewhere
#![no_std]
#![no_main]

use core::arch::global_asm;
use core::ptr::addr_of_mut;
use week06::println;

week06::entry!(main);

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
week06::report_panic(info)
}

global_asm!(
r#"
.globl resume
resume: # a0 = *const Resume: sp at 0, ra at 8
ld sp, 0(a0) # the saved stack
ld ra, 8(a0) # the saved return address
ret # "return" into it
"#
);

#[repr(C)]
struct Resume {
sp: usize,
ra: usize,
}

extern "C" {
fn resume(r: *const Resume) -> !;
}

/// A second stack: 4 KiB, 16-byte aligned, as a RISC-V `sp` must be. It is
/// `static mut` because it will be written: `landing`'s frames live in it.
#[repr(C, align(16))]
struct Stack([u8; 4096]);
static mut SECOND: Stack = Stack([0; 4096]);

/// The second stack's lowest address and the address just past its top.
fn second_stack() -> (usize, usize) {
let lo = addr_of_mut!(SECOND) as usize;
(lo, lo + 4096)
}

extern "C" fn landing() -> ! {
let sp: usize;
unsafe { core::arch::asm!("mv {}, sp", out(reg) sp) };
let (lo, hi) = second_stack();
println!("landing: nobody called me, and sp = {sp:#x}");
println!("landing: that is inside the second stack: {}", lo <= sp && sp < hi);
println!("landing: main's frame is still there, but nothing points at it");
week06::exit(0)
}

fn main() {
let (lo, hi) = second_stack();
let r = Resume { sp: hi, ra: landing as *const () as usize };

1 println!("== two stacks ==");
let sp: usize;
unsafe { core::arch::asm!("mv {}, sp", out(reg) sp) };
println!("main runs with sp = {sp:#x}");
println!("the second stack is {lo:#x}..{hi:#x}");

1== two stacks ==
main runs with sp = 0x80006540
the second stack is 0x800015a0..0x800025a0
2 println!("\n== resume: load sp, load ra, ret ==");
println!("Resume {{ sp: {:#x}, ra: {:#x} = landing }}", r.sp, r.ra);
unsafe { resume(&r) }
2== resume: load sp, load ra, ret ==
Resume { sp: 0x800025a0, ra: 0x800000fe = landing }
landing: nobody called me, and sp = 0x80002570
landing: that is inside the second stack: true
landing: main's frame is still there, but nothing points at it
}
exit status 0

08 A device register is an address: a raw pointer reads it.

a device register is an address, read through a raw pointer · point at MTIMECMP.add(3) = 0x2004018

$ cargo run --bin 08_mmio_clock
//! 08 — A device register is an address: a raw pointer reads it.▸ 12 more lines
//!
//! QEMU's `virt` board maps its devices into the physical address space. Two
//! of them tell time, and neither needs a driver, only a pointer:
//!
//! 0x0010_1000 goldfish RTC wall-clock nanoseconds since 1970
//! 0x0200_4000 CLINT mtimecmp one u64 alarm per hart: base + 8 * hart
//! 0x0200_BFF8 CLINT mtime a counter, 10,000,000 ticks per second
//!
//! Building the pointer is safe. Reading through it is `unsafe`, because only
//! you know the address is a live device and not garbage.
//!
//! Run: cargo run --bin 08_mmio_clock
#![no_std]
#![no_main]

use core::ptr::read_volatile;
use week06::println;

week06::entry!(main);

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
week06::report_panic(info)
}

const MTIME: *const u64 = 0x0200_BFF8 as *const u64;
const MTIMECMP: *mut u64 = 0x0200_4000 as *mut u64; // one alarm per hart
const RTC: *const u32 = 0x0010_1000 as *const u32; // TIME_LOW, then TIME_HIGH

/// SAFETY, for the caller: this is QEMU's `virt`, where MTIME is the CLINT.
unsafe fn wait_until(deadline: u64) {
while read_volatile(MTIME) < deadline {}
}

// fold: days since 1970-01-01 → (year, month, day), from Howard Hinnant's algorithm▸ 11 more lines
fn civil(days: i64) -> (i64, u32, u32) {
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
(yoe + era * 400 + (m <= 2) as i64, m, d)
}

fn main() {
1 println!("== the counter moves on its own ==");
let (a, b) = unsafe { (read_volatile(MTIME), read_volatile(MTIME)) };
println!("mtime = {a}");
println!("mtime = {b} (+{} between two loads)", b - a);

1== the counter moves on its own ==
mtime = 2860
mtime = 2930 (+70 between two loads)
2 println!("\n== .add counts elements, not bytes ==");
for hart in 0..4 {
let p = unsafe { MTIMECMP.add(hart) };
println!("MTIMECMP.add({hart}) = {p:p}");
}
println!("each step is size_of::<u64>() = 8 bytes; nothing was dereferenced");

2== .add counts elements, not bytes ==
MTIMECMP.add(0) = 0x2004000
MTIMECMP.add(1) = 0x2004008
MTIMECMP.add(2) = 0x2004010
MTIMECMP.add(3) = 0x2004018
each step is size_of::<u64>() = 8 bytes; nothing was dereferenced
3 println!("\n== wait_until: poll the counter ==");
let start = unsafe { read_volatile(MTIME) };
unsafe { wait_until(start + 100_000) }; // 10 ms
let end = unsafe { read_volatile(MTIME) };
println!("asked for 100000 ticks (10 ms), waited {}", end - start);

3== wait_until: poll the counter ==
asked for 100000 ticks (10 ms), waited 100040
4 println!("\n== a second device: the wall clock ==");
let ns = unsafe { read_volatile(RTC) as u64 | (read_volatile(RTC.add(1)) as u64) << 32 };
let secs = ns / 1_000_000_000;
let (y, m, d) = civil((secs / 86_400) as i64);
let t = secs % 86_400;
println!("RTC = {ns} ns since 1970");
println!(" = {y}-{m:02}-{d:02} {:02}:{:02}:{:02} UTC", t / 3600, t / 60 % 60, t % 60);
println!("low word first: reading TIME_LOW latches TIME_HIGH, so the two halves match");
4== a second device: the wall clock ==
RTC = 1790690192012003000 ns since 1970
= 2026-09-29 13:56:32 UTC
low word first: reading TIME_LOW latches TIME_HIGH, so the two halves match
}
exit status 0

09 Without volatile, the compiler may merge, drop, or hoist device accesses.

plain device accesses get merged, dropped, or hoisted · point at the UART received: B

$ cargo run --bin 09_volatile_matters
//! 09 — Without `volatile`, the compiler may merge, drop, or hoist device accesses.▸ 16 more lines
//!
//! To the optimizer, a plain `*p = x` is a store to memory, and memory only
//! remembers the last store. A plain `*p` is a load, and memory does not
//! change unless this program changes it. Both are false for a device:
//!
//! plain: *UART = b'A'; *UART = b'B'; → one store: "B"
//! volatile: write_volatile ×2 → two stores: "AB"
//!
//! plain: loop { t = *MTIME; … } → one load, hoisted out
//! volatile: loop { read_volatile(MTIME) } → a load every time around
//!
//! This crate builds its dev profile at opt-level 1 (see Cargo.toml), so what
//! you see is what an optimized kernel would do. At opt-level 0 the plain
//! versions happen to work, and the bug waits for a release build.
//!
//! Run: cargo run --bin 09_volatile_matters
#![no_std]
#![no_main]

use core::ptr::{read_volatile, write_volatile};
use week06::{print, println};

week06::entry!(main);

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
week06::report_panic(info)
}

const UART: *mut u8 = 0x1000_0000 as *mut u8;
const MTIME: *const u64 = 0x0200_BFF8 as *const u64;

fn plain_pair() {
unsafe {
*UART = b'A';
*UART = b'B';
}
}

fn volatile_pair() {
unsafe {
write_volatile(UART, b'A');
write_volatile(UART, b'B');
}
}

/// How many different values `n` plain loads of the clock saw.
fn plain_distinct(n: u32) -> u32 {
let (mut seen, mut last) = (0, u64::MAX);
for _ in 0..n {
let t = unsafe { *MTIME };
if t != last {
seen += 1;
last = t;
}
}
seen
}

/// The same loop, with every load kept.
fn volatile_distinct(n: u32) -> u32 {
let (mut seen, mut last) = (0, u64::MAX);
for _ in 0..n {
let t = unsafe { read_volatile(MTIME) };
if t != last {
seen += 1;
last = t;
}
}
seen
}

fn main() {
1 println!("== two plain stores to the UART ==");
print!("the UART received: ");
plain_pair();
println!();
println!("the first store was dead to the optimizer: the second overwrites it");

1== two plain stores to the UART ==
the UART received: B
the first store was dead to the optimizer: the second overwrites it
2 println!("\n== two volatile stores ==");
print!("the UART received: ");
volatile_pair();
println!();
println!("both bytes, in order: each volatile store happens, exactly as written");

2== two volatile stores ==
the UART received: AB
both bytes, in order: each volatile store happens, exactly as written
3 println!("\n== 100,000 plain loads of mtime ==");
let n = plain_distinct(100_000);
println!("distinct values seen: {n}");
println!("the load was hoisted out of the loop: the clock was read once");

3== 100,000 plain loads of mtime ==
distinct values seen: 1
the load was hoisted out of the loop: the clock was read once
4 println!("\n== 100,000 volatile loads ==");
let n = volatile_distinct(100_000);
println!("distinct values seen: {n}");
println!("every trip around the loop asked the device again");
4== 100,000 volatile loads ==
distinct values seen: 13032
every trip around the loop asked the device again
}
exit status 0

10 unsafe unlocks five operations and turns nothing off.

unsafe unlocks five operations and turns nothing off · point at pub fn end_of(s: &Span) -> u64

$ cargo run --bin 10_unsafe_does_not_turn_off
cargo build: 1 warning e
warning: unnecessary `unsafe` block
|
67 | let got = unsafe { table.get(i) };
| ^^^^^^ unnecessary `unsafe` block
|
= note: `#[warn(unused_unsafe)]` (part of `#[warn(unused)]`) on by default
//! 10 — `unsafe` unlocks five operations and turns nothing off.▸ 13 more lines
//!
//! Inside an `unsafe` block you may dereference a raw pointer, call an
//! `unsafe fn` (every `extern "C"` function is one), touch a `static mut`,
//! implement an `unsafe trait`, and access a union field. That is the whole list. The
//! borrow checker, the type checker and bounds checks all still run.
//!
//! The usual shape is a small `unsafe` core inside a safe function whose
//! signature makes the promise the caller can no longer get wrong:
//!
//! pub fn end_of(s: &Span) -> u64 a &Span is live, aligned, 16 bytes
//! unsafe { span_end(s) } …so this call is always fine
//!
//! Run: cargo run --bin 10_unsafe_does_not_turn_off
#![no_std]
#![no_main]

use core::arch::global_asm;
use core::hint::black_box;
use week06::println;

week06::entry!(main);

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
week06::report_panic(info)
}

global_asm!(
r#"
.globl span_end
span_end: # a0 = *const Span
ld t0, 0(a0)
ld t1, 8(a0)
add a0, t0, t1
ret
"#
);

#[repr(C)]
pub struct Span {
pub start: u64,
pub len: u64,
}

extern "C" {
fn span_end(s: *const Span) -> u64;
}

/// Safe to call: the reference is the proof the assembly needs.
pub fn end_of(s: &Span) -> u64 {
// promise: s is live, aligned, and 16 bytes long
unsafe { span_end(s) }
}

static mut BOOTS: u32 = 0;

fn main() {
1 println!("== a safe wrapper ==");
let s = Span { start: 0x8020_0000, len: 0x2000 };
println!("end_of(&s) = {:#x} no unsafe at the call site", end_of(&s));
println!("the one unsafe block is inside end_of, where the promise is written");

1== a safe wrapper ==
end_of(&s) = 0x80202000 no unsafe at the call site
the one unsafe block is inside end_of, where the promise is written
2 println!("\n== bounds checks still run ==");
let table = [10u32, 20, 30];
let i = black_box(7);
let got = unsafe { table.get(i) };
println!("inside unsafe, table.get({i}) = {got:?}");
println!("rustc even warns that this block is unnecessary: press e to see it");
println!("table[{i}] would still panic; get_unchecked({i}) is the unsafe way to skip");
println!("the check, and with 7 it would read past the end: undefined behavior");

2== bounds checks still run ==
inside unsafe, table.get(7) = None
rustc even warns that this block is unnecessary: press e to see it
table[7] would still panic; get_unchecked(7) is the unsafe way to skip
the check, and with 7 it would read past the end: undefined behavior
3 println!("\n== a static mut needs unsafe for every touch ==");
unsafe {
BOOTS += 1;
BOOTS += 1;
}
let n = unsafe { BOOTS };
println!("BOOTS = {n}");
println!("the compiler cannot see who else touches a global, so each access is yours to justify");

3== a static mut needs unsafe for every touch ==
BOOTS = 2
the compiler cannot see who else touches a global, so each access is yours to justify
4 println!("\n== types and borrows still apply ==");
println!("broken/e0308_unsafe_keeps_types.rs: a u64 is still not a u32 inside unsafe");
println!("broken/e0133_raw_deref_needs_unsafe.rs: and outside it, *p does not compile at all");
4== types and borrows still apply ==
broken/e0308_unsafe_keeps_types.rs: a u64 is still not a u32 inside unsafe
broken/e0133_raw_deref_needs_unsafe.rs: and outside it, *p does not compile at all
}
exit status 0

11 #![no_std] takes away the OS, not the language.

no_std removes the OS, not the language · point at write! into a 48-byte buffer on the stack

$ cargo run --bin 11_what_core_still_has
//! 11 — `#![no_std]` takes away the OS, not the language.▸ 12 more lines
//!
//! `std` is three layers, and a kernel keeps the bottom one:
//!
//! std needs an OS println!, files, threads, HashMap
//! alloc needs a heap Box, Vec, String
//! core needs nothing Option, Result, slices, iterators, fmt, ptr
//!
//! Everything below runs with no OS, no heap and no allocator, on a stack
//! of 16 KiB. Even this program's `println!` is `core::fmt` writing bytes to
//! the UART; the runtime supplies the last step, and the rest is core's.
//!
//! Run: cargo run --bin 11_what_core_still_has
#![no_std]
#![no_main]

use core::fmt::{self, Write};
use core::mem::size_of;
use week06::println;

week06::entry!(main);

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
week06::report_panic(info)
}

/// Text formatted into a fixed array on the stack: `fmt::Write` for 48 bytes.
struct Line {
buf: [u8; 48],
len: usize,
}

impl Write for Line {
fn write_str(&mut self, s: &str) -> fmt::Result {
let room = &mut self.buf[self.len..];
if s.len() > room.len() {
return Err(fmt::Error);
}
room[..s.len()].copy_from_slice(s.as_bytes());
self.len += s.len();
Ok(())
}
}

fn main() {
1 println!("== slices, iterators, Option ==");
let ticks = [7u32, 3, 9, 3, 5];
println!("max = {:?}", ticks.iter().max());
println!("position = {:?}", ticks.iter().position(|&t| t == 9));
println!("sum of odd = {}", ticks.iter().filter(|&&t| t % 2 == 1).sum::<u32>());
println!("first > 8 = {:?}", ticks.iter().find(|&&t| t > 8));
println!("10 / 0 = {:?}", 10u32.checked_div(0));

1== slices, iterators, Option ==
max = Some(9)
position = Some(2)
sum of odd = 27
first > 8 = Some(9)
10 / 0 = None
2 println!("\n== formatting, with no heap ==");
let mut line = Line { buf: [0; 48], len: 0 };
let r = write!(line, "hart {} at {:#010x}, {:>5} ticks", 0, 0x8000_0000u32, 1234);
println!("{:?}: {:?}", r, core::str::from_utf8(&line.buf[..line.len]).unwrap());
println!("{} of 48 bytes used", line.len);
let r = write!(line, " and this sentence does not fit in what is left");
println!("{r:?}: a full buffer is an error value, not a reallocation");

2== formatting, with no heap ==
Ok(()): "hart 0 at 0x80000000, 1234 ticks"
33 of 48 bytes used
Err(Error): a full buffer is an error value, not a reallocation
3 println!("\n== sizes are compile-time facts ==");
println!("size_of::<Option<&u8>>() = {} the null pointer is None", size_of::<Option<&u8>>());
println!("size_of::<Option<u8>>() = {} a u8 has no spare value", size_of::<Option<u8>>());
println!("size_of::<&[u8]>() = {} pointer + length", size_of::<&[u8]>());

3== sizes are compile-time facts ==
size_of::<Option<&u8>>() = 8 the null pointer is None
size_of::<Option<u8>>() = 2 a u8 has no spare value
size_of::<&[u8]>() = 16 pointer + length
4 println!("\n== what is not here ==");
println!("Vec, String and Box live in alloc, which needs an allocator: this program has none");
println!("broken/e0433_vec_needs_alloc.rs is what rustc says when you reach for one anyway");
4== what is not here ==
Vec, String and Box live in alloc, which needs an allocator: this program has none
broken/e0433_vec_needs_alloc.rs is what rustc says when you reach for one anyway
}
exit status 0

12 #![no_main]: nobody generates a main, so something else must be first.

no_main: the program names its own entry point · point at _entry 0x80000000

$ cargo run --bin 12_who_calls_main
//! 12 — `#![no_main]`: nobody generates a `main`, so something else must be first.▸ 12 more lines
//!
//! On a host, the OS starts a program at a C runtime entry that std provides,
//! which calls `main`. Here there is no OS and no runtime from std, so the
//! program says where to begin itself:
//!
//! link.ld ENTRY(_entry), and .entry first at 0x8000_0000
//! entry!(main) emits _entry: sp = __stack_top, then __week06_start
//! __week06_start calls main(), then powers off
//!
//! `main` is an ordinary function here. It could be called anything.
//!
//! Run: cargo run --bin 12_who_calls_main
#![no_std]
#![no_main]

use core::arch::asm;
use core::ptr::addr_of;
use week06::println;

week06::entry!(main);

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
week06::report_panic(info)
}

extern "C" {
fn _entry();
static etext: u8;
static end: u8;
static __stack_top: u8;
}

fn main() {
// Read before main calls anything: a call would overwrite ra.
let ra: usize;
unsafe { asm!("mv {}, ra", out(reg) ra) };

1 println!("== where this program lives ==");
let start = __week06_start as *const () as usize;
println!("_entry {:#x} where the boot ROM's jump landed", _entry as *const () as usize);
println!("__week06_start {start:#x}");
println!("main {:#x}", main as fn() as usize);
println!("etext {:p} end of code", addr_of!(etext));
println!("end {:p} end of data", addr_of!(end));
println!("__stack_top {:p} 16 KiB above end", addr_of!(__stack_top));

1== where this program lives ==
_entry 0x80000000 where the boot ROM's jump landed
__week06_start 0x800001c4
main 0x8000001e
etext 0x80001000 end of code
end 0x800016a0 end of data
__stack_top 0x800056a0 16 KiB above end
2 println!("\n== who called main ==");
println!("main's ra = {ra:#x} = __week06_start + {:#x}", ra - start);
println!("no generated main, no C runtime: a function in this program called it");

2== who called main ==
main's ra = 0x800001d6 = __week06_start + 0x12
no generated main, no C runtime: a function in this program called it
3 println!("\n== after main returns ==");
println!("__week06_start calls week06::exit(0), and the test finisher ends QEMU");
println!("without it there is nothing to return to: the CPU would run into whatever is next");
3== after main returns ==
__week06_start calls week06::exit(0), and the test finisher ends QEMU
without it there is nothing to return to: the CPU would run into whatever is next
}
exit status 0

13 Every no_std program says what a panic does, exactly once.

every no_std program says what a panic does, once · point at fn panic(info: &PanicInfo) -> !

$ cargo run --bin 13_the_panic_handler
//! 13 — Every `no_std` program says what a panic does, exactly once.▸ 14 more lines
//!
//! With std, a panic prints a message and unwinds or aborts, and std wrote
//! that code. Without it, the program must supply one function with this
//! signature, marked `#[panic_handler]`:
//!
//! fn(&PanicInfo) -> ! ! : it never returns, because there is
//! nothing sensible to return to
//!
//! Every other program here hands the report to `week06::report_panic`. This
//! one writes the handler out: print where and why, then power off with a
//! failure status. rv6's does the same with `OSLINGS:FAIL (panic)`.
//!
//! Run: cargo run --bin 13_the_panic_handler
//! Exit: 1 (the handler powers QEMU off with a failure status, on purpose)
#![no_std]
#![no_main]

use core::hint::black_box;
use core::panic::PanicInfo;
use week06::println;

week06::entry!(main);

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("[panic handler] a panic, and this function is all that is left");
if let Some(at) = info.location() {
println!("[panic handler] at {}:{}", at.file(), at.line());
}
println!("[panic handler] why {}", info.message());
week06::exit(1)
}

fn add_ticks(a: u8, b: u8) -> u8 {
a + b // overflow checks are on in a dev build
}

fn main() {
1 println!("== an ordinary call ==");
println!("add_ticks(200, 50) = {}", add_ticks(200, black_box(50)));

1== an ordinary call ==
add_ticks(200, 50) = 250
2 println!("\n== one that overflows ==");
println!("add_ticks(200, 60): 260 does not fit in a u8");
let n = add_ticks(200, black_box(60));
println!("unreachable: {n}");
2== one that overflows ==
add_ticks(200, 60): 260 does not fit in a u8
[panic handler] a panic, and this function is all that is left
[panic handler] at src/bin/13_the_panic_handler.rs:36
[panic handler] why attempt to add with overflow
}
exit status 1

E0133 dereference of raw pointer is unsafe and requires unsafe function or block

reading through a raw pointer outside unsafe · fix: unsafe { read_volatile(MTIME) }, or an unsafe fn

$ ./show-errors.sh e0133
// error[E0133]: dereference of raw pointer is unsafe and requires unsafe
// function or block
//
// Making the pointer was safe: `0x0200_BFF8 as *const u64` is only a number
// with a type. Reading through it is the step the compiler cannot check,
// because only you know that address is QEMU's clock and not garbage.
//
// FIX 1: `unsafe { read_volatile(MTIME) }`: unsafe, because you vouch for the
// address; volatile, because it is a device (program 09).
// FIX 2: `unsafe fn now()`, which moves the promise to every caller instead.
#![no_std]
#![no_main]

use core::panic::PanicInfo;

const MTIME: *const u64 = 0x0200_BFF8 as *const u64;

fn now() -> u64 {
*MTIME
}

#[panic_handler]
fn panic(_: &PanicInfo) -> ! {
halt()
}

fn halt() -> ! {
loop {
core::hint::spin_loop();
}
}

#[no_mangle]
pub extern "C" fn _entry() -> ! {
let _ = now();
halt()
}
Why, and 2 fixes e

Making the pointer was safe: 0x0200_BFF8 as *const u64 is only a number with a type. Reading through it is the step the compiler cannot check, because only you know that address is QEMU's clock and not garbage.

  1. unsafe { read_volatile(MTIME) }: unsafe, because you vouch for the address; volatile, because it is a device (program 09).
  2. unsafe fn now(), which moves the promise to every caller instead.
$ rustc --edition 2021 --target riscv64gc-unknown-none-elf --emit=metadata -o /dev/null e0133_raw_deref_needs_unsafe.rs
error[E0133]: dereference of raw pointer is unsafe and requires unsafe function or block
|
19 | *MTIME
| ^^^^^^ dereference of raw pointer
|
= note: raw pointers may be null, dangling or unaligned; they can violate aliasing rules and cause data races: all of these are undefined behavior
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0133`.
exit status 1

E0152 found duplicate lang item panic_impl

"exactly one" holds across the whole program · fix: delete one

$ ./show-errors.sh e0152
// error[E0152]: found duplicate lang item `panic_impl`
//
// "Exactly one" is enforced across the whole program: this crate and every
// crate it links. `#[panic_handler]` fills a slot the compiler calls
// `panic_impl`, and a second one has nowhere to go. It would fail the same
// way if a library you depend on already brought its own handler.
//
// FIX 1: delete one of them.
// FIX 2: if the library's handler is the one you want, write none yourself.
#![no_std]
#![no_main]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_: &PanicInfo) -> ! {
halt()
}

#[panic_handler]
fn also_panic(_: &PanicInfo) -> ! {
halt()
}

fn halt() -> ! {
loop {
core::hint::spin_loop();
}
}

#[no_mangle]
pub extern "C" fn _entry() -> ! {
halt()
}
Why, and 2 fixes e

"Exactly one" is enforced across the whole program: this crate and every crate it links. #[panic_handler] fills a slot the compiler calls panic_impl, and a second one has nowhere to go. It would fail the same way if a library you depend on already brought its own handler.

  1. delete one of them.
  2. if the library's handler is the one you want, write none yourself.
$ rustc --edition 2021 --target riscv64gc-unknown-none-elf --emit=metadata -o /dev/null e0152_two_panic_handlers.rs
error[E0152]: found duplicate lang item `panic_impl`
|
21 | / fn also_panic(_: &PanicInfo) -> ! {
22 | | halt()
23 | | }
| |_^
|
note: the lang item is first defined here
|
16 | / fn panic(_: &PanicInfo) -> ! {
17 | | halt()
18 | | }
| |_^
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0152`.
exit status 1

E0308 mismatched types expected u32, found u64

a u64 register read into a u32 · fix: take the register's type, or say as u32

$ ./show-errors.sh e0308
// error[E0308]: mismatched types
// expected `u32`, found `u64`
//
// `unsafe` lets you dereference the pointer. It does not change what the
// pointer points at: `*const u64` still yields a u64, and a u64 still does not
// fit a u32 without saying how. Type checks, borrow checks and bounds checks
// all run inside the block, exactly as outside it.
//
// FIX 1: `let ticks: u64 = …`: take the type the device register has.
// FIX 2: `… as u32`, if you mean to keep only the low 32 bits.
#![no_std]
#![no_main]

use core::panic::PanicInfo;
use core::ptr::read_volatile;

const MTIME: *const u64 = 0x0200_BFF8 as *const u64;

#[panic_handler]
fn panic(_: &PanicInfo) -> ! {
halt()
}

fn halt() -> ! {
loop {
core::hint::spin_loop();
}
}

#[no_mangle]
pub extern "C" fn _entry() -> ! {
let ticks: u32 = unsafe { read_volatile(MTIME) };
let _ = ticks;
halt()
}
Why, and 2 fixes e

unsafe lets you dereference the pointer. It does not change what the pointer points at: *const u64 still yields a u64, and a u64 still does not fit a u32 without saying how. Type checks, borrow checks and bounds checks all run inside the block, exactly as outside it.

  1. let ticks: u64 = …: take the type the device register has.
  2. … as u32, if you mean to keep only the low 32 bits.
$ rustc --edition 2021 --target riscv64gc-unknown-none-elf --emit=metadata -o /dev/null e0308_unsafe_keeps_types.rs
error[E0308]: mismatched types
|
32 | let ticks: u32 = unsafe { read_volatile(MTIME) };
| ------------- ^^^^^ expected `*const u32`, found `*const u64`
| |
| arguments to this function are incorrect
|
= note: expected raw pointer `*const u32`
found raw pointer `*const u64`
note: function defined here
--> /rustc/59807616e1fa2540724bfbac14d7976d7e4a3860/library/core/src/ptr/mod.rs:2080:14
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
exit status 1

E0433 cannot find type Vec in this scope

Vec is in alloc, not core · fix: a fixed array and a count, until there is a heap

$ ./show-errors.sh e0433
// error[E0433]: cannot find type `Vec` in this scope
//
// `Vec` is not in core. It lives in `alloc`, because growing needs a heap,
// and a heap needs an allocator that somebody wrote. `#![no_std]` took away
// std's prelude, and with it `Vec`, `String` and `Box`.
//
// FIX 1: a fixed-size array, `[u64; 16]`, and a count of how many are used:
// what a kernel does until it has an allocator (38k gives rv6 one).
// FIX 2: `extern crate alloc;` plus a `#[global_allocator]`, and then
// `alloc::vec::Vec`. Only once there is memory to hand out.
#![no_std]
#![no_main]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_: &PanicInfo) -> ! {
halt()
}

fn halt() -> ! {
loop {
core::hint::spin_loop();
}
}

#[no_mangle]
pub extern "C" fn _entry() -> ! {
let mut log = Vec::new();
log.push(1u64);
halt()
}
Why, and 2 fixes e

Vec is not in core. It lives in alloc, because growing needs a heap, and a heap needs an allocator that somebody wrote. #![no_std] took away std's prelude, and with it Vec, String and Box.

  1. a fixed-size array, [u64; 16], and a count of how many are used: what a kernel does until it has an allocator (38k gives rv6 one).
  2. extern crate alloc; plus a #[global_allocator], and then alloc::vec::Vec. Only once there is memory to hand out.
$ rustc --edition 2021 --target riscv64gc-unknown-none-elf --emit=metadata -o /dev/null e0433_vec_needs_alloc.rs
error[E0433]: cannot find type `Vec` in this scope
|
29 | let mut log = Vec::new();
| ^^^ use of undeclared type `Vec`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0433`.
exit status 1

E0463 can't find crate for std

the target has no std to link · fix: #![no_std], with the !

$ ./show-errors.sh e0463
// error[E0463]: can't find crate for `std`
//
// Every crate links std unless it says otherwise, and this target has no std
// to link: `riscv64gc-unknown-none-elf` means no operating system, so no
// files, threads or heap for std to be built on. `#![no_main]` alone is not
// enough; the program still asked for std by not refusing it.
//
// FIX 1: `#![no_std]` as the first line, with the `!`: an inner attribute
// speaks for the whole crate. `#[no_std]` without it decorates only the
// item below, and rustc warns you to add the exclamation mark.
#![no_main]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_: &PanicInfo) -> ! {
halt()
}

fn halt() -> ! {
loop {
core::hint::spin_loop();
}
}

#[no_mangle]
pub extern "C" fn _entry() -> ! {
halt()
}
Why, and the fix e

Every crate links std unless it says otherwise, and this target has no std to link: riscv64gc-unknown-none-elf means no operating system, so no files, threads or heap for std to be built on. #![no_main] alone is not enough; the program still asked for std by not refusing it.

  1. #![no_std] as the first line, with the !: an inner attribute speaks for the whole crate. #[no_std] without it decorates only the item below, and rustc warns you to add the exclamation mark.
$ rustc --edition 2021 --target riscv64gc-unknown-none-elf --emit=metadata -o /dev/null e0463_forgot_no_std.rs
error[E0463]: can't find crate for `std`
|
= note: the `riscv64gc-unknown-none-elf` target may not support the standard library
= note: `std` is required by `<unknown>` because it does not declare `#![no_std]`
error: cannot resolve a prelude import
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0463`.
exit status 1

E0601 main function not found in crate e0601_forgot_no_main

rustc wants a main nobody will call · fix: #![no_main], not an empty fn main

$ ./show-errors.sh e0601
// error[E0601]: `main` function not found in crate `e0601_forgot_no_main`
//
// Without `#![no_main]`, rustc treats this as an ordinary program: it wants a
// `fn main` and would generate the code that calls it. Nothing would. QEMU
// jumps to `_entry`, the linker script's ENTRY, and nothing runs before it.
// Adding an empty `fn main() {}` silences rustc and fixes nothing.
//
// FIX 1: `#![no_main]` beside `#![no_std]`: "I name my own entry point."
#![no_std]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_: &PanicInfo) -> ! {
halt()
}

fn halt() -> ! {
loop {
core::hint::spin_loop();
}
}

#[no_mangle]
pub extern "C" fn _entry() -> ! {
halt()
}
Why, and the fix e

Without #![no_main], rustc treats this as an ordinary program: it wants a fn main and would generate the code that calls it. Nothing would. QEMU jumps to _entry, the linker script's ENTRY, and nothing runs before it. Adding an empty fn main() {} silences rustc and fixes nothing.

  1. #![no_main] beside #![no_std]: "I name my own entry point."
$ rustc --edition 2021 --target riscv64gc-unknown-none-elf --emit=metadata -o /dev/null e0601_forgot_no_main.rs
error[E0601]: `main` function not found in crate `e0601_forgot_no_main`
|
27 | }
| ^ consider adding a `main` function to `e0601_forgot_no_main.rs`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0601`.
exit status 1

panic_handler_required error: #[panic_handler] function required, but not found

a no_std program with nowhere for a panic to go · fix: one #[panic_handler] returning !

$ ./show-errors.sh panic_handler_required
// error: `#[panic_handler]` function required, but not found
//
// Indexing, overflow and `unwrap` can all panic, and std is where a panic
// normally goes. With `#![no_std]` there is nowhere, so the program must say:
// exactly one function in the whole program, marked `#[panic_handler]`. This
// error has no code; the whole message is in its one line.
//
// FIX 1: add one: `#[panic_handler] fn panic(info: &PanicInfo) -> ! { … }`.
// It must not return: `!` is the type of "never comes back".
// FIX 2: in examples/, `week06::report_panic(info)` as its body, as every
// program there does.
#![no_std]
#![no_main]

fn halt() -> ! {
loop {
core::hint::spin_loop();
}
}

#[no_mangle]
pub extern "C" fn _entry() -> ! {
halt()
}
Why, and 2 fixes e

Indexing, overflow and unwrap can all panic, and std is where a panic normally goes. With #![no_std] there is nowhere, so the program must say: exactly one function in the whole program, marked #[panic_handler]. This error has no code; the whole message is in its one line.

  1. add one: #[panic_handler] fn panic(info: &PanicInfo) -> ! { … }. It must not return: ! is the type of "never comes back".
  2. in examples/, week06::report_panic(info) as its body, as every program there does.
$ rustc --edition 2021 --target riscv64gc-unknown-none-elf --emit=metadata -o /dev/null panic_handler_required.rs
error: `#[panic_handler]` function required, but not found
error: aborting due to 1 previous error
exit status 1