2. Move Under OpenSBI
The first checkpoint ran as machine-mode firmware with -bios none.
That was useful because it exposed the raw machine. It is not the final architecture. From this point on, OpenSBI owns M-mode and the kernel runs in S-mode.
At the end of this chapter:
jikei: booted under OpenSBImode: Shart=0x0dtb=0x87e00000The DTB address will vary by QEMU version. The kernel still parks after printing. No allocator, paging, or traps yet.
What Changes
Section titled “What Changes”Four files change. Two stay the same.
| File | Change |
|---|---|
.cargo/config.toml | -bios none becomes -bios default |
link.ld | origin moves from 0x80000000 to 0x80200000 |
src/uart.rs | deleted, replaced by src/sbi.rs |
src/main.rs | uart becomes sbi, prints DTB address |
src/panic.rs | uart becomes sbi |
boot.S | no change |
Cargo.toml | no change |
The assembly does not change because OpenSBI uses the same calling convention as QEMU’s direct boot: a0 = hart ID, a1 = device tree pointer. The linker handles the address change.
Why Use OpenSBI?
Section titled “Why Use OpenSBI?”OpenSBI is firmware that runs in M-mode before the kernel starts. It provides services through ecall:
- console output
- timer programming
- starting secondary harts
- machine-specific hardware setup
The kernel calls OpenSBI the same way a user process will later call the kernel: by loading arguments into registers and executing ecall. The difference is the privilege boundary. An ecall from S-mode traps into M-mode. An ecall from U-mode traps into S-mode.
Moving the kernel under OpenSBI means giving up direct access to M-mode resources like the UART MMIO. In exchange, the kernel can rely on OpenSBI for hardware details and focus on supervisor-mode work: page tables, traps, processes.
Update the QEMU Configuration
Section titled “Update the QEMU Configuration”In .cargo/config.toml, change -bios none to -bios default:
[build]target = "riscv64gc-unknown-none-elf"
[target.riscv64gc-unknown-none-elf]runner = "qemu-system-riscv64 -machine virt -m 256M -smp 1 -nographic -serial mon:stdio -bios default -kernel"rustflags = [ "-C", "link-arg=-Tlink.ld",]With -bios default, QEMU loads OpenSBI as firmware. OpenSBI initializes the machine in M-mode, then jumps to the kernel image in S-mode.
We keep -smp 1 for now. Multiple harts come later.
Update the Linker Script
Section titled “Update the Linker Script”In link.ld, change the origin address from 0x80000000 to 0x80200000:
ENTRY(_start)
SECTIONS { . = 0x80200000; /* OpenSBI loads kernel here */
.text : { _text_start = .; KEEP(*(.text.init)) *(.text .text.*) _text_end = .; }
.rodata : { *(.srodata .srodata.*) *(.rodata .rodata.*) }
.data : { *(.sdata .sdata.*) *(.data .data.*) }
.bss : { . = ALIGN(8); _bss_start = .; *(.sbss .sbss.*) *(.bss .bss.*) . = ALIGN(8); _bss_end = .; }}OpenSBI occupies 0x80000000. It expects the kernel payload at 0x80200000. If the linker script still says 0x80000000, the kernel image will collide with OpenSBI and neither will work.
Everything else in the linker script is unchanged.
Replace UART with SBI Console
Section titled “Replace UART with SBI Console”Delete src/uart.rs. Create src/sbi.rs:
pub fn putchar(byte: u8) { unsafe { core::arch::asm!( "ecall", in("a7") 0x01usize, in("a0") byte as usize, ); }}
pub fn puts(s: &str) { for byte in s.bytes() { putchar(byte); }}
pub fn put_hex(n: usize) { puts("0x");
let mut started = false; let mut shift = usize::BITS as usize;
while shift > 0 { shift -= 4; let digit = ((n >> shift) & 0xF) as u8; if digit != 0 || started || shift == 0 { started = true; putchar(match digit { 0..=9 => b'0' + digit, _ => b'a' + (digit - 10), }); } }}The only function that actually changes is putchar. In Chapter 1, it wrote a byte directly to the UART’s MMIO address:
unsafe { core::ptr::write_volatile(0x1000_0000 as *mut u8, byte); }Now it asks OpenSBI to write the byte:
unsafe { core::arch::asm!("ecall", in("a7") 0x01usize, in("a0") byte as usize); }a7 holds the SBI extension ID. 0x01 is the legacy console putchar extension. a0 holds the byte to print. The ecall instruction traps into M-mode, where OpenSBI handles the write and returns.
puts and put_hex are unchanged. They call putchar without caring how it reaches the hardware.
Update the Panic Handler
Section titled “Update the Panic Handler”In src/panic.rs, change uart to sbi:
use core::panic::PanicInfo;
#[panic_handler]fn panic(_info: &PanicInfo) -> ! { crate::sbi::puts("kernel panic\n"); loop { unsafe { core::arch::asm!("wfi") } }}Update the Entry Point
Section titled “Update the Entry Point”In src/main.rs, change the module name and add the DTB address to the output:
#![no_std]#![no_main]
mod panic;mod sbi;
core::arch::global_asm!(include_str!("../boot.S"));
#[unsafe(no_mangle)]pub extern "C" fn kernel_main(hartid: usize, dtb_ptr: usize) -> ! { sbi::puts("jikei: booted under OpenSBI\n"); sbi::puts("mode: S\n"); sbi::puts("hart="); sbi::put_hex(hartid); sbi::puts("\n"); sbi::puts("dtb="); sbi::put_hex(dtb_ptr); sbi::puts("\n");
loop { unsafe { core::arch::asm!("wfi") } }}The function signature has not changed. OpenSBI passes a0 = hart ID and a1 = device tree pointer, the same convention QEMU used for direct boot. The assembly in boot.S still saves a0 into tp and calls kernel_main with both arguments intact.
The device tree pointer is not parsed yet. We print it to prove it arrived. Chapter 3 will use it to discover RAM.
About boot.S
Section titled “About boot.S”The assembly from Chapter 1 works without modification. This is worth pausing on.
_start saves the hart ID, sets a stack, clears .bss, and calls Rust. None of those steps depend on whether the caller was QEMU (M-mode) or OpenSBI (S-mode). The linker resolves _boot_stack_top and _bss_start to the right addresses regardless of where the image is placed.
The only thing that changed is who called _start and what privilege mode we are in.
Run It
Section titled “Run It”cargo run --releaseOpenSBI prints its own boot banner before the kernel starts. Scroll past it. The kernel output follows:
jikei: booted under OpenSBImode: Shart=0x0dtb=0x87e00000The DTB address depends on QEMU version and memory size. Any address in the 0x8xxxxxxx range is normal.
If you still see mode: M or the output from Chapter 1, check that .cargo/config.toml has -bios default and that link.ld starts at 0x80200000.
Checkpoint
Section titled “Checkpoint”At this checkpoint, the system is:
OpenSBI (M-mode) -> _start at 0x80200000 (S-mode) -> boot stack -> zero .bss -> kernel_main -> SBI ecall console output -> wfi loopCompare this to Chapter 1:
Chapter 1: kernel IS the firmware (M-mode, 0x80000000)Chapter 2: kernel RUNS UNDER firmware (S-mode, 0x80200000)The kernel does less now. It cannot touch the UART directly. It cannot program timers or configure interrupts at the machine level. In exchange, it has a stable firmware interface and the right privilege level for everything that comes next.
What Comes Next
Section titled “What Comes Next”The device tree pointer is sitting in a1, unused. The next chapter parses it to find RAM, then builds a frame allocator. Once we can allocate physical pages, we can build page tables.