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
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.
| Program | The one idea | The line to point at | Sections | |
|---|---|---|---|---|
| 01 | registers | sp and ra are ordinary registers you can read | ra = main + 0x…: the address right after the call | 4 |
| 02 | calling_assembly | extern "C" is the whole bridge, and the signature is a promise | fn find_byte(s: *const u8, b: u8) -> *const u8 | 3 |
| 03 | lb_vs_lbu | lb sign-extends, lbu zero-extends | 0xe9: lb … = -23 lbu … = 233 | 3 |
| 04 | repr_c | repr(C) is what makes the assembly's offsets true | span_end = 0x80000007 after a field was inserted | 3 |
| 05 | add_wraps | the hardware add drops the carry; Rust's + checks | span_end(start 0xffffffffffffffff, len 0x1) = 0x0 | 3 |
| 06 | prologue_and_s0 | a caller saves ra; a user of s0 saves s0 | keeper(rude) -> s1 = 0x63 | 3 |
| 07 | a_ret_that_lands_elsewhere | ret goes wherever ra says, on whatever stack sp says | landing: nobody called me | 2 |
| 08 | mmio_clock | a device register is an address, read through a raw pointer | MTIMECMP.add(3) = 0x2004018 | 4 |
| 09 | volatile_matters | plain device accesses get merged, dropped, or hoisted | the UART received: B | 4 |
| 10 | unsafe_does_not_turn_off | unsafe unlocks five operations and turns nothing off | pub fn end_of(s: &Span) -> u64 | 4 |
| 11 | what_core_still_has | no_std removes the OS, not the language | write! into a 48-byte buffer on the stack | 4 |
| 12 | who_calls_main | no_main: the program names its own entry point | _entry 0x80000000 | 3 |
| 13 | the_panic_handler | every no_std program says what a panic does, once | fn panic(info: &PanicInfo) -> ! | 2 |
| Error | What rustc says | The fix |
|---|---|---|
| E0133 | dereference of raw pointer is unsafe and requires unsafe function or block | unsafe { read_volatile(MTIME) }, or an unsafe fn |
| E0152 | found duplicate lang item panic_impl | delete one |
| E0308 | mismatched types expected u32, found u64 | take the register's type, or say as u32 |
| E0433 | cannot find type Vec in this scope | a fixed array and a count, until there is a heap |
| E0463 | can't find crate for std | #![no_std], with the ! |
| E0601 | main function not found in crate e0601_forgot_no_main | #![no_main], not an empty fn main |
| panic_handler_required | error: #[panic_handler] function required, but not found | one #[panic_handler] returning ! |
| Space j ↓ → | next section (dims the rest) |
| Shift+Space k ↑ ← | previous section |
| 0–9 | jump to that section |
| click | pin an output line and the code that printed it (click again to unpin) |
| click a number | copy a link to that section, or to the whole example from the number beside its title |
| l | copy a link to the pinned line, else the section in focus, else this example |
| Esc | unpin, then leave section focus, and drop it from the link so a reload does not bring it back |
| n p | next or previous example |
| r | next run, for programs run more than once |
| e | open or close the explanation (broken files) or the build warnings |
| + - | larger or smaller text (remembered) |
| o | on a narrow screen, switch between the code and the output column |
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
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
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
#[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
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
ra; a function that uses s0 saves it.a caller saves ra; a user of s0 saves s0 · point at keeper(rude) -> s1 = 0x63
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
a device register is an address, read through a raw pointer · point at MTIMECMP.add(3) = 0x2004018
volatile, the compiler may merge, drop, or hoist device accesses.plain device accesses get merged, dropped, or hoisted · point at the UART received: B
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
#![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
#![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
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) -> !
reading through a raw pointer outside unsafe · fix: unsafe { read_volatile(MTIME) }, or an unsafe fn
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.
unsafe { read_volatile(MTIME) }: unsafe, because you vouch for the address; volatile, because it is a device (program 09).unsafe fn now(), which moves the promise to every caller instead.panic_impl"exactly one" holds across the whole program · fix: delete one
"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.
u32, found u64a u64 register read into a u32 · fix: take the register's type, or say as u32
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.
let ticks: u64 = …: take the type the device register has.… as u32, if you mean to keep only the low 32 bits.Vec in this scopeVec is in alloc, not core · fix: a fixed array and a count, until there is a heap
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.
[u64; 16], and a count of how many are used: what a kernel does until it has an allocator (38k gives rv6 one).extern crate alloc; plus a #[global_allocator], and then alloc::vec::Vec. Only once there is memory to hand out.stdthe target has no std to link · fix: #![no_std], with the !
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.
#![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.main function not found in crate e0601_forgot_no_mainrustc wants a main nobody will call · fix: #![no_main], not an empty fn 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.
#![no_main] beside #![no_std]: "I name my own entry point."#[panic_handler] function required, but not founda no_std program with nowhere for a panic to go · fix: one #[panic_handler] returning !
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.
#[panic_handler] fn panic(info: &PanicInfo) -> ! { … }. It must not return: ! is the type of "never comes back".week06::report_panic(info) as its body, as every program there does.