A Rust kernel for RISC-V 64 — from bare-metal to SV39 paging, progressive refactoring toward a framekernel architecture with an Alpine-like userland
Built along rCore Tutorial (Ch.1–4) · ELF task loading · per-task page tables · trampoline satp switching · `KERNEL_SPACE` singleton
racho is evolving toward the framekernel architecture pioneered by Asterinas — a novel OS architecture that achieves monolithic-kernel performance while enforcing microkernel-like separation between safe and unsafe code:
┌──────────────────────────────────────┐
│ Safe Kernel (core) │ 100% safe Rust
│ syscall / fs / net / task / mm ... │
├──────────────────────────────────────┤
│ Framework (ostd / hal) │ Minimal unsafe Rust
│ page table / trap / context switch │ Small, auditable TCB
├──────────────────────────────────────┤
│ RustSBI (M-mode) │
└──────────────────────────────────────┘
The current codebase follows the rCore monolithic structure. The medium-term refactoring goal is to extract a thin unsafe framework layer (akin to Asterinas's OSTD) that encapsulates all unsafe operations — page table manipulation, context switching, trap entry/exit, and hardware interaction — while the rest of the kernel is written entirely in safe Rust.
| Aspect | Traditional Monolithic | Framekernel |
|---|---|---|
| Memory safety | Unsafe Rust pervasive | Unsafe confined to thin framework |
| TCB size | Entire kernel | Framework layer only (~few KLOC) |
| Performance | Direct function calls | Direct function calls (not IPC) |
| Auditability | Hard to isolate | Framework is small & explicit |
- Bare-metal kernel — runs directly on QEMU
virt(RISC-V 64, Supervisor mode), no host OS, nostd - Batch processing — ELF-based loading:
TaskControlBlock::new()callsMemorySet::from_elf()→ maps LOAD segments + user stack (guard page) + heap + TrapContext + kernel stack inKERNEL_SPACE, populatesTrapContextwith entry/sp/kernel_satp/kernel_sp/trap_handler;TaskManagerusesVec<TaskControlBlock>withcurrent_user_token()andcurrent_trap_cx() - Time-sharing scheduling — round-robin scheduler with preemptive timer interrupts (~100 Hz)
- Trap handling —
__alltrapssaves context →csrw satp+sfence.vma→jr trap_handler;trap_handler()returns!, dispatches syscalls/page faults/timer, callstrap_return();trap_return()setsstvecto trampoline, computes__restoreVA, passesa0=TrapContext/a1=user_satp,jrto__restore;__restoreswitches back to user page table, restores regs,sret;trap_from_kernel()catches kernel-mode traps - Syscall interface —
write,exit,yield,get_time - Virtual memory — SV39 paging with runtime activation:
KERNEL_SPACE(Arc<UPSafeCell<MemorySet>>) global singleton;mm::init()enables paging viasatp::write()+sfence.vma;MemorySet::active()writes satp token;MemorySet::remap_test()verifies .text not writable, .rodata not writable, .data not executable;MemorySet::new_kernel()maps all kernel sections/.data/.bss/physical memory/MMIO/trampoline;MemorySet::from_elf()parses ELF (xmas-elf) → mapsLOADsegments + user stack (guard page) + heap +TrapContext;PageTableEntrywithreadable()/writable()/executable()query methods;MapAreawithMapType(Identical/Framed) &MapPermission(R/W/X/U) +copy_data();PageTablewith 3-level walk,map/unmap/translate;StackFrameAllocator(recycled);FrameTracker(RAII);VPNRangeiterator;VirtPageNum.indexes()3-level VPN decomposition - User library —
user_libcrate for writing user-space apps withprintln!, ecall wrappers, and a linker script - GDB debugging — scripts for connecting
riscv64-elf-gdbto QEMU - CI pipeline — GitHub Actions builds and runs the kernel in QEMU on every push
┌──────────────────────────────────────────────┐
│ User Space │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ power_3 │ │ power_5 │ │ sleep │ │
│ │ (app 0) │ │ (app 1) │ │ (app 3) │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ ecall │ ecall │ ecall │
├───────┼─────────────┼─────────────┼───────────┤
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ Trap Handler │ │
│ │ (trap.S / trap_handler) │ │
│ └──────────────┬───────────────────────┘ │
│ │ │
│ ┌──────────────▼───────────────────────┐ │
│ │ Syscall Dispatcher │ │
│ │ write / exit / yield / get_time │ │
│ └──────────────┬───────────────────────┘ │
│ │ │
│ ┌──────────────▼───────────────────────┐ │
│ │ Task Manager (Round-Robin) │ │
│ │ __switch (switch.S) │ │
│ └──────────────────────────────────────┘ │
│ Kernel Space │
├──────────────────────────────────────────────┤
│ RustSBI (M-mode) │
└──────────────────────────────────────────────┘
│ QEMU virt (RISC-V 64) │
└──────────────────────────────────────────────┘
Memory Layout
| Region | Address | Size |
|---|---|---|
| Kernel | 0x80200000 |
— |
| Memory | 0x80000000 .. 0x88000000 |
128 MiB |
| Trampoline | 0xFFFFFFFFFFFFF000 |
4 KiB |
| TrapCtx | 0xFFFFFFFFFFFFE000 |
4 KiB |
| MMIO | 0x00100000 |
8 KiB |
| App 0 | 0x80400000 |
128 KiB |
| App 1 | 0x80420000 |
128 KiB |
| ... | ... | ... |
| App 15 | 0x80400000 + 15*0x20000 |
128 KiB |
| Max apps | 16 | |
| Kernel stack | 8 KiB per app | |
| User stack | 8 KiB per app |
racho/
├── bootloader/ # Prebuilt RustSBI binary (rustsbi-qemu.bin)
├── os/ # Kernel crate
│ ├── src/
│ │ ├── main.rs # Entry point: rust_main()
│ │ ├── entry.asm # ASM entry: _start (sets up boot stack)
│ │ ├── config.rs # Constants + kernel_stack_position() (virtual addr placement)
│ │ ├── link_app.S # Generated: embeds user app binaries into .data
│ │ ├── trap/ # trap_handler() → trap_return() → __restore trampoline flow
│ │ ├── task/ # Vec<TaskControlBlock> — per-task MemorySet + trap_cx_ppn + base_size + goto_trap_return
│ │ ├── syscall/ # Syscall dispatcher (mod.rs / fs.rs / process.rs)
│ │ ├── sync/ # UPSafeCell (uniprocessor-safe interior mutability)
│ │ ├── mm/ # Memory management (heap / frame_allocator / memory_set / page_table / address)
│ │ ├── loader.rs # Loads apps (load_apps) & provides get_app_data() for ELF parsing
│ │ ├── timer.rs # RISC-V timer (mtime), ~100 Hz tick
│ │ ├── logging.rs # Color-coded kernel logger
│ │ ├── console.rs # print!/println! via SBI console_putchar
│ │ ├── sbi.rs # SBI ecall wrappers (console, timer, shutdown)
│ │ ├── boards/qemu.rs # Board constants: CLOCK_FREQ, MEMORY_END (128 MiB), MMIO
│ │ └── lang_items.rs # Panic handler
│ ├── linker-qemu.ld # Linker script: .text.trampoline section (page-aligned), base 0x80200000
│ ├── build.rs # Generates link_app.S from user app binaries
│ ├── rust_objcopy.sh # Strips kernel ELF → raw binary
│ ├── rust-analyzer.toml
│ └── Makefile
├── user/ # User-space crate (user_lib)
│ ├── src/
│ │ ├── lib.rs # User library: _start entry, syscall wrappers
│ │ ├── syscall.rs # ecall wrappers (write, exit, yield, get_time)
│ │ ├── console.rs # print!/println! via write syscall
│ │ ├── lang_items.rs # Panic handler (infinite loop)
│ │ ├── linker.ld # App linker script (base 0x80400000, patched per app)
│ │ └── bin/ # User applications
│ │ ├── 00power_3.rs # 3^200000 mod 998244353 (CPU-bound)
│ │ ├── 01power_5.rs # 5^140000 mod 998244353
│ │ ├── 02power_7.rs # 7^160000 mod 998244353
│ │ └── 03sleep.rs # Busy-wait 3s with yield (cooperative multitasking)
│ ├── build.py # Builds each app at incrementing base addresses
│ ├── rust-analyzer.toml
│ └── Makefile
├── rust-toolchain.toml # Nightly Rust + RISC-V target
├── run_tcp_off.sh # Run kernel in QEMU
├── run_tcp_on.sh # Run QEMU with GDB stub (-s -S)
├── tcp_gdb_on.sh # Connect GDB to QEMU
├── .github/workflows/CI.yml # GitHub Actions: builds & runs in QEMU
└── Makefile # Top-level build & run aliases
- Rust nightly toolchain (see
rust-toolchain.toml) - QEMU with RISC-V 64 support (
qemu-system-riscv64) - GDB for RISC-V (
riscv64-elf-gdb) — optional, for debugging
Install Rust with the required components:
rustup toolchain install nightly
rustup default nightly
rustup target add riscv64gc-unknown-none-elf
rustup component add rust-src llvm-tools-preview
cargo install cargo-binutilsInstall QEMU (Ubuntu/Debian):
sudo apt install qemu-system-riscv64cd os && make buildCompiles the 4 user-space apps, embeds them into the kernel via link_app.S, builds the kernel ELF, and strips it to os/target/riscv64gc-unknown-none-elf/release/os.bin.
cd os && make run # builds + runs in one commandExpected output:
[ INFO] [kernel] Hello, world!
heap_test passed!
frame_allocator_test passed!
[ INFO] num_app = 4
power_3 [10000/200000]
power_3 [20000/200000]
...
3^200000 = 590847095(MOD 998244353)
Test power_3 OK!
power_5 [10000/140000]
...
Test sleep OK!
[ INFO] All applications completed!
# Build first, then start QEMU with GDB stub
cd os && make build
# Terminal 1:
./run_tcp_on.sh
# Terminal 2: connect GDB
riscv64-elf-gdb \
-ex 'file os/target/riscv64gc-unknown-none-elf/release/os' \
-ex 'set arch riscv:rv64' \
-ex 'target remote localhost:1234'| ID | Name | Signature | Description |
|---|---|---|---|
| 64 | write |
(fd: usize, buf: *const u8, len: usize) -> isize |
Write to stdout (fd=1) |
| 93 | exit |
(code: i32) -> ! |
Terminate current task |
| 124 | yield |
() -> isize |
Voluntarily yield CPU |
| 169 | get_time |
() -> isize |
Get uptime in ms |
User-space apps call these via the ecall instruction (wrappers in user/src/syscall.rs).
racho's userland design follows the Alpine Linux philosophy — lightweight, secure, and simple:
| Layer | Component | Status |
|---|---|---|
| C library | musl libc | 🔲 |
| Core utils | BusyBox | 🔲 |
| Init system | OpenRC (TBD) | 🔲 |
- 🎯 Boot BusyBox on racho — implement file system support, expand syscalls (
fork,exec,mmap,brk, etc.), and build a minimal process model sufficient to run a statically-linked BusyBox with musl libc.
SV39 page table management— done:PageTablewithmap/unmap/translate,satptokenAddress space (— done:MemorySet)new_kernel()+from_elf()+active()+remap_test()+KERNEL_SPACE+translate()Trap-based page table switching— done:trap_handler() → trap_return() → __restoretrampoline flow, full satp switchingELF-based task loading— done:TaskControlBlock::new()usesMemorySet::from_elf(), per-task page tables,goto_trap_return, kernel stack inKERNEL_SPACE- Wire
mm::init()intomain.rs— replace manual heap/frame init with unified boot, enable SV39 paging at startup - Virtual file system (VFS) layer
fork+execprocess model- Signal handling
- Framekernel refactoring — extract unsafe framework layer (page tables, trap, context switch) from safe kernel core, following Asterinas's OSTD/kernel split
- Multi-core support (SMP)
- TCP/IP networking stack
- Port OpenRC as init system
- POSIX compatibility layer
This project follows the excellent rCore Tutorial Book v3 by the THU OS team. Chapters covered:
- Chapter 1 — Bare-metal Rust: remove
std, ASM entry,println!via SBI - Chapter 2 — Batch OS: trap handling, privilege levels, first syscalls, batch execution of multiple apps
- Chapter 3 — Time-sharing OS: timer interrupts, task switching, round-robin scheduling, preemptive multitasking
- Chapter 4 — Address space & paging: Full integration achieved.
TaskControlBlock::new()callsMemorySet::from_elf()→ ELF parsing (xmas-elf) → maps LOAD segments + user stack (guard page) + heap + TrapContext + kernel stack inKERNEL_SPACE→ populatesTrapContext(entry/sp/kernel_satp/kernel_sp/trap_handler).trap_handler()(returns!) →trap_return()(setsstvecto trampoline, computes__restoreVA, passesa0=TrapContext/a1=user_satp) →__restore(csrw satp+sfence.vma→ restore →sret).TaskManagerusesVec<TaskControlBlock>withcurrent_user_token()/current_trap_cx().TaskContext::goto_trap_return()replacesgoto_restore().kernel_stack_position()computes virtual addresses fromTRAMPOLINE.trap_from_kernel()catches kernel-mode traps.mm::init()orchestrates heap + frame allocator + paging activation viaKERNEL_SPACE.active()
The framekernel architecture target is inspired by Asterinas, a production-grade Rust OS kernel that confines unsafe code to a small, auditable framework (OSTD) while keeping the rest of the kernel in safe Rust.
GPLv2 © 2026 shyweeds