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

Week 07 · From reset to page tables: boot, the free list, and Sv39

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 week07/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
01the_boot_romat reset, six instructions in ROM hand the hart to 0x8000_0000[t0 + 24] = 0x80000000 loaded into t0, then jr t04
02where_the_linker_put_itthe linker script gives every byte an address, and end names the first one past the image&end as *const u8 as usize = 0x80003…3
03the_boot_stackthe stack is 16 KiB of plain RAM: sp starts at the top, and nothing guards the bottomFLOOR[63], just below end, now holds … = dig + 0x124
04reset_to_rusteach step of the boot needs the one before it, and each left evidenceall three match: true. Nothing between the ROM and main wrote them3
05page_number_and_offsetan address is a page number and an offset>> 12 beside & 0xFFF for five real addresses3
06why_one_page_sizeone block size: every request costs whole pages4097 bytes -> 2 pages … 4095 unused3
07a_page_holds_an_addressa free page has room for the address of another pageP holds Q's address, and Q itself was never touched4
08where_free_memory_startsfree memory starts at the first whole page above the stackadd 0xFFF, then clear the low 12 bits4
09bits_by_handshift, mask, extract, pack; nothing checks that a field fitspack_date(46, 10, 32) = 0x5D60: month 11, day 04
10split_the_addressan Sv39 address is three 9-bit indices over a 12-bit offsetva 0x20_00A0_35F0 into 128, 5, 3, 0x5F04
11pte_by_handa PTE is a page number at bit 10, above ten bits of flags| V R W 0x2048_D0074
12satp_reads_zerosatp names the root table, and here it reads 0: paging is offcsrr satp = 0x03
13w_xor_xa page may be writable or executable, never bothanswer() = 42, after a store rewrote its code4

Programs that must not compile

ErrorWhat rustc saysThe fix
E0080attempt to shift left by 60_i32, which would overflowwiden before the shift: (SV39 as u64) << 60
E0133use of extern static is unsafe and requires unsafe function or blockunsafe { &end as *const u8 as usize }, or the safe addr_of!(end)
E0308mismatched types expected bool, found u64if pte & V != 0: Rust's & binds tighter than !=
E0308mismatched types expected usize, found u64convert where the type changes, as usize
E0369cannot add usize to *mut u8page.wrapping_add(PGSIZE), or do the arithmetic on usize
E0606casting &u8 as usize is invalid&end as *const u8 as usize, or addr_of!(end) as usize
E0614type usize cannot be dereferencedcast first: *(p as *mut usize) = q
end_is_not_a_consterror: pointers cannot be cast to integers during const evalwork it out at run time: only the linker knows it
tilde_is_not_an_operatorerror: ~ cannot be used as a unary operator!0xFFF: Rust's bitwise NOT is !

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 At reset, six instructions in ROM hand the hart to 0x8000_0000.

at reset, six instructions in ROM hand the hart to 0x8000_0000 · point at [t0 + 24] = 0x80000000 loaded into t0, then jr t0

$ cargo run --bin 01_the_boot_rom
//! 01 — At reset, six instructions in ROM hand the hart to 0x8000_0000.▸ 15 more lines
//!
//! QEMU's `virt` machine starts one hart at pc = 0x1000, in machine mode,
//! with paging off. With `-bios none` there is no firmware. A boot ROM runs
//! six instructions, and the last one jumps to RAM, where QEMU copied this
//! program. The ROM is memory like any other, so a load can read it:
//!
//! 0x1000 six instructions auipc, addi, csrr, ld, ld, jr
//! 0x1018 a word where the jr goes: 0x8000_0000
//! 0x1020 a word the device tree, a description of the board
//! 0x1028 a table for firmware, which -bios none leaves out
//!
//! QEMU writes those words into the ROM at startup. The last section follows
//! the device-tree word and finds the lecture's memory map there.
//!
//! Run: cargo run --bin 01_the_boot_rom
#![no_std]
#![no_main]

use core::arch::asm;
use core::ptr::read_volatile;
use week07::{print, println};

week07::entry!(main);

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

const ROM: usize = 0x1000;

fn rom_word(offset: usize) -> u32 {
// SAFETY: `virt` maps its boot ROM at 0x1000, and a load cannot change it.
unsafe { read_volatile((ROM + offset) as *const u32) }
}

fn rom_dword(offset: usize) -> u64 {
// SAFETY: as above; the ROM's data words are 8-byte aligned.
unsafe { read_volatile((ROM + offset) as *const u64) }
}

// fold: decodes just the five instruction shapes the ROM uses▸ 19 more lines
fn decode(at: usize, w: u32) {
const REG: [&str; 32] = [
"zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1", "a0", "a1", "a2", "a3",
"a4", "a5", "a6", "a7", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "s10", "s11",
"t3", "t4", "t5", "t6",
];
let rd = REG[(w >> 7 & 31) as usize];
let rs1 = REG[(w >> 15 & 31) as usize];
let imm = (w as i32) >> 20;
print!("{at:#06x} {w:08x} ");
match (w & 0x7f, w >> 12 & 7) {
(0x17, _) => println!("auipc {rd}, {:#x}", w >> 12),
(0x13, 0) => println!("addi {rd}, {rs1}, {imm}"),
(0x73, 2) if w >> 20 == 0xf14 => println!("csrr {rd}, mhartid"),
(0x03, 3) => println!("ld {rd}, {imm}({rs1})"),
(0x67, 0) if rd == "zero" && imm == 0 => println!("jr {rs1}"),
_ => println!("(not one of the five)"),
}
}

/// Four bytes of the device tree. Everything in it is big-endian.
fn be32(at: usize) -> u32 {
// SAFETY: `at` is inside the device tree the ROM points at.
u32::from_be(unsafe { read_volatile(at as *const u32) })
}

// fold: finds the `reg` property (base, size) of the first node named prefix@…▸ 30 more lines
fn reg_of(dtb: usize, prefix: &str) -> (u64, u64) {
let cstr = |at: usize| unsafe { core::ffi::CStr::from_ptr(at as *const _) }.to_str().unwrap();
let be64 = |at: usize| (be32(at) as u64) << 32 | be32(at + 4) as u64;
let strings = dtb + be32(dtb + 12) as usize;
let mut p = dtb + be32(dtb + 8) as usize;
let mut here = false;
loop {
let token = be32(p);
p += 4;
match token {
1 => {
// FDT_BEGIN_NODE: the node's name, NUL-terminated, padded to 4
let name = cstr(p);
here = name.starts_with(prefix) && name[prefix.len()..].starts_with('@');
p = (p + name.len() + 4) & !3;
}
3 => {
// FDT_PROP: the value's length, where its name is, the value
let len = be32(p) as usize;
if here && cstr(strings + be32(p + 4) as usize) == "reg" {
return (be64(p + 8), be64(p + 16));
}
p = (p + 8 + len + 3) & !3;
}
2 => here = false, // FDT_END_NODE
4 => {} // FDT_NOP
_ => panic!("no node named {prefix}@ in the device tree"),
}
}
}

fn main() {
1 println!("== the hart, as reset left it ==");
let (hart, misa, mtvec): (usize, usize, usize);
unsafe {
asm!("csrr {}, mhartid", out(reg) hart);
asm!("csrr {}, misa", out(reg) misa);
asm!("csrr {}, mtvec", out(reg) mtvec);
}
println!("mhartid = {hart} this hart's ID; -smp 1 starts only this one");
println!("mtvec = {mtvec:#x} a fault would jump to address 0: no handler is set");
print!("misa = {misa:#x}: RV{} with ", 16 << (misa >> 62));
for bit in 0..26 {
if misa >> bit & 1 == 1 {
print!("{}", (b'A' + bit as u8) as char);
}
}
println!();
println!("IMAFD (plus Zicsr, Zifencei) is the g in riscv64gc, and C is the c");
println!("S and U: supervisor and user mode. H: hypervisor");
println!("reading mhartid, misa and mtvec did not trap: this is machine mode");

1== the hart, as reset left it ==
mhartid = 0 this hart's ID; -smp 1 starts only this one
mtvec = 0x0 a fault would jump to address 0: no handler is set
misa = 0x80000000001411ad: RV64 with ACDFHIMSU
IMAFD (plus Zicsr, Zifencei) is the g in riscv64gc, and C is the c
S and U: supervisor and user mode. H: hypervisor
reading mhartid, misa and mtvec did not trap: this is machine mode
2 println!("\n== the six instructions at 0x1000 ==");
for i in 0..6 {
decode(ROM + 4 * i, rom_word(4 * i));
}
println!("auipc t0, 0x0 sets t0 to its own address, 0x1000; the rest count from there");

2== the six instructions at 0x1000 ==
0x1000 00000297 auipc t0, 0x0
0x1004 02828613 addi a2, t0, 40
0x1008 f1402573 csrr a0, mhartid
0x100c 0202b583 ld a1, 32(t0)
0x1010 0182b283 ld t0, 24(t0)
0x1014 00028067 jr t0
auipc t0, 0x0 sets t0 to its own address, 0x1000; the rest count from there
3 println!("\n== the words the ROM loads ==");
let target = rom_dword(24);
let dtb = rom_dword(32) as usize;
println!("[t0 + 24] = {target:#x} loaded into t0, then jr t0");
println!(" the RAM base, where QEMU copied this program");
println!("[t0 + 32] = {dtb:#x} loaded into a1: the device tree");
println!(" magic {:#x}, {} bytes long", be32(dtb), be32(dtb + 4));
let magic = rom_word(40).to_le_bytes();
let magic = core::str::from_utf8(&magic).unwrap();
println!("[t0 + 40] = {magic:?} where a2 points: a table for OpenSBI, unused here");

3== the words the ROM loads ==
[t0 + 24] = 0x80000000 loaded into t0, then jr t0
the RAM base, where QEMU copied this program
[t0 + 32] = 0x87e00000 loaded into a1: the device tree
magic 0xd00dfeed, 5028 bytes long
[t0 + 40] = "OSBI" where a2 points: a table for OpenSBI, unused here
4 println!("\n== the board, as its device tree describes it ==");
let map = [
("test", "the test finisher"),
("clint", "the CLINT: the timer"),
("plic", "the PLIC: routes device interrupts"),
("serial", "the UART"),
("memory", "RAM"),
];
for (node, what) in map {
let (base, size) = reg_of(dtb, node);
println!("{node:<7} {base:#010x} {size:#10x} bytes {what}");
}
let (ram, ram_size) = reg_of(dtb, "memory");
let phystop = (ram + ram_size) as usize;
println!("RAM is {} MiB and ends at {phystop:#x}", ram_size >> 20);
println!("the tree itself sits {} MiB below that end", (phystop - dtb) >> 20);
println!("rv6 never reads the tree: it hardcodes these addresses, so it boots on virt only");
4== the board, as its device tree describes it ==
test 0x00100000 0x1000 bytes the test finisher
clint 0x02000000 0x10000 bytes the CLINT: the timer
plic 0x0c000000 0x600000 bytes the PLIC: routes device interrupts
serial 0x10000000 0x100 bytes the UART
memory 0x80000000 0x8000000 bytes RAM
RAM is 128 MiB and ends at 0x88000000
the tree itself sits 2 MiB below that end
rv6 never reads the tree: it hardcodes these addresses, so it boots on virt only
}
exit status 0

02 The linker script gives every byte an address, and end names the first one past the image.

the linker script gives every byte an address, and end names the first one past the image · point at &end as *const u8 as usize = 0x80003…

$ cargo run --bin 02_where_the_linker_put_it
//! 02 — The linker script gives every byte an address, and `end` names the first one past the image.▸ 16 more lines
//!
//! link.ld starts the location counter at the RAM base and lays the image out
//! in order. Each item below lands in one of these, by what kind of item it is:
//!
//! 0x8000_0000 .entry _entry, listed first so the ROM's jump lands on it
//! .text the rest of the code, then padding to a page: etext
//! .rodata string literals and read-only statics
//! .data writable statics that start with a value
//! .bss writable statics that start as zeros
//! end the first address past the image
//! __stack_top 16 KiB higher: this runtime's stack
//!
//! Rust sees a linker symbol as an extern static. Its byte means nothing; its
//! address is the answer.
//!
//! Run: cargo run --bin 02_where_the_linker_put_it
#![no_std]
#![no_main]

use core::ptr::addr_of;
use week07::println;

week07::entry!(main);

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

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

const GREETING: &str = "hello from .rodata";
static mut COUNT: u64 = 326; // mut, so it needs writable memory: .data
static mut TABLE: [u64; 512] = [0; 512]; // all zeros: .bss

const PHYSTOP: usize = 0x8800_0000; // the end of RAM, from program 01

fn main() {
1 println!("== one item of each kind, in address order ==");
let entry = _entry as *const () as usize;
let etext_at = addr_of!(etext) as usize;
let table = addr_of!(TABLE) as usize;
let end_at = addr_of!(end) as usize;
let top = addr_of!(__stack_top) as usize;
let items = [
("_entry", entry, ".entry: first, where the ROM jumps"),
("main", main as fn() as usize, ".text"),
("etext", etext_at, "the end of the code, rounded up to a page"),
("GREETING", GREETING.as_ptr() as usize, ".rodata: a string literal's bytes"),
("COUNT", addr_of!(COUNT) as usize, ".data: a static mut that starts at 326"),
("TABLE", table, ".bss: 512 u64s that start as zeros"),
("end", end_at, "the first address past the image"),
("__stack_top", top, "the top of the 16 KiB stack"),
];
for (name, at, what) in items {
println!("{name:<12} {at:#x} {what}");
}
let in_order = items.windows(2).all(|w| w[0].1 < w[1].1);
println!("in increasing order: {in_order}, as link.ld lists them");
println!("_entry is first because link.ld lists *(.entry) first");
println!("ENTRY(_entry) only fills in the ELF header, which the ROM never reads");

1== one item of each kind, in address order ==
_entry 0x80000000 .entry: first, where the ROM jumps
main 0x80000014 .text
etext 0x80002000 the end of the code, rounded up to a page
GREETING 0x80002221 .rodata: a string literal's bytes
COUNT 0x80002ce0 .data: a static mut that starts at 326
TABLE 0x80002cf0 .bss: 512 u64s that start as zeros
end 0x80003cf0 the first address past the image
__stack_top 0x80007cf0 the top of the 16 KiB stack
in increasing order: true, as link.ld lists them
_entry is first because link.ld lists *(.entry) first
ENTRY(_entry) only fills in the ELF header, which the ROM never reads
2 println!("\n== how big each part is ==");
let code = etext_at - entry;
println!("code etext - _entry {code:>9} bytes: {} pages, the last one padded", code / 4096);
println!("data end - etext {:>9} bytes: everything after the code", end_at - etext_at);
println!("stack __stack_top - end {:>9} bytes: reserved by link.ld", top - end_at);
println!("free PHYSTOP - __stack_top {:>9} bytes: this program never touches them", PHYSTOP - top);

2== how big each part is ==
code etext - _entry 8192 bytes: 2 pages, the last one padded
data end - etext 7408 bytes: everything after the code
stack __stack_top - end 16384 bytes: reserved by link.ld
free PHYSTOP - __stack_top 134185744 bytes: this program never touches them
3 println!("\n== end is an address, not a value ==");
let first_free = unsafe { &end as *const u8 as usize };
let byte = unsafe { end };
println!("&end as *const u8 as usize = {first_free:#x}");
println!("in rv6 free memory starts here. This runtime puts its stack here instead,");
println!("so here free memory starts at __stack_top (program 08)");
println!("the byte at end holds {byte}: it means nothing (here, the stack's lowest byte)");
let size = core::mem::size_of::<[u64; 512]>();
println!("end - TABLE = {}, and TABLE is {size} bytes: nothing follows it", first_free - table);
println!("make TABLE bigger and end moves up with it: a symbol, not a constant");
3== end is an address, not a value ==
&end as *const u8 as usize = 0x80003cf0
in rv6 free memory starts here. This runtime puts its stack here instead,
so here free memory starts at __stack_top (program 08)
the byte at end holds 0: it means nothing (here, the stack's lowest byte)
end - TABLE = 4096, and TABLE is 4096 bytes: nothing follows it
make TABLE bigger and end moves up with it: a symbol, not a constant
}
exit status 0

03 The stack is 16 KiB of plain RAM: sp starts at the top, and nothing guards the bottom.

the stack is 16 KiB of plain RAM: sp starts at the top, and nothing guards the bottom · point at FLOOR[63], just below end, now holds … = dig + 0x12

$ cargo run --bin 03_the_boot_stack
//! 03 — The stack is 16 KiB of plain RAM: `sp` starts at the top, and nothing guards the bottom.▸ 16 more lines
//!
//! Before any Rust ran, the runtime's `_entry` pointed `sp` at `__stack_top`.
//! Every call since has moved `sp` down, and every return has moved it back:
//!
//! __stack_top ← sp started here, the high end
//! │ the runtime's frame, then main's, then each callee's
//! ▼
//! end ← the low end: the stack's own lowest byte
//! .bss ← below it, somebody else's data
//!
//! rv6 keeps its boot stack as a static array in the image, below `end`,
//! because 32k hands out the pages above `end`. This runtime hands out
//! nothing, so link.ld puts the stack above `end`. Either way `sp` starts at
//! the high end.
//!
//! Run: cargo run --bin 03_the_boot_stack
#![no_std]
#![no_main]

use core::arch::asm;
use core::hint::black_box;
use core::ptr::{addr_of, addr_of_mut, read_volatile, write_volatile};
use week07::println;

week07::entry!(main);

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

extern "C" {
static end: u8;
static __stack_top: u8;
}

const PAINT: u64 = 0x5a5a_5a5a_5a5a_5a5a;

/// The last thing in .bss, so it ends where the stack begins: at `end`.
static mut FLOOR: [u64; 64] = [0; 64];

#[inline(always)]
fn sp() -> usize {
let sp;
unsafe { asm!("mv {}, sp", out(reg) sp, options(nomem, nostack)) };
sp
}

/// Records `sp` at each depth. The work after the call keeps every frame alive.
#[inline(never)]
fn descend(depth: usize, trail: &mut [usize]) {
trail[depth] = sp();
if depth + 1 < trail.len() {
descend(depth + 1, trail);
}
black_box(depth);
}

/// Calls itself until `sp` is below `target`, then reports the deepest `sp`.
#[inline(never)]
fn dig(target: usize) -> usize {
let here = sp();
if here < target {
return here;
}
black_box(dig(target))
}

// fold: fills the unused stack, from `end` up to just below the caller, with PAINT▸ 8 more lines
#[inline(never)]
fn paint() {
let mut at = addr_of!(end) as usize;
while at < sp() - 64 {
unsafe { write_volatile(at as *mut u64, PAINT) };
at += 8;
}
}

// fold: the lowest stack address written since paint(): the first word that changed▸ 8 more lines
#[inline(never)]
fn lowest_touched() -> usize {
let mut at = addr_of!(end) as usize;
while unsafe { read_volatile(at as *const u64) } == PAINT {
at += 8;
}
at
}

fn main() {
1 println!("== where sp starts ==");
let top = addr_of!(__stack_top) as usize;
let bottom = addr_of!(end) as usize;
let here = sp();
println!("__stack_top = {top:#x} the high end, where _entry pointed sp");
println!("end = {bottom:#x} the low end, {} bytes below", top - bottom);
println!("sp in main = {here:#x} {} bytes down: the runtime's frame and main's", top - here);
println!("sp % 16 = {} the calling convention keeps sp 16-byte aligned", here % 16);

1== where sp starts ==
__stack_top = 0x800070a0 the high end, where _entry pointed sp
end = 0x800030a0 the low end, 16384 bytes below
sp in main = 0x80006f70 304 bytes down: the runtime's frame and main's
sp % 16 = 0 the calling convention keeps sp 16-byte aligned
2 println!("\n== each call moves it down ==");
let mut trail = [0; 5];
descend(0, &mut trail);
for (depth, at) in trail.iter().enumerate() {
println!("descend depth {depth}: sp = {at:#x} {} bytes below main's", here - at);
}
let frame = trail[0] - trail[1];
println!("each call claimed {frame} bytes for its frame; each return gave them back");

2== each call moves it down ==
descend depth 0: sp = 0x80006f50 32 bytes below main's
descend depth 1: sp = 0x80006f30 64 bytes below main's
descend depth 2: sp = 0x80006f10 96 bytes below main's
descend depth 3: sp = 0x80006ef0 128 bytes below main's
descend depth 4: sp = 0x80006ed0 160 bytes below main's
each call claimed 32 bytes for its frame; each return gave them back
3 println!("\n== what 16 KiB buys ==");
paint();
println!("the free stack below main is painted with {PAINT:#x}");
let deepest = lowest_touched();
println!("printing that line wrote as deep as {} bytes below main's sp", here - deepest);
let room = here - bottom;
println!("room left below main: {room} bytes = {} more calls of descend", room / frame);

3== what 16 KiB buys ==
the free stack below main is painted with 0x5a5a5a5a5a5a5a5a
printing that line wrote as deep as 352 bytes below main's sp
room left below main: 16080 bytes = 502 more calls of descend
4 println!("\n== nothing guards the low end ==");
let floor = addr_of_mut!(FLOOR) as *mut u64;
for i in 0..64 {
unsafe { write_volatile(floor.add(i), PAINT) };
}
println!("FLOOR, 512 bytes of .bss, ends at end: {}", floor as usize + 512 == bottom);
let low = dig(bottom - 128);
println!("dig called itself until sp = {low:#x}, {} bytes below end", bottom - low);
let changed = (0..64).filter(|&i| unsafe { read_volatile(floor.add(i)) } != PAINT).count();
println!("words of FLOOR the frames overwrote: {changed} of 64");
let dig_at = dig as fn(usize) -> usize as u64;
let word = unsafe { read_volatile(floor.add(63)) };
println!("FLOOR[63], just below end, now holds {word:#x} = dig + {:#x}", word.wrapping_sub(dig_at));
println!("that is a return address: a frame of dig saved its ra there");
println!("no fault and no message: in machine mode, nothing marks the memory below end off-limits");
println!("a stub that started sp at end, the low end, would do this from the first prologue");
4== nothing guards the low end ==
FLOOR, 512 bytes of .bss, ends at end: true
dig called itself until sp = 0x80003010, 144 bytes below end
words of FLOOR the frames overwrote: 18 of 64
FLOOR[63], just below end, now holds 0x8000009c = dig + 0x12
that is a return address: a frame of dig saved its ra there
no fault and no message: in machine mode, nothing marks the memory below end off-limits
a stub that started sp at end, the low end, would do this from the first prologue
}
exit status 0

04 Reset to Rust: each step of the boot needs the one before it.

each step of the boot needs the one before it, and each left evidence · point at all three match: true. Nothing between the ROM and main wrote them

$ cargo run --bin 04_reset_to_rust
//! 04 — Reset to Rust: each step of the boot needs the one before it.▸ 15 more lines
//!
//! By the time `main` runs, all five steps are over. Each one left something
//! behind that `main` can still read:
//!
//! 1. QEMU copies the ELF into RAM .data holds its value; .bss is zeros
//! 2. reset: the ROM at 0x1000 runs its a0, a1 and a2 are still in registers
//! 3. it jumps to 0x8000_0000 _entry is there: link.ld put .entry first
//! 4. _entry points sp at the top the first frame starts right at the top
//! 5. only then does Rust run its first store is 8 bytes below the top
//!
//! Here step 5 is a jump, not a call. `_entry` jumps to `__week07_start`, the
//! first Rust function. Its prologue is the first store through `sp`. Then it
//! calls `main`.
//!
//! Run: cargo run --bin 04_reset_to_rust
#![no_std]
#![no_main]

use core::arch::asm;
use core::ptr::{addr_of, read_volatile};
use week07::println;

week07::entry!(main);

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

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

static mut SEEDED: u64 = 326; // .data: the ELF carries these 8 bytes
static mut CLEARED: [u64; 256] = [0; 256]; // .bss: the ELF carries only its size

const ROM: usize = 0x1000;

fn main() {
// Read before anything else: the first call would overwrite all four.
// Naming a0-a2 as outputs reads what they hold; the asm changes nothing.
let (a0, a1, a2, ra): (usize, usize, usize, usize);
unsafe { asm!("mv {ra}, ra", ra = out(reg) ra, out("a0") a0, out("a1") a1, out("a2") a2) };

1 println!("== 1. QEMU loaded the image before the first instruction ==");
let first = unsafe { read_volatile(0x8000_0000 as *const u32) };
let seeded = unsafe { read_volatile(addr_of!(SEEDED)) };
let cleared = addr_of!(CLEARED) as *const u64;
let zeros = (0..256).all(|i| unsafe { read_volatile(cleared.add(i)) } == 0);
println!("[0x80000000] = {first:#010x} _entry's first instruction, loaded before any ran");
println!("SEEDED = {seeded} .data: the value came in the ELF");
println!("CLEARED all zero: {zeros} .bss: the ELF records only its size");
println!("nothing in this program stores to SEEDED or CLEARED: QEMU's loader did both");

1== 1. QEMU loaded the image before the first instruction ==
[0x80000000] = 0x00007117 _entry's first instruction, loaded before any ran
SEEDED = 326 .data: the value came in the ELF
CLEARED all zero: true .bss: the ELF records only its size
nothing in this program stores to SEEDED or CLEARED: QEMU's loader did both
2 println!("\n== 2, 3. reset: the ROM ran, then jumped to _entry ==");
let mhartid: usize;
unsafe { asm!("csrr {}, mhartid", out(reg) mhartid) };
let dtb = unsafe { read_volatile((ROM + 32) as *const u64) } as usize;
println!("a0 = {a0} from csrr a0, mhartid: mhartid is {mhartid}");
println!("a1 = {a1:#x} from ld a1, 32(t0): the ROM's word there is {dtb:#x}");
println!("a2 = {a2:#x} from addi a2, t0, 40: an address inside the ROM itself");
let same = a0 == mhartid && a1 == dtb && a2 == ROM + 40;
println!("all three match: {same}. Nothing between the ROM and main wrote them");
println!("a1 and a2 are the proof: QEMU zeroes every register at reset, so a0 is 0 either way");
let target = unsafe { read_volatile((ROM + 24) as *const u64) } as usize;
let entry = _entry as *const () as usize;
println!("jump target {target:#x} == _entry {entry:#x}: {}", target == entry);
println!("step 3 needs step 1: the ROM jumps there whether or not anything was loaded");

2== 2, 3. reset: the ROM ran, then jumped to _entry ==
a0 = 0 from csrr a0, mhartid: mhartid is 0
a1 = 0x87e00000 from ld a1, 32(t0): the ROM's word there is 0x87e00000
a2 = 0x1028 from addi a2, t0, 40: an address inside the ROM itself
all three match: true. Nothing between the ROM and main wrote them
a1 and a2 are the proof: QEMU zeroes every register at reset, so a0 is 0 either way
jump target 0x80000000 == _entry 0x80000000: true
step 3 needs step 1: the ROM jumps there whether or not anything was loaded
3 println!("\n== 4, 5. sp at the top, then the first Rust frame ==");
let top = addr_of!(__stack_top) as usize;
let word = |below: usize| unsafe { read_volatile((top - below) as *const usize) };
let (w8, w16, w24) = (word(8), word(16), word(24));
let start = __week07_start as *const () as usize;
println!("__stack_top {top:<#12x} where _entry pointed sp, then jumped to __week07_start");
println!("__stack_top - 8 {w8:<#12x} __week07_start's saved ra: reset's 0; a jump sets no ra");
println!("__stack_top - 16 {w16:<#12x} padding: its frame is 16 bytes, to keep sp aligned");
println!("__stack_top - 24 {w24:<#12x} main's saved ra: __week07_start + {:#x}", w24.wrapping_sub(start));
println!("matches main's ra: {}. The frames start right at __stack_top: _entry pushed nothing", w24 == ra);
println!("step 5 needs step 4: Rust's very first store went through sp, 8 bytes below the top");
3== 4, 5. sp at the top, then the first Rust frame ==
__stack_top 0x80007350 where _entry pointed sp, then jumped to __week07_start
__stack_top - 8 0x0 __week07_start's saved ra: reset's 0; a jump sets no ra
__stack_top - 16 0x0 padding: its frame is 16 bytes, to keep sp aligned
__stack_top - 24 0x800003c6 main's saved ra: __week07_start + 0x12
matches main's ra: true. The frames start right at __stack_top: _entry pushed nothing
step 5 needs step 4: Rust's very first store went through sp, 8 bytes below the top
}
exit status 0

05 An address is a page number and an offset.

an address is a page number and an offset · point at >> 12 beside & 0xFFF for five real addresses

$ cargo run --bin 05_page_number_and_offset
//! 05 — An address is a page number and an offset.▸ 14 more lines
//!
//! A page is 4096 = 2^12 bytes. So the low 12 bits of an address say where a
//! byte sits inside its page, and the bits above them say which page it is:
//!
//! 0x8000_2B6C
//! >> 12 0x8_0002 the page number: drop three hex digits
//! & 0xFFF 0xB6C the offset: keep only those three
//! & !0xFFF 0x8000_2000 the page it falls in: zero those three
//!
//! An address is page-aligned when its offset is 0: in hex it ends in 000.
//! The addresses below are this program's own, as QEMU runs it, plus the
//! lecture's 0x8002_3D40.
//!
//! Run: cargo run --bin 05_page_number_and_offset
#![no_std]
#![no_main]

use core::ptr::addr_of;
use week07::{println, Hex};

week07::entry!(main);

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

const PGSIZE: usize = 4096;
const KERNBASE: usize = 0x8000_0000; // RAM starts here
const PHYSTOP: usize = 0x8800_0000; // and ends here: 128 MiB
const UART: usize = 0x1000_0000; // a device, not RAM

extern "C" {
static etext: u8;
static __stack_top: u8;
}

fn main() {
let local = 0u64;
let here = [
("main", main as fn() as usize),
("etext", addr_of!(etext) as usize),
("a local", addr_of!(local) as usize),
("__stack_top", addr_of!(__stack_top) as usize),
("the UART", UART),
];

1 println!("== page number and offset ==");
for (name, a) in here {
println!("{name:<12} {:>12} number {:>8} offset {:>5}", Hex(a), Hex(a >> 12), Hex(a & 0xFFF));
}
let same = here.iter().all(|&(_, a)| a >> 12 == a / PGSIZE && (a & 0xFFF) == a % PGSIZE);
let back = here.iter().all(|&(_, a)| (a >> 12) * PGSIZE + (a & 0xFFF) == a);
println!(">> 12 is / 4096 and & 0xFFF is % 4096, for every one: {same}");
println!("number * 4096 + offset gives each address back: {back}");
println!("the shift and the mask work only because 4096 is a power of two, 2^12");

1== page number and offset ==
main 0x8000_0052 number 0x8_0000 offset 0x52
etext 0x8000_3000 number 0x8_0003 offset 0x0
a local 0x8000_8238 number 0x8_0008 offset 0x238
__stack_top 0x8000_83E0 number 0x8_0008 offset 0x3E0
the UART 0x1000_0000 number 0x1_0000 offset 0x0
>> 12 is / 4096 and & 0xFFF is % 4096, for every one: true
number * 4096 + offset gives each address back: true
the shift and the mask work only because 4096 is a power of two, 2^12
2 println!("\n== the page it falls in ==");
for (name, a) in here {
let page = a & !(PGSIZE - 1);
match a.checked_sub(KERNBASE) {
Some(d) => println!("{name:<12} in page {:>12} RAM page {}", Hex(page), d >> 12),
None => println!("{name:<12} in page {:>12} below 0x8000_0000: not RAM", Hex(page)),
}
}
let pages = (PHYSTOP - KERNBASE) >> 12;
println!("RAM has {pages} pages: page 0 is {}, and the last, {}, is {}",
Hex(KERNBASE), pages - 1, Hex(KERNBASE + (pages - 1) * PGSIZE));
let below = addr_of!(__stack_top) as usize - addr_of!(local) as usize;
println!("a local is {below} bytes below __stack_top: main's frame is near the top of the stack");

2== the page it falls in ==
main in page 0x8000_0000 RAM page 0
etext in page 0x8000_3000 RAM page 3
a local in page 0x8000_8000 RAM page 8
__stack_top in page 0x8000_8000 RAM page 8
the UART in page 0x1000_0000 below 0x8000_0000: not RAM
RAM has 32768 pages: page 0 is 0x8000_0000, and the last, 32767, is 0x87FF_F000
a local is 424 bytes below __stack_top: main's frame is near the top of the stack
3 println!("\n== aligned means low zero bits ==");
let some = [
("the lecture", 0x8002_3D40),
("etext", addr_of!(etext) as usize),
("a local", addr_of!(local) as usize),
("KERNBASE", KERNBASE),
];
for (name, a) in some {
let k = a.trailing_zeros();
println!("{name:<12} {:>12} {k:>2} low zero bits {:>10}-byte aligned page-aligned: {}",
Hex(a), 1usize << k, k >= 12);
}
println!("0x8002_3D40 ends in 40: six zero bits, so 64-byte aligned, not page-aligned");
println!("etext is page-aligned because link.ld says ALIGN(0x1000) just before it");
3== aligned means low zero bits ==
the lecture 0x8002_3D40 6 low zero bits 64-byte aligned page-aligned: false
etext 0x8000_3000 12 low zero bits 4096-byte aligned page-aligned: true
a local 0x8000_8238 3 low zero bits 8-byte aligned page-aligned: false
KERNBASE 0x8000_0000 31 low zero bits 2147483648-byte aligned page-aligned: true
0x8002_3D40 ends in 40: six zero bits, so 64-byte aligned, not page-aligned
etext is page-aligned because link.ld says ALIGN(0x1000) just before it
}
exit status 0

06 One block size: every request costs whole pages.

one block size: every request costs whole pages · point at 4097 bytes -> 2 pages … 4095 unused

$ cargo run --bin 06_why_one_page_size
//! 06 — One block size: every request costs whole pages.▸ 12 more lines
//!
//! The page allocator hands out 4096 bytes or nothing, so a caller that needs
//! n bytes must take whole pages. The bytes it did not need are spent anyway:
//!
//! 100 bytes -> 1 page 3996 bytes nobody uses
//! 5000 bytes -> 2 pages 3192 bytes nobody uses, and the two must touch
//!
//! Space lost inside a block is internal fragmentation. What one size buys is
//! that any free page fits any one-page request. No hole between blocks is
//! ever too small to use, and there is nothing to search.
//!
//! Run: cargo run --bin 06_why_one_page_size
#![no_std]
#![no_main]

use week07::{println, Hex};

week07::entry!(main);

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

const PGSIZE: usize = 4096;

fn main() {
let asks = [8usize, 100, 4000, 4096, 4097, 5000, 12288];

1 println!("== a request, in whole pages ==");
for n in asks {
let pages = n.div_ceil(PGSIZE);
println!("{n:>6} bytes -> {pages} page{} {:>6} bytes given {:>5} unused",
if pages == 1 { " " } else { "s" }, pages * PGSIZE, pages * PGSIZE - n);
}
println!("n.div_ceil(4096) rounds the count up");
println!("4096 and 12288 fit exactly, and 4097 costs a second page");

1== a request, in whole pages ==
8 bytes -> 1 page 4096 bytes given 4088 unused
100 bytes -> 1 page 4096 bytes given 3996 unused
4000 bytes -> 1 page 4096 bytes given 96 unused
4096 bytes -> 1 page 4096 bytes given 0 unused
4097 bytes -> 2 pages 8192 bytes given 4095 unused
5000 bytes -> 2 pages 8192 bytes given 3192 unused
12288 bytes -> 3 pages 12288 bytes given 0 unused
n.div_ceil(4096) rounds the count up
4096 and 12288 fit exactly, and 4097 costs a second page
2 println!("\n== the waste stays inside the block ==");
let asked: usize = asks.iter().sum();
let given: usize = asks.iter().map(|n| n.div_ceil(PGSIZE) * PGSIZE).sum();
println!("asked for {asked} bytes, given {given}: {} unused", given - asked);
// integers only: QEMU starts with the FPU off (mstatus.FS = 0)
let permille = 100 * 1000 / PGSIZE;
println!("a 100-byte request uses {}.{}% of its page", permille / 10, permille % 10);
println!("one request can waste at most 4095 bytes: ask for 1, or for 4097");
println!("that loss is internal fragmentation");
println!("week 10's first heap gives every request a page of its own");
println!("so an 8-byte Box there leaves 4088 bytes idle, like the first row above");

2== the waste stays inside the block ==
asked for 29589 bytes, given 45056: 15467 unused
a 100-byte request uses 2.4% of its page
one request can waste at most 4095 bytes: ask for 1, or for 4097
that loss is internal fragmentation
week 10's first heap gives every request a page of its own
so an 8-byte Box there leaves 4088 bytes idle, like the first row above
3 println!("\n== two pages are not one buffer ==");
let (a, b) = (0x8710_3000usize, 0x8710_5000usize);
println!("the lecture's trace hands out {}, then {}", Hex(a), Hex(b));
println!("page numbers {} and {}: {} apart", Hex(a >> 12), Hex(b >> 12), (b >> 12) - (a >> 12));
println!("a 5000-byte buffer needs page numbers p and p + 1; these are not");
println!("the free list gives one page per call and cannot promise neighbors (Going deeper)");
3== two pages are not one buffer ==
the lecture's trace hands out 0x8710_3000, then 0x8710_5000
page numbers 0x8_7103 and 0x8_7105: 2 apart
a 5000-byte buffer needs page numbers p and p + 1; these are not
the free list gives one page per call and cannot promise neighbors (Going deeper)
}
exit status 0

07 A free page has room for the address of another page.

a free page has room for the address of another page · point at P holds Q's address, and Q itself was never touched

$ cargo run --bin 07_a_page_holds_an_address
//! 07 — A free page has room for the address of another page.▸ 13 more lines
//!
//! Nobody uses a free page, so all 4096 of its bytes are spare. Its first 8
//! are exactly one usize, and a usize holds an address. So a free page can
//! hold the record that it is free, at no cost outside the page. That is what
//! makes the free list intrusive: its links live in the free pages.
//!
//! page P +0 the address of page Q one usize, 8 bytes
//! +8 0 0 0 ... 0 4088 bytes nobody reads
//!
//! This program builds no list. It does one store through a cast, then some
//! loads to see what the store did.
//!
//! Run: cargo run --bin 07_a_page_holds_an_address
#![no_std]
#![no_main]

use core::mem::size_of;
use core::ptr::addr_of;
use week07::{println, Hex};

week07::entry!(main);

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

const PGSIZE: usize = 4096;
const PHYSTOP: usize = 0x8800_0000;
const P: usize = 0x8400_6000; // two pages 64 MiB into RAM, far above this program
const Q: usize = 0x8400_A000;

extern "C" {
static __stack_top: u8;
}

fn main() {
let top = addr_of!(__stack_top) as usize;

1 println!("== two pages nobody uses ==");
println!("__stack_top {:>12} the stack grows down from here", Hex(top));
println!("page P {:>12} above __stack_top: {} below 0x8800_0000: {}",
Hex(P), P >= top, P + PGSIZE <= PHYSTOP);
println!("page Q {:>12} above __stack_top: {} page-aligned: {}",
Hex(Q), Q >= top, Q % PGSIZE == 0);
println!("this program keeps nothing above __stack_top, so both are free");
let before = unsafe { *(P as *const usize) };
println!("P's first usize before the store: {}", Hex(before));
println!("QEMU starts RAM zeroed; real hardware makes no such promise");

1== two pages nobody uses ==
__stack_top 0x8000_8430 the stack grows down from here
page P 0x8400_6000 above __stack_top: true below 0x8800_0000: true
page Q 0x8400_A000 above __stack_top: true page-aligned: true
this program keeps nothing above __stack_top, so both are free
P's first usize before the store: 0x0
QEMU starts RAM zeroed; real hardware makes no such promise
2 println!("\n== one store, through a cast ==");
let slot = P as *mut usize; // P's first 8 bytes, seen as one usize
unsafe { *slot = Q };
let back = unsafe { *(P as *const usize) };
println!("*(P as *mut usize) = Q stored {}", Hex(Q));
println!("P's first usize, read back: {}", Hex(back));
let q_first = unsafe { *(Q as *const usize) };
println!("Q's own first usize is still {}", Hex(q_first));
println!("P holds Q's address, and Q itself was never touched");

2== one store, through a cast ==
*(P as *mut usize) = Q stored 0x8400_A000
P's first usize, read back: 0x8400_A000
Q's own first usize is still 0x0
P holds Q's address, and Q itself was never touched
3 println!("\n== the same bytes, one at a time ==");
let bytes = P as *const u8;
let first = unsafe { *(bytes as *const [u8; 8]) };
println!("P + 0 .. P + 8: {first:02X?}");
println!("low byte first, because RISC-V is little-endian");
println!("from_le_bytes gives Q back: {}", usize::from_le_bytes(first) == Q);
let rest = (8..PGSIZE).filter(|&i| unsafe { *bytes.add(i) } != 0).count();
println!("nonzero bytes in P + 8 .. P + 4096: {rest} the store wrote 8 bytes and no more");

3== the same bytes, one at a time ==
P + 0 .. P + 8: [00, A0, 00, 84, 00, 00, 00, 00]
low byte first, because RISC-V is little-endian
from_le_bytes gives Q back: true
nonzero bytes in P + 8 .. P + 4096: 0 the store wrote 8 bytes and no more
4 println!("\n== what the record costs ==");
println!("size_of::<usize>() = {}, and size_of::<*mut u8>() = {}: an address is 8 bytes",
size_of::<usize>(), size_of::<*mut u8>());
println!("the page gives up 8 of its 4096 bytes, and only while it is free");
println!("once a page is handed out, nothing reads that word, and its owner may overwrite all 4096");
println!("so taking a page back needs no memory of its own, and cannot fail for lack of it");
4== what the record costs ==
size_of::<usize>() = 8, and size_of::<*mut u8>() = 8: an address is 8 bytes
the page gives up 8 of its 4096 bytes, and only while it is free
once a page is handed out, nothing reads that word, and its owner may overwrite all 4096
so taking a page back needs no memory of its own, and cannot fail for lack of it
}
exit status 0

08 Free memory starts at the first whole page above the stack.

free memory starts at the first whole page above the stack · point at add 0xFFF, then clear the low 12 bits

$ cargo run --bin 08_where_free_memory_starts
//! 08 — Free memory starts at the first whole page above the stack.▸ 15 more lines
//!
//! In rv6 the boot stack is an array inside the image, so free memory starts
//! at `end`. The link.ld these programs share puts the stack after `end`
//! instead, so here the first byte nobody owns is `__stack_top`:
//!
//! 0x8000_0000 _entry, then code .text
//! etext constants and data .rodata .data .bss
//! end 16 KiB of stack grows down from __stack_top
//! __stack_top round up to a page
//! first page free, 4096 bytes at a time, up to 0x8800_0000
//!
//! Round up: add 0xFFF, then clear the low 12 bits. The clear alone rounds
//! down, into memory that is already in use.
//!
//! Run: cargo run --bin 08_where_free_memory_starts
#![no_std]
#![no_main]

use core::ptr::addr_of;
use week07::{println, Hex};

week07::entry!(main);

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

const PGSIZE: usize = 4096;
const KERNBASE: usize = 0x8000_0000;
const PHYSTOP: usize = 0x8800_0000;

extern "C" {
static etext: u8;
static end: u8;
static __stack_top: u8;
}

fn main() {
let local = 0u64;
let here = addr_of!(local) as usize;
let text = addr_of!(etext) as usize;
let image = addr_of!(end) as usize;
let top = addr_of!(__stack_top) as usize;

1 println!("== the stack is already in use ==");
println!("end {:>12} the image stops here", Hex(image));
println!("__stack_top {:>12} end + {} bytes of stack", Hex(top), top - image);
println!("a local {:>12} in [end, __stack_top): {}", Hex(here), (image..top).contains(&here));
println!("program 02 prints the whole layout; here only the stack matters");
println!("sp started at __stack_top before main ran, so free memory starts there, not at end");

1== the stack is already in use ==
end 0x8000_43E0 the image stops here
__stack_top 0x8000_83E0 end + 16384 bytes of stack
a local 0x8000_8300 in [end, __stack_top): true
program 02 prints the whole layout; here only the stack matters
sp started at __stack_top before main ran, so free memory starts there, not at end
2 println!("\n== round __stack_top up to a page ==");
let plus = top + (PGSIZE - 1);
let first = plus & !(PGSIZE - 1);
println!("__stack_top {:>12}", Hex(top));
let crossed = plus >> 12 != top >> 12;
println!("add 0xFFF {:>12} in the next page now: {crossed}", Hex(plus));
println!("clear the low 12 bits {:>12} the first whole page nobody uses", Hex(first));
let t = (text + 0xFFF) & !0xFFF;
println!("etext, the same way {:>12} already aligned, so it stays put: {}", Hex(t), t == text);
println!("0xFFF = {:b}: twelve ones, because 4096 is a power of two", PGSIZE - 1);
println!("so clearing the low 12 bits is zeroing the last three hex digits");

2== round __stack_top up to a page ==
__stack_top 0x8000_83E0
add 0xFFF 0x8000_93DF in the next page now: true
clear the low 12 bits 0x8000_9000 the first whole page nobody uses
etext, the same way 0x8000_3000 already aligned, so it stays put: true
0xFFF = 111111111111: twelve ones, because 4096 is a power of two
so clearing the low 12 bits is zeroing the last three hex digits
3 println!("\n== two ways to get it wrong ==");
let down = top & !(PGSIZE - 1);
println!("round down instead {:>12} the clear step alone", Hex(down));
println!(" that page starts inside the stack: {}", (image..top).contains(&down));
println!(" its first {} bytes are the top of the stack, where the first frames go", top - down);
let from_end = (image + 0xFFF) & !0xFFF;
println!("round up end instead {:>12} right for rv6, whose stack is below end", Hex(from_end));
println!(" inside this program's stack: {}, {} bytes below __stack_top",
(image..top).contains(&from_end), top - from_end);
println!("hand out a page of the stack, and the page's new owner");
println!("and the next function call write the same bytes");

3== two ways to get it wrong ==
round down instead 0x8000_8000 the clear step alone
that page starts inside the stack: true
its first 992 bytes are the top of the stack, where the first frames go
round up end instead 0x8000_5000 right for rv6, whose stack is below end
inside this program's stack: true, 13280 bytes below __stack_top
hand out a page of the stack, and the page's new owner
and the next function call write the same bytes
4 println!("\n== whole pages up to 0x8800_0000 ==");
let pages = (PHYSTOP - first) >> 12;
let below = (first - KERNBASE) >> 12;
println!("first page {:>12}", Hex(first));
println!("last page {:>12} 0x8800_0000 - 0x1000", Hex(PHYSTOP - PGSIZE));
println!("pages (0x8800_0000 - {}) >> 12 = {pages}", Hex(first));
println!("check 32768 in RAM - {below} below {} = {}", Hex(first), 32768 - below);
println!("count from both ends, and the two must agree: {}", pages == 32768 - below);
4== whole pages up to 0x8800_0000 ==
first page 0x8000_9000
last page 0x87FF_F000 0x8800_0000 - 0x1000
pages (0x8800_0000 - 0x8000_9000) >> 12 = 32759
check 32768 in RAM - 9 below 0x8000_9000 = 32759
count from both ends, and the two must agree: true
}
exit status 0

09 Shift, mask, extract, pack: four moves do every translation.

shift, mask, extract, pack; nothing checks that a field fits · point at pack_date(46, 10, 32) = 0x5D60: month 11, day 0

$ cargo run --bin 09_bits_by_hand
//! 09 — Shift, mask, extract, pack: four moves do every translation.▸ 15 more lines
//!
//! A hex digit is four bits, so a shift by 4, 8 or 12 moves whole digits.
//! Friday's checks and the midterm's translations are all these four:
//!
//! shift 0x8_1234 << 12 = 0x8123_4000
//! mask (1 << 9) - 1 = 0x1FF, nine ones
//! extract (0x8123_4ABC >> 12) & 0x1FF = 0x34
//! pack (0x5 << 4) | 0x3 = 0x53
//!
//! FAT file systems pack a date into 16 bits the same way. Packing trusts
//! each field to fit in its bits. Nothing checks, and a field that is too
//! big spills into its neighbor or off the top. Rust's operators are C's
//! with two changes: NOT is `!`, and `&` binds tighter than `==` and `!=`.
//!
//! Run: cargo run --bin 09_bits_by_hand
#![no_std]
#![no_main]

use core::hint::black_box;
use week07::{println, Hex};

week07::entry!(main);

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

// years since 1980 in bits 15..9, month in 8..5, day in 4..0
const fn pack_date(y: u16, m: u16, d: u16) -> u16 {
(y << 9) | (m << 5) | d
}

const fn month(date: u16) -> u16 {
(date >> 5) & 0xF
}

fn main() {
let ppn = black_box(0x8_1234u64);
let a = black_box(0x8123_4ABCu64);

1 println!("== four moves ==");
println!("shift {} << 12 = {}", Hex(ppn), Hex(ppn << 12));
println!("mask (1 << 12) - 1 = {} (1 << 10) - 1 = {} (1 << 9) - 1 = {}",
Hex((1 << 12) - 1), Hex((1 << 10) - 1), Hex((1 << 9) - 1));
println!("extract ({} >> 12) & 0x1FF = {}", Hex(a), Hex((a >> 12) & 0x1FF));
println!("pack (0x5 << 4) | 0x3 = {}", Hex((black_box(0x5) << 4) | 0x3));
println!("clear {} & !0xFFF = {}", Hex(a), Hex(a & !0xFFF));
println!("a shift by 4 moves one hex digit, and by 12 moves three: {} << 4 = {}",
Hex(ppn), Hex(ppn << 4));

1== four moves ==
shift 0x8_1234 << 12 = 0x8123_4000
mask (1 << 12) - 1 = 0xFFF (1 << 10) - 1 = 0x3FF (1 << 9) - 1 = 0x1FF
extract (0x8123_4ABC >> 12) & 0x1FF = 0x34
pack (0x5 << 4) | 0x3 = 0x53
clear 0x8123_4ABC & !0xFFF = 0x8123_4000
a shift by 4 moves one hex digit, and by 12 moves three: 0x8_1234 << 4 = 0x81_2340
2 println!("\n== a FAT date ==");
let date = pack_date(black_box(46), 10, 9);
println!("pack_date(46, 10, 9) = {}", Hex(date as u64));
println!("in binary: {:07b} {:04b} {:05b} year, month, day",
date >> 9, month(date), date & 0x1F);
println!("month({}) = {}", Hex(date as u64), month(date));
println!("unmasked: {} >> 5 = {}, the year still attached",
Hex(date as u64), Hex((date >> 5) as u64));
println!("year: date >> 9 = {}, so {}", date >> 9, 1980 + (date >> 9));
println!("day: date & 0x1F = {}", date & 0x1F);
println!("slide the field down to bit 0, then keep only its width");

2== a FAT date ==
pack_date(46, 10, 9) = 0x5D49
in binary: 0101110 1010 01001 year, month, day
month(0x5D49) = 10
unmasked: 0x5D49 >> 5 = 0x2EA, the year still attached
year: date >> 9 = 46, so 2026
day: date & 0x1F = 9
slide the field down to bit 0, then keep only its width
3 println!("\n== nothing checks that a field fits ==");
let spill = pack_date(46, 10, black_box(32));
println!("pack_date(46, 10, 32) = {}: month {}, day {}",
Hex(spill as u64), month(spill), spill & 0x1F);
println!("32 is 0b10_0000, six bits: the sixth landed in the month");
println!("October became November, and the day became 0");
let late = pack_date(black_box(128), 1, 1);
println!("pack_date(128, 1, 1) = {}: year {}, so {}",
Hex(late as u64), late >> 9, 1980 + (late >> 9));
println!("128 needs eight bits: its top one went past bit 15 and off the u16");
println!("a FAT date cannot say 2108; the last year it holds is 1980 + 127 = {}", 1980 + 127);

3== nothing checks that a field fits ==
pack_date(46, 10, 32) = 0x5D60: month 11, day 0
32 is 0b10_0000, six bits: the sixth landed in the month
October became November, and the day became 0
pack_date(128, 1, 1) = 0x21: year 0, so 1980
128 needs eight bits: its top one went past bit 15 and off the u16
a FAT date cannot say 2108; the last year it holds is 1980 + 127 = 2107
4 println!("\n== Rust's operators are not C's ==");
println!("!0xFFF = {}: Rust's bitwise NOT is `!`, and `~` does not exist in Rust", Hex(!0xFFF));
let flags = black_box(0x6u64); // R and W set, V clear
println!("flags = {}, so W (0x4) is set", Hex(flags));
println!("Rust: flags & 0x4 != 0 is {}: it reads (flags & 0x4) != 0", flags & 0x4 != 0);
println!("C: flags & 0x4 != 0 is {}, false: it reads flags & (0x4 != 0), so {} & 1",
flags & (0x4 != 0) as u64, Hex(flags));
println!("C's != binds tighter than &, so C misses the W bit; with parentheses both agree");
let one = black_box(1u16);
println!("1u16.checked_shl(15) = {:?}, 1u16.checked_shl(16) = {:?}",
one.checked_shl(15), one.checked_shl(16));
println!("with overflow checks on, a u16 `<<` panics only for a shift of 16 or more");
println!("bits pushed off the top, as in 2108 above, vanish without a word");
4== Rust's operators are not C's ==
!0xFFF = 0xFFFF_FFFF_FFFF_F000: Rust's bitwise NOT is `!`, and `~` does not exist in Rust
flags = 0x6, so W (0x4) is set
Rust: flags & 0x4 != 0 is true: it reads (flags & 0x4) != 0
C: flags & 0x4 != 0 is 0, false: it reads flags & (0x4 != 0), so 0x6 & 1
C's != binds tighter than &, so C misses the W bit; with parentheses both agree
1u16.checked_shl(15) = Some(32768), 1u16.checked_shl(16) = None
with overflow checks on, a u16 `<<` panics only for a shift of 16 or more
bits pushed off the top, as in 2108 above, vanish without a word
}
exit status 0

10 An Sv39 address is three 9-bit indices over a 12-bit offset.

an Sv39 address is three 9-bit indices over a 12-bit offset · point at va 0x20_00A0_35F0 into 128, 5, 3, 0x5F0

$ cargo run --bin 10_split_the_address
//! 10 — An Sv39 address is three 9-bit indices over a 12-bit offset.▸ 15 more lines
//!
//! Sv39 reads the low 39 bits, and the bits above must copy bit 38. The
//! offset is copied across untranslated; the 27 bits above it pick one entry
//! at each level of the tree:
//!
//! 38 30 29 21 20 12 11 0
//! +----------+----------+----------+----------+
//! | VPN[2] | VPN[1] | VPN[0] | offset |
//! +----------+----------+----------+----------+
//! 1 GiB 2 MiB 4 KiB byte in page what one entry covers
//!
//! VPN[i] is `(va >> (12 + 9 * i)) & 0x1FF`: shift first, then mask. Nine
//! bits, because one table is one page: 512 entries of 8 bytes.
//!
//! Run: cargo run --bin 10_split_the_address
#![no_std]
#![no_main]

use core::hint::black_box;
use core::ptr::addr_of;
use week07::{println, Hex};

week07::entry!(main);

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

extern "C" {
static end: u8;
}

static GREETING: [u8; 8] = *b"hello, 7";

/// One row of a table: an address, then its three indices and its offset.
fn row(what: &str, va: u64) {
println!("{what:<14} {:>19} {:>6} {:>6} {:>6} {:.3}",
Hex(va), (va >> 30) & 0x1FF, (va >> 21) & 0x1FF, (va >> 12) & 0x1FF, Hex(va & 0xFFF));
}

fn main() {
let va = black_box(0x20_00A0_35F0u64);

1 println!("== the lecture's address, by hand ==");
println!("va = {}", Hex(va));
println!("va >> 12 = {:>11} & 0x1FF = {:>3} VPN[0]", Hex(va >> 12), (va >> 12) & 0x1FF);
println!("va >> 21 = {:>11} & 0x1FF = {:>3} VPN[1]", Hex(va >> 21), (va >> 21) & 0x1FF);
println!("va >> 30 = {:>11} & 0x1FF = {:>3} VPN[2]", Hex(va >> 30), (va >> 30) & 0x1FF);
println!("va & 0xFFF = {:>11} offset", Hex(va & 0xFFF));
let (v2, v1, v0) = ((va >> 30) & 0x1FF, (va >> 21) & 0x1FF, (va >> 12) & 0x1FF);
let off = va & 0xFFF;
let back = (v2 << 30) | (v1 << 21) | (v0 << 12) | off;
println!("({v2} << 30) | ({v1} << 21) | ({v0} << 12) | {} = {}", Hex(off), Hex(back));
println!("each index is nine bits, 0 to 511; the pieces add back up to the address");

1== the lecture's address, by hand ==
va = 0x20_00A0_35F0
va >> 12 = 0x200_0A03 & 0x1FF = 3 VPN[0]
va >> 21 = 0x1_0005 & 0x1FF = 5 VPN[1]
va >> 30 = 0x80 & 0x1FF = 128 VPN[2]
va & 0xFFF = 0x5F0 offset
(128 << 30) | (5 << 21) | (3 << 12) | 0x5F0 = 0x20_00A0_35F0
each index is nine bits, 0 to 511; the pieces add back up to the address
2 println!("\n== what one entry covers ==");
println!("an entry at level 0: 1 << 12 = {:>10} bytes, 4 KiB", 1u64 << 12);
println!("an entry at level 1: 1 << 21 = {:>10} bytes, 2 MiB = 512 x 4 KiB", 1u64 << 21);
println!("an entry at level 2: 1 << 30 = {:>10} bytes, 1 GiB = 512 x 2 MiB", 1u64 << 30);
println!("{:<14} {:>19} VPN[2] VPN[1] VPN[0] offset", "", "address");
row("va", va);
row("va + 4 KiB", va + (1 << 12));
row("va + 2 MiB", va + (1 << 21));
row("va + 1 GiB", va + (1 << 30));
println!("each step moves one index by one, and the offset never moves");
let flat = (1u64 << 27) * 8;
println!("a flat table for all 2^27 pages: 2^27 x 8 bytes = {} GiB, per process", flat >> 30);
println!("a tree has tables only where something is mapped: 12 KiB for a first page");

2== what one entry covers ==
an entry at level 0: 1 << 12 = 4096 bytes, 4 KiB
an entry at level 1: 1 << 21 = 2097152 bytes, 2 MiB = 512 x 4 KiB
an entry at level 2: 1 << 30 = 1073741824 bytes, 1 GiB = 512 x 2 MiB
address VPN[2] VPN[1] VPN[0] offset
va 0x20_00A0_35F0 128 5 3 0x5F0
va + 4 KiB 0x20_00A0_45F0 128 5 4 0x5F0
va + 2 MiB 0x20_00C0_35F0 128 6 3 0x5F0
va + 1 GiB 0x20_40A0_35F0 129 5 3 0x5F0
each step moves one index by one, and the offset never moves
a flat table for all 2^27 pages: 2^27 x 8 bytes = 1 GiB, per process
a tree has tables only where something is mapped: 12 KiB for a first page
3 println!("\n== this program's own addresses ==");
let local = black_box(0u64);
println!("{:<14} {:>19} VPN[2] VPN[1] VPN[0] offset", "", "address");
row("main", main as fn() as usize as u64);
row("a static", addr_of!(GREETING) as u64);
row("end", addr_of!(end) as u64);
row("a local", &local as *const u64 as u64);
row("last RAM byte", 0x87FF_FFFF);
println!("RAM starts at 0x8000_0000, which is 2 GiB: all of it is root entry 2");
println!("128 MiB of RAM is 64 x 2 MiB, so VPN[1] runs from 0 to 63 across it");
let leaves = (128u64 << 20) >> 12;
println!("mapped with 4 KiB pages: {leaves} leaves, in {} level-0 tables of 512", leaves / 512);
println!("add one level-1 table and the root: {} pages of tables", leaves / 512 + 2);
println!("rv6's kernel will map RAM to itself, so these are its virtual addresses too");

3== this program's own addresses ==
address VPN[2] VPN[1] VPN[0] offset
main 0x8000_0014 2 0 0 0x014
a static 0x8000_3000 2 0 3 0x000
end 0x8000_44C0 2 0 4 0x4C0
a local 0x8000_8398 2 0 8 0x398
last RAM byte 0x87FF_FFFF 2 63 511 0xFFF
RAM starts at 0x8000_0000, which is 2 GiB: all of it is root entry 2
128 MiB of RAM is 64 x 2 MiB, so VPN[1] runs from 0 to 63 across it
mapped with 4 KiB pages: 32768 leaves, in 64 level-0 tables of 512
add one level-1 table and the root: 66 pages of tables
rv6's kernel will map RAM to itself, so these are its virtual addresses too
4 println!("\n== the top: MAXVA ==");
let maxva = black_box(1u64 << 38);
println!("MAXVA = 1 << 38 = {}: one bit short of 39", Hex(maxva));
println!("{:<14} {:>19} VPN[2] VPN[1] VPN[0] offset", "", "address");
row("TRAMPOLINE", maxva - 0x1000);
row("MAXVA", maxva);
println!("TRAMPOLINE is the top page below MAXVA: root entry 255, the last below bit 38");
println!("MAXVA itself would be root entry 256: entries 256 to 511 all have bit 38 set");
let copied = ((maxva << 25) as i64 >> 25) as u64;
println!("Sv39 wants bits 63..39 to copy bit 38, so 1 << 38 is legal only as {}", Hex(copied));
println!("rv6 stays below MAXVA, so no address it uses needs sign extension");
4== the top: MAXVA ==
MAXVA = 1 << 38 = 0x40_0000_0000: one bit short of 39
address VPN[2] VPN[1] VPN[0] offset
TRAMPOLINE 0x3F_FFFF_F000 255 511 511 0x000
MAXVA 0x40_0000_0000 256 0 0 0x000
TRAMPOLINE is the top page below MAXVA: root entry 255, the last below bit 38
MAXVA itself would be root entry 256: entries 256 to 511 all have bit 38 set
Sv39 wants bits 63..39 to copy bit 38, so 1 << 38 is legal only as 0xFFFF_FFC0_0000_0000
rv6 stays below MAXVA, so no address it uses needs sign extension
}
exit status 0

11 A PTE is a page number at bit 10, above ten bits of flags.

a PTE is a page number at bit 10, above ten bits of flags · point at | V R W 0x2048_D007

$ cargo run --bin 11_pte_by_hand
//! 11 — A PTE is a page number at bit 10, above ten bits of flags.▸ 18 more lines
//!
//! An entry stores the physical page number, not the address. A page's low
//! 12 bits are always 0, so the entry drops them and spends the room on flags:
//!
//! 63 54 53 10 9 8 7 6 5 4 3 2 1 0
//! +--------+-----------------+---+-+-+-+-+-+-+-+-+
//! |reserved| PPN (44 bits) |RSW|D|A|G|U|X|W|R|V|
//! +--------+-----------------+---+-+-+-+-+-+-+-+-+
//!
//! Valid with R, W and X all clear is a branch: its PPN names the next
//! table. Valid with any of them set is a leaf: its PPN names the page.
//! Packing shifts right by 12 and left by 10, and the two do not cancel.
//!
//! rv6 wraps an entry in a `#[repr(transparent)]` newtype: its own type, and
//! only its integer in memory. The last section shows that on the lecture's
//! framebuffer Pixel.
//!
//! Run: cargo run --bin 11_pte_by_hand
#![no_std]
#![no_main]

use core::fmt;
use core::hint::black_box;
use core::mem::{align_of, size_of};
use week07::{println, Hex};

week07::entry!(main);

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

const V: u64 = 1 << 0;
const R: u64 = 1 << 1;
const W: u64 = 1 << 2;
const X: u64 = 1 << 3;

/// The lecture's framebuffer pixel: its own type, laid out exactly as a u32.
#[repr(transparent)]
struct Pixel(u32);

/// An entry's low eight bits, shown as the letters of the flags that are set.
struct Flags(u64);

// fold: prints the letters V R W X U G A D, V first, for each bit that is set▸ 13 more lines
impl fmt::Display for Flags {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut buf = [b' '; 16];
let mut n = 0;
for (bit, &name) in b"VRWXUGAD".iter().enumerate() {
if self.0 >> bit & 1 == 1 {
buf[n] = name;
n += 2;
}
}
f.pad(if n == 0 { "none" } else { core::str::from_utf8(&buf[..n - 1]).unwrap() })
}
}

/// Leaf or branch comes from V and R, W, X alone, never from the level.
fn kind(entry: u64) -> &'static str {
if entry & V == 0 {
"invalid: V is clear, and no other bit means anything"
} else if entry & (R | W | X) == 0 {
"a branch: the address is the next table"
} else {
"a leaf: the address is the mapped page"
}
}

fn main() {
1 println!("== encode 0x8123_4000 as V R W ==");
let pa = black_box(0x8123_4000u64);
let ppn = pa >> 12;
println!("pa {:.8}", Hex(pa));
println!("pa >> 12 {:.8} the PPN: three hex digits dropped", Hex(ppn));
println!("PPN << 8 {:.8} two zero digits appended", Hex(ppn << 8));
println!("times 4 {:.8} that makes << 10", Hex((ppn << 8) * 4));
println!("| V R W {:.8} the flags, 1 + 2 + 4 = {}",
Hex((ppn << 10) | (V | R | W)), V | R | W);
println!("the PPN now starts at bit 10, and the flags fill bits 9..0");

1== encode 0x8123_4000 as V R W ==
pa 0x8123_4000
pa >> 12 0x0008_1234 the PPN: three hex digits dropped
PPN << 8 0x0812_3400 two zero digits appended
times 4 0x2048_D000 that makes << 10
| V R W 0x2048_D007 the flags, 1 + 2 + 4 = 7
the PPN now starts at bit 10, and the flags fill bits 9..0
2 println!("\n== decode it, reading upward ==");
let e = black_box(0x2048_D007u64);
println!("entry {:.8}", Hex(e));
println!("entry & 0x3FF {:.3} flags {}", Hex(e & 0x3FF), Flags(e));
println!("entry >> 10 {:.8} the PPN", Hex(e >> 10));
println!("PPN << 12 {:.8} the address", Hex((e >> 10) << 12));
println!("R and W are set, so this is {}", kind(e));
println!("{:<12} {:<6} {:<12} kind", "entry", "flags", "address");
for entry in [(e & !0x3FF) | V, 0x21FF_F401, 0x2014_8C07, 0] {
let to = (entry >> 10) << 12;
println!("{:<12.8} {:<6} {:<12.8} {}", Hex(entry), Flags(entry), Hex(to), kind(entry));
}
println!("low digit 1, V alone: the same PPN is now a table, not a page");
println!("the middle two are from the lecture's walk: level 1's entry 5, level 0's entry 3");

2== decode it, reading upward ==
entry 0x2048_D007
entry & 0x3FF 0x007 flags V R W
entry >> 10 0x0008_1234 the PPN
PPN << 12 0x8123_4000 the address
R and W are set, so this is a leaf: the address is the mapped page
entry flags address kind
0x2048_D001 V 0x8123_4000 a branch: the address is the next table
0x21FF_F401 V 0x87FF_D000 a branch: the address is the next table
0x2014_8C07 V R W 0x8052_3000 a leaf: the address is the mapped page
0x0000_0000 none 0x0000_0000 invalid: V is clear, and no other bit means anything
low digit 1, V alone: the same PPN is now a table, not a page
the middle two are from the lecture's walk: level 1's entry 5, level 0's entry 3
3 println!("\n== two slips ==");
println!("(entry >> 12) << 12 = {:.8} page-aligned, plausible, and wrong",
Hex((e >> 12) << 12));
println!("(entry >> 10) << 12 = {:.8} right: the PPN starts at bit 10", Hex((e >> 10) << 12));
let odd = black_box(0x8123_4ABCu64);
println!("{:<12} {:>18} {:>13}", "pa", "(pa >> 12) << 10", "pa >> 2");
for p in [pa, odd] {
println!("{:<12.8} {:>18.8} {:>13.8}", Hex(p), Hex((p >> 12) << 10), Hex(p >> 2));
}
println!("for an aligned pa the two agree, so pa >> 2 looks like a shortcut");
let low = (odd >> 2) & 0x3FF;
println!("unaligned, (pa >> 2) & 0x3FF = {:.3}: flags {}, and RSW = {}",
Hex(low), Flags(low), low >> 8);
println!("the offset turned into flags, W and X among them");
println!(">> 12 drops the offset first, and only then does << 10 make room for flags");

3== two slips ==
(entry >> 12) << 12 = 0x2048_D000 page-aligned, plausible, and wrong
(entry >> 10) << 12 = 0x8123_4000 right: the PPN starts at bit 10
pa (pa >> 12) << 10 pa >> 2
0x8123_4000 0x2048_D000 0x2048_D000
0x8123_4ABC 0x2048_D000 0x2048_D2AF
for an aligned pa the two agree, so pa >> 2 looks like a shortcut
unaligned, (pa >> 2) & 0x3FF = 0x2AF: flags V R W X G D, and RSW = 2
the offset turned into flags, W and X among them
>> 12 drops the offset first, and only then does << 10 make room for flags
4 println!("\n== a newtype is only its integer ==");
let fb = 0x8600_0000 as *mut Pixel; // free RAM, far above this program
let row1 = unsafe { fb.add(640) };
println!("fb = {}, fb.add(640) = {}: 640 pixels along is {} bytes",
Hex(fb as u64), Hex(row1 as u64), row1 as usize - fb as usize);
unsafe { row1.write_volatile(Pixel(0x00C0_FFEE)) };
let as_u32 = unsafe { (row1 as *const u32).read_volatile() };
let as_pixel = unsafe { row1.read_volatile() };
println!("stored Pixel(0x00C0_FFEE) at fb.add(640)");
println!("read back as a u32: {:.8}", Hex(as_u32 as u64));
println!("read back as a Pixel: {:.8}, its .0", Hex(as_pixel.0 as u64));
println!("size_of::<Pixel>() = {}, align_of::<Pixel>() = {}: exactly a u32's",
size_of::<Pixel>(), align_of::<Pixel>());
println!("on 8-byte entries, .add(511) is 511 x 8 = {} bytes in: a table's last entry",
511 * 8);
4== a newtype is only its integer ==
fb = 0x8600_0000, fb.add(640) = 0x8600_0A00: 640 pixels along is 2560 bytes
stored Pixel(0x00C0_FFEE) at fb.add(640)
read back as a u32: 0x00C0_FFEE
read back as a Pixel: 0x00C0_FFEE, its .0
size_of::<Pixel>() = 4, align_of::<Pixel>() = 4: exactly a u32's
on 8-byte entries, .add(511) is 511 x 8 = 4088 bytes in: a table's last entry
}
exit status 0

12 satp names the root table, and here it reads 0: Bare, no tree.

satp names the root table, and here it reads 0: paging is off · point at csrr satp = 0x0

$ cargo run --bin 12_satp_reads_zero
//! 12 — `satp` names the root table, and here it reads 0: Bare, no tree.▸ 19 more lines
//!
//! One CSR decides whether addresses are translated, and through which tree.
//! It packs three fields into 64 bits:
//!
//! 63 60 59 44 43 0
//! +------+-------------+---------------------------+
//! | MODE | ASID | PPN of the root table |
//! +------+-------------+---------------------------+
//!
//! MODE 8 is Sv39, 0 is Bare: no translation at all
//! ASID an address-space tag for the TLB; rv6 leaves it 0
//! PPN the root table's address >> 12
//!
//! MODE is the top hex digit, so a `satp` that starts with 8 is Sv39. MODE 0
//! turns translation off for supervisor and user mode. This program runs in
//! machine mode, which translates none of its own accesses while mstatus.MPRV
//! is clear. It only reads the register: writing it is week 10's job.
//!
//! Run: cargo run --bin 12_satp_reads_zero
#![no_std]
#![no_main]

use core::arch::asm;
use core::hint::black_box;
use week07::{println, Hex};

week07::entry!(main);

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

const PPN_BITS: u64 = (1 << 44) - 1;

fn main() {
1 println!("== pack satp by hand ==");
let root = black_box(0x87FF_F000u64);
let mode = black_box(8u64) << 60;
println!("MODE 8 << 60 = {:.16}", Hex(mode));
println!("PPN {} >> 12 = {:.16}", Hex(root), Hex(root >> 12));
println!("satp MODE | PPN = {:.16}", Hex(mode | (root >> 12)));
println!("{} is the top page of RAM, one page below 0x8800_0000", Hex(root));
println!("satp holds its page number, not its address: the low 12 bits are 0 anyway");

1== pack satp by hand ==
MODE 8 << 60 = 0x8000_0000_0000_0000
PPN 0x87FF_F000 >> 12 = 0x0000_0000_0008_7FFF
satp MODE | PPN = 0x8000_0000_0008_7FFF
0x87FF_F000 is the top page of RAM, one page below 0x8800_0000
satp holds its page number, not its address: the low 12 bits are 0 anyway
2 println!("\n== and back ==");
let satp = black_box(0x8000_0000_0008_7FFFu64);
println!("satp = {:.16}", Hex(satp));
println!("satp >> 60 = {} MODE: Sv39", satp >> 60);
println!("(satp >> 44) & 0xFFFF = {} the ASID", (satp >> 44) & 0xFFFF);
println!("satp & ((1 << 44) - 1) = {} the root's PPN", Hex(satp & PPN_BITS));
println!("that << 12 = {} the root table", Hex((satp & PPN_BITS) << 12));
let modes = [
(0u64, "Bare: no translation"),
(8, "Sv39: three levels"),
(9, "Sv48: four levels"),
(10, "Sv57: five levels"),
];
for (m, what) in modes {
println!("MODE {m:>2}: satp starts {:.16} {what}", Hex(m << 60));
}
println!("stop at the PPN and you have 0x8_7FFF, a page number and not a table");

2== and back ==
satp = 0x8000_0000_0008_7FFF
satp >> 60 = 8 MODE: Sv39
(satp >> 44) & 0xFFFF = 0 the ASID
satp & ((1 << 44) - 1) = 0x8_7FFF the root's PPN
that << 12 = 0x87FF_F000 the root table
MODE 0: satp starts 0x0000_0000_0000_0000 Bare: no translation
MODE 8: satp starts 0x8000_0000_0000_0000 Sv39: three levels
MODE 9: satp starts 0x9000_0000_0000_0000 Sv48: four levels
MODE 10: satp starts 0xA000_0000_0000_0000 Sv57: five levels
stop at the PPN and you have 0x8_7FFF, a page number and not a table
3 println!("\n== the real satp ==");
let (real, mstatus): (u64, u64);
unsafe {
asm!("csrr {}, satp", out(reg) real);
asm!("csrr {}, mstatus", out(reg) mstatus);
}
println!("csrr satp = {}", Hex(real));
println!("MODE {}, ASID {}, PPN {}: Bare, with no root table",
real >> 60, (real >> 44) & 0xFFFF, Hex(real & PPN_BITS));
println!("program 01 read the same 0 at reset, and nothing here writes it");
println!("MODE 0 is paging off for supervisor and user mode");
println!("mstatus.MPRV = {}: machine mode's own loads and stores are not translated",
(mstatus >> 17) & 1);
println!("its fetches never are, so this program runs on physical addresses either way");
3== the real satp ==
csrr satp = 0x0
MODE 0, ASID 0, PPN 0x0: Bare, with no root table
program 01 read the same 0 at reset, and nothing here writes it
MODE 0 is paging off for supervisor and user mode
mstatus.MPRV = 0: machine mode's own loads and stores are not translated
its fetches never are, so this program runs on physical addresses either way
}
exit status 0

13 W⊕X: a page may be writable or executable, never both.

a page may be writable or executable, never both · point at answer() = 42, after a store rewrote its code

$ cargo run --bin 13_w_xor_x
//! 13 — W⊕X: a page may be writable or executable, never both.▸ 12 more lines
//!
//! With paging off, nothing marks code as code. This program runs in
//! machine mode on bare RAM, so a store can rewrite one of its instructions,
//! and the next call runs the new one. A page table stops that with flags:
//!
//! code R X fetch and read, never write
//! stack, data R W read and write, never fetch
//! R W X the violation: whoever can write it can run anything
//!
//! W is 4 and X is 8, so a violation shows in one hex digit of the entry.
//!
//! Run: cargo run --bin 13_w_xor_x
#![no_std]
#![no_main]

use core::arch::{asm, global_asm};
use core::fmt;
use core::hint::black_box;
use core::ptr::addr_of;
use week07::{println, Hex};

week07::entry!(main);

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

global_asm!(
r#"
.pushsection .text.answer, "ax"
.option push
.option norvc # 4-byte instructions, so one word store replaces one
.p2align 2
.globl answer
answer:
addi a0, zero, 7 # a0 = 7
ret
.option pop
.popsection
"#
);

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

const V: u64 = 1 << 0;
const R: u64 = 1 << 1;
const W: u64 = 1 << 2;
const X: u64 = 1 << 3;

/// An entry's low eight bits, shown as the letters of the flags that are set.
struct Flags(u64);

// fold: prints the letters V R W X U G A D, V first, for each bit that is set▸ 13 more lines
impl fmt::Display for Flags {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut buf = [b' '; 16];
let mut n = 0;
for (bit, &name) in b"VRWXUGAD".iter().enumerate() {
if self.0 >> bit & 1 == 1 {
buf[n] = name;
n += 2;
}
}
f.pad(if n == 0 { "none" } else { core::str::from_utf8(&buf[..n - 1]).unwrap() })
}
}

/// What a valid entry is, from its R, W and X bits.
fn kind(flags: u64) -> &'static str {
match (flags & R != 0, flags & W != 0, flags & X != 0) {
(false, false, false) => "branch: the next table",
(false, true, _) => "reserved: W without R",
(true, false, false) => "leaf: read-only data",
(true, true, false) => "leaf: data",
(false, false, true) => "leaf: execute-only code",
(true, false, true) => "leaf: code",
(true, true, true) => "leaf: breaks W xor X",
}
}

fn main() {
1 println!("== with paging off, code is data ==");
let code = answer as *const () as *mut u32;
println!("answer() = {}", unsafe { answer() });
let first = unsafe { code.read_volatile() };
println!("its first instruction, at {}: {:.8} addi a0, zero, 7",
Hex(code as u64), Hex(first as u64));
let patched = (black_box(42u32) << 20) | (10 << 7) | 0x13;
println!("addi a0, zero, 42 packs as (42 << 20) | (10 << 7) | 0x13 = {:.8}",
Hex(patched as u64));
unsafe {
code.write_volatile(patched);
asm!("fence.i"); // the next fetch sees the store
}
println!("stored it over the first instruction, then fence.i");
println!("answer() = {}", unsafe { answer() });
println!("machine mode, no page table: every byte of RAM is readable, writable and executable");
println!("in supervisor mode, with this page mapped R X, that store would be a page fault");

1== with paging off, code is data ==
answer() = 7
its first instruction, at 0x8000_03B0: 0x0070_0513 addi a0, zero, 7
addi a0, zero, 42 packs as (42 << 20) | (10 << 7) | 0x13 = 0x02A0_0513
stored it over the first instruction, then fence.i
answer() = 42
machine mode, no page table: every byte of RAM is readable, writable and executable
in supervisor mode, with this page mapped R X, that store would be a page fault
2 println!("\n== X W R: the eight combinations with V set ==");
println!("{:<6} {:<6} {:<8} kind", "flags", "X W R", "letters");
for xwr in 0..8u64 {
let flags = V | xwr << 1;
println!("{:<6.2} {:03b} {:<8} {}", Hex(flags), xwr, Flags(flags), kind(flags));
}
println!("a leaf breaks W xor X when W (4) and X (8) are both set: a low hex digit of F");
println!("W without R, X W R = 010 or 110, is reserved: the hardware faults on it");

2== X W R: the eight combinations with V set ==
flags X W R letters kind
0x01 000 V branch: the next table
0x03 001 V R leaf: read-only data
0x05 010 V W reserved: W without R
0x07 011 V R W leaf: data
0x09 100 V X leaf: execute-only code
0x0B 101 V R X leaf: code
0x0D 110 V W X reserved: W without R
0x0F 111 V R W X leaf: breaks W xor X
a leaf breaks W xor X when W (4) and X (8) are both set: a low hex digit of F
W without R, X W R = 010 or 110, is reserved: the hardware faults on it
3 println!("\n== this program, split at etext ==");
let (text, top) = (addr_of!(etext) as u64, addr_of!(__stack_top) as u64);
println!("_entry {:>11} code", Hex(_entry as *const () as u64));
println!("answer {:>11} code, and rewritten above", Hex(answer as *const () as u64));
println!("etext {:>11} the end of code, rounded up to a page", Hex(text));
println!("end {:>11} the end of the data", Hex(addr_of!(end) as u64));
println!("__stack_top {:>11} the top of the stack", Hex(top));
println!("etext & 0xFFF = {}: link.ld aligns it, so no page holds both code and data",
text & 0xFFF);
println!("[0x8000_0000, etext) is {} pages to map R X", (text - 0x8000_0000) >> 12);
println!("[etext, __stack_top) is {} pages to map R W", (top - text + 0xFFF) >> 12);
println!("xv6 splits its kernel's map of RAM at etext this way");

3== this program, split at etext ==
_entry 0x8000_0000 code
answer 0x8000_03B0 code, and rewritten above
etext 0x8000_3000 the end of code, rounded up to a page
end 0x8000_4830 the end of the data
__stack_top 0x8000_8830 the top of the stack
etext & 0xFFF = 0: link.ld aligns it, so no page holds both code and data
[0x8000_0000, etext) is 3 pages to map R X
[etext, __stack_top) is 6 pages to map R W
xv6 splits its kernel's map of RAM at etext this way
4 println!("\n== the top pages ==");
let maxva = black_box(1u64 << 38);
let top_pages = [
("TRAMPOLINE", maxva - 0x1000, V | R | X),
("TRAPFRAME", maxva - 0x2000, V | R | W),
];
for (name, va, flags) in top_pages {
println!("{name:<11} {} {:.2} {:<6} {}", Hex(va), Hex(flags), Flags(flags), kind(flags));
}
println!("code in one, data in the other: each keeps W xor X");
println!("neither has U: supervisor mode may use both, and a user-mode access faults");
4== the top pages ==
TRAMPOLINE 0x3F_FFFF_F000 0x0B V R X leaf: code
TRAPFRAME 0x3F_FFFF_E000 0x07 V R W leaf: data
code in one, data in the other: each keeps W xor X
neither has U: supervisor mode may use both, and a user-mode access faults
}
exit status 0

E0080 attempt to shift left by 60_i32, which would overflow

SV39 << 60 with SV39: u32 · fix: widen before the shift: (SV39 as u64) << 60

$ ./show-errors.sh e0080
// error[E0080]: attempt to shift left by `60_i32`, which would overflow
//
// MODE belongs in bits 63..60, but SV39 is a u32, so `SV39 << 60` shifts a
// 32-bit value by 60. A const is computed while compiling, and there a shift
// by the type's width or more is an error. The `as u64` comes too late: it
// widens the result, after the u32 shift has already failed.
//
// FIX 1: `(SV39 as u64) << 60`: widen first, then shift.
// FIX 2: `const SV39: u64 = 8;`: give the field the register's type.
#![no_std]
#![no_main]

use core::panic::PanicInfo;

const SV39: u32 = 8;
const ROOT: u64 = 0x87FF_F000;
const SATP: u64 = (SV39 << 60) as u64 | (ROOT >> 12);

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

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

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

MODE belongs in bits 63..60, but SV39 is a u32, so SV39 << 60 shifts a 32-bit value by 60. A const is computed while compiling, and there a shift by the type's width or more is an error. The as u64 comes too late: it widens the result, after the u32 shift has already failed.

  1. (SV39 as u64) << 60: widen first, then shift.
  2. const SV39: u64 = 8;: give the field the register's type.
$ rustc --edition 2021 --target riscv64gc-unknown-none-elf --emit=metadata -o /dev/null e0080_shift_past_the_type.rs
error[E0080]: attempt to shift left by `60_i32`, which would overflow
|
17 | const SATP: u64 = (SV39 << 60) as u64 | (ROOT >> 12);
| ^^^^^^^^^^^^ evaluation of `SATP` failed here
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0080`.
exit status 1

E0133 use of extern static is unsafe and requires unsafe function or block

reading the linker's end outside unsafe · fix: unsafe { &end as *const u8 as usize }, or the safe addr_of!(end)

$ ./show-errors.sh e0133
// error[E0133]: use of extern static is unsafe and requires unsafe function or
// block
//
// `end` is declared in Rust but defined by the linker script, so rustc cannot
// check that a byte really lives there, or that nothing else changes it.
// Reading it, or making a reference to it, is unsafe, even `&end`, which only
// takes its address.
//
// FIX 1: `unsafe { &end as *const u8 as usize }`, as the lecture writes it.
// FIX 2: `core::ptr::addr_of!(end) as usize`, which is safe: it makes the
// pointer without making a reference, and never reads the byte.
#![no_std]
#![no_main]

use core::panic::PanicInfo;

extern "C" {
static end: u8;
}

fn first_free() -> usize {
&end as *const u8 as usize
}

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

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

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

end is declared in Rust but defined by the linker script, so rustc cannot check that a byte really lives there, or that nothing else changes it. Reading it, or making a reference to it, is unsafe, even &end, which only takes its address.

  1. unsafe { &end as *const u8 as usize }, as the lecture writes it.
  2. core::ptr::addr_of!(end) as usize, which is safe: it makes the pointer without making a reference, and never reads the byte.
$ rustc --edition 2021 --target riscv64gc-unknown-none-elf --emit=metadata -o /dev/null e0133_extern_static_needs_unsafe.rs
error[E0133]: use of extern static is unsafe and requires unsafe function or block
|
22 | &end as *const u8 as usize
| ^^^ use of extern static
|
= note: extern statics are not controlled by the Rust type system: invalid data, aliasing violations or data races will cause undefined behavior
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0133`.
exit status 1

E0308 mismatched types expected bool, found u64

if pte & V · fix: if pte & V != 0: Rust's & binds tighter than !=

$ ./show-errors.sh e0308
// error[E0308]: mismatched types
// expected `bool`, found `u64`
//
// C counts any nonzero integer as true, so `if (pte & PTE_V)` tests a flag.
// Rust's `if` takes only a bool, and `pte & V` is still a u64: the flag bit,
// or 0. Say the question you mean, which is "is it nonzero?".
//
// FIX 1: `if pte & V != 0`: Rust's `&` binds tighter than `!=` (program 09).
// FIX 2: `if (pte & V) != 0`: the same test, and it reads the same in C.
#![no_std]
#![no_main]

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

const V: u64 = 1 << 0;

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

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

#[no_mangle]
pub extern "C" fn _entry() -> ! {
let pte: u64 = black_box(0x2048_D007);
let kind = if pte & V { "valid" } else { "invalid" };
let _ = black_box(kind);
halt()
}
Why, and 2 fixes e

C counts any nonzero integer as true, so if (pte & PTE_V) tests a flag. Rust's if takes only a bool, and pte & V is still a u64: the flag bit, or 0. Say the question you mean, which is "is it nonzero?".

  1. if pte & V != 0: Rust's & binds tighter than != (program 09).
  2. if (pte & V) != 0: the same test, and it reads the same in C.
$ rustc --edition 2021 --target riscv64gc-unknown-none-elf --emit=metadata -o /dev/null e0308_a_flag_test_is_not_a_bool.rs
error[E0308]: mismatched types
|
32 | let kind = if pte & V { "valid" } else { "invalid" };
| ^^^^^^^ expected `bool`, found `u64`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
exit status 1

E0308 mismatched types expected usize, found u64

a u64 from satp into a usize · fix: convert where the type changes, as usize

$ ./show-errors.sh e0308
// error[E0308]: mismatched types
// expected `usize`, found `u64`
//
// On RV64 a usize is 64 bits, as wide as a u64, and Rust still keeps the two
// apart: an integer changes type only where you convert it, with `as` or the
// `try_into` rustc suggests. `csrr` fills in whatever type the variable was
// declared with, and here that was u64.
//
// FIX 1: `let root = ((satp & PPN_BITS) << 12) as usize;`: say the
// conversion.
// FIX 2: `let satp: usize;` with `PPN_BITS: usize`: the type rv6 gives
// addresses, from the start.
#![no_std]
#![no_main]

use core::arch::asm;
use core::panic::PanicInfo;

const PPN_BITS: u64 = (1 << 44) - 1;

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

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

#[no_mangle]
pub extern "C" fn _entry() -> ! {
let satp: u64;
unsafe { asm!("csrr {}, satp", out(reg) satp) };
let root: usize = (satp & PPN_BITS) << 12;
let _ = root;
halt()
}
Why, and 2 fixes e

On RV64 a usize is 64 bits, as wide as a u64, and Rust still keeps the two apart: an integer changes type only where you convert it, with as or the try_into rustc suggests. csrr fills in whatever type the variable was declared with, and here that was u64.

  1. let root = ((satp & PPN_BITS) << 12) as usize;: say the conversion.
  2. let satp: usize; with PPN_BITS: usize: the type rv6 gives addresses, from the start.
$ rustc --edition 2021 --target riscv64gc-unknown-none-elf --emit=metadata -o /dev/null e0308_usize_is_not_u64.rs
error[E0308]: mismatched types
|
36 | let root: usize = (satp & PPN_BITS) << 12;
| ----- ^^^^^^^^^^^^^^^^^^^^^^^ expected `usize`, found `u64`
| |
| expected due to this
|
help: you can convert a `u64` to a `usize` and panic if the converted value doesn't fit
|
36 | let root: usize = ((satp & PPN_BITS) << 12).try_into().unwrap();
| + +++++++++++++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
exit status 1

E0369 cannot add usize to *mut u8

page + PGSIZE on a *mut u8 · fix: page.wrapping_add(PGSIZE), or do the arithmetic on usize

$ ./show-errors.sh e0369
// error[E0369]: cannot add `usize` to `*mut u8`
//
// A raw pointer has no `+`. The methods rustc suggests, `wrapping_add` and
// `add`, count elements, not bytes: 4096 steps along a `*mut u8` is one page,
// but along a `*mut u64` it is eight. Rust makes you name the step.
//
// FIX 1: `page.wrapping_add(PGSIZE)`: safe to call, and a u8 is one byte.
// FIX 2: do page arithmetic on usize, where + means bytes, and cast back
// once: `(page as usize + PGSIZE) as *mut u8`.
#![no_std]
#![no_main]

use core::panic::PanicInfo;

const PGSIZE: usize = 4096;

fn page_after(page: *mut u8) -> *mut u8 {
page + PGSIZE
}

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

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

#[no_mangle]
pub extern "C" fn _entry() -> ! {
let _ = page_after(0x8400_6000 as *mut u8);
halt()
}
Why, and 2 fixes e

A raw pointer has no +. The methods rustc suggests, wrapping_add and add, count elements, not bytes: 4096 steps along a *mut u8 is one page, but along a *mut u64 it is eight. Rust makes you name the step.

  1. page.wrapping_add(PGSIZE): safe to call, and a u8 is one byte.
  2. do page arithmetic on usize, where + means bytes, and cast back once: (page as usize + PGSIZE) as *mut u8.
$ rustc --edition 2021 --target riscv64gc-unknown-none-elf --emit=metadata -o /dev/null e0369_pointer_plus_integer.rs
error[E0369]: cannot add `usize` to `*mut u8`
|
18 | page + PGSIZE
| ---- ^ ------ usize
| |
| *mut u8
|
help: consider using `wrapping_add` or `add` for pointer + {integer}
|
18 - page + PGSIZE
18 + page.wrapping_add(PGSIZE)
|
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0369`.
exit status 1

E0606 casting &u8 as usize is invalid

&end as usize · fix: &end as *const u8 as usize, or addr_of!(end) as usize

$ ./show-errors.sh e0606
// error[E0606]: casting `&u8` as `usize` is invalid
//
// A reference is not a number, so `as` will not turn one into a usize; it
// must become a raw pointer first. rustc's help, "remove the unneeded
// borrow", is wrong here. `end as usize` compiles, but it reads the byte
// stored at `end` and gives you its value. That byte is meaningless: the
// address is the answer.
//
// FIX 1: `&end as *const u8 as usize`: reference, raw pointer, then integer
// (the line program 02 prints).
// FIX 2: `addr_of!(end) as usize` makes the raw pointer directly, with no
// unsafe (program 08).
#![no_std]
#![no_main]

use core::panic::PanicInfo;

extern "C" {
static end: u8;
}

fn first_free() -> usize {
unsafe { &end as usize }
}

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

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

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

A reference is not a number, so as will not turn one into a usize; it must become a raw pointer first. rustc's help, "remove the unneeded borrow", is wrong here. end as usize compiles, but it reads the byte stored at end and gives you its value. That byte is meaningless: the address is the answer.

  1. &end as *const u8 as usize: reference, raw pointer, then integer (the line program 02 prints).
  2. addr_of!(end) as usize makes the raw pointer directly, with no unsafe (program 08).
$ rustc --edition 2021 --target riscv64gc-unknown-none-elf --emit=metadata -o /dev/null e0606_reference_as_usize.rs
error[E0606]: casting `&u8` as `usize` is invalid
|
23 | unsafe { &end as usize }
| ^^^^^^^^^^^^^
|
help: remove the unneeded borrow
|
23 - unsafe { &end as usize }
23 + unsafe { end as usize }
|
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0606`.
exit status 1

E0614 type usize cannot be dereferenced

*p = q with p: usize · fix: cast first: *(p as *mut usize) = q

$ ./show-errors.sh e0614
// error[E0614]: type `usize` cannot be dereferenced
//
// A usize that holds an address is still just a number, and `*` works only
// on a pointer or a reference. The cast makes the pointer, and its type,
// `*mut usize`, says the store is 8 bytes wide. Storing through a raw
// pointer is `unsafe`: only you know the page is free.
//
// FIX 1: `unsafe { *(p as *mut usize) = q }`: the store program 07 makes.
// FIX 2: `unsafe { (p as *mut usize).write(q) }`: the same store, as a method.
#![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 p: usize = 0x8400_6000;
let q: usize = 0x8400_A000;
*p = q;
halt()
}
Why, and 2 fixes e

A usize that holds an address is still just a number, and * works only on a pointer or a reference. The cast makes the pointer, and its type, *mut usize, says the store is 8 bytes wide. Storing through a raw pointer is unsafe: only you know the page is free.

  1. unsafe { *(p as *mut usize) = q }: the store program 07 makes.
  2. unsafe { (p as *mut usize).write(q) }: the same store, as a method.
$ rustc --edition 2021 --target riscv64gc-unknown-none-elf --emit=metadata -o /dev/null e0614_an_address_is_not_a_pointer.rs
error[E0614]: type `usize` cannot be dereferenced
|
30 | *p = q;
| ^^ can't be dereferenced
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0614`.
exit status 1

end_is_not_a_const error: pointers cannot be cast to integers during const eval

end's address in a const · fix: work it out at run time: only the linker knows it

$ ./show-errors.sh end_is_not_a_const
// error: pointers cannot be cast to integers during const eval
//
// rustc works out a `const` while it compiles this file. `end` gets its
// address later, when the linker lays out the whole image, so at compile time
// there is no number to give. That is why `end` is a symbol and not a constant.
// The same is true of every static's address: the linker picks them all.
// This error has no code; the whole message is its one line.
//
// FIX 1: work it out at run time, where it is needed:
// `let first_free = unsafe { &end as *const u8 as usize };`
#![no_std]
#![no_main]

use core::panic::PanicInfo;

extern "C" {
static end: u8;
}

const FIRST_FREE: usize = unsafe { &end as *const u8 as usize };

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

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

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

rustc works out a const while it compiles this file. end gets its address later, when the linker lays out the whole image, so at compile time there is no number to give. That is why end is a symbol and not a constant. The same is true of every static's address: the linker picks them all. This error has no code; the whole message is its one line.

  1. work it out at run time, where it is needed: let first_free = unsafe { &end as *const u8 as usize };
$ rustc --edition 2021 --target riscv64gc-unknown-none-elf --emit=metadata -o /dev/null end_is_not_a_const.rs
error: pointers cannot be cast to integers during const eval
|
20 | const FIRST_FREE: usize = unsafe { &end as *const u8 as usize };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: at compile-time, pointers do not have an integer value
= note: avoiding this restriction via `transmute`, `union`, or raw pointers leads to compile-time undefined behavior
error: aborting due to 1 previous error
exit status 1

tilde_is_not_an_operator error: ~ cannot be used as a unary operator

C's ~0xFFF · fix: !0xFFF: Rust's bitwise NOT is !

$ ./show-errors.sh tilde_is_not_an_operator
// error: `~` cannot be used as a unary operator
//
// C clears a page offset with `va & ~0xFFF`. Rust has no `~`: its bitwise
// NOT is `!`, the same operator that negates a bool. This error has no code,
// and rustc's help line under it names the fix.
//
// FIX 1: `va & !0xFFF`: all ones except the low 12 bits.
// FIX 2: `va & !(4096 - 1)`: the same mask, built from the page size.
#![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 va: u64 = core::hint::black_box(0x20_00A0_35F0);
let page = va & ~0xFFF;
let _ = page;
halt()
}
Why, and 2 fixes e

C clears a page offset with va & ~0xFFF. Rust has no ~: its bitwise NOT is !, the same operator that negates a bool. This error has no code, and rustc's help line under it names the fix.

  1. va & !0xFFF: all ones except the low 12 bits.
  2. va & !(4096 - 1): the same mask, built from the page size.
$ rustc --edition 2021 --target riscv64gc-unknown-none-elf --emit=metadata -o /dev/null tilde_is_not_an_operator.rs
error: `~` cannot be used as a unary operator
|
28 | let page = va & ~0xFFF;
| ^
|
help: use `!` to perform bitwise not
|
28 - let page = va & ~0xFFF;
28 + let page = va & !0xFFF;
|
error: aborting due to 1 previous error
exit status 1