xv6 for RISC-V in Rust
- Rust nightly toolchain
- qemu-system-riscv64
# Build kernel and user programs
cargo build --release
# Create and populate the filesystem image
qemu-img create target/fs.img 2G
./mkfs.sh
# Run in QEMU
cargo run --releaseThe QEMU runner in .cargo/config.toml includes -s, which always opens a GDB server on
tcp::1234. To halt the kernel at startup and wait for a debugger to attach, add -S to the
runner flags, then connect from a second terminal:
cargo build # build with debug info
cargo run # QEMU starts frozen, waiting for GDB
# in a second terminal:
riscv64-elf-gdb # .gdbinit connects to port 1234 and loads symbols automaticallyThe kernel boots, initializes all subsystems, and runs a full userspace environment including a shell with pipes, redirections, and background jobs. All planned stages are complete.
A kernel's subsystems have deep interdependencies, making it non-trivial to find an order in which
they can be built incrementally. This is the sequence I followed, though stubs and todo!()s were
often needed to break circular dependencies.
- Entry point at 0x80000000 — per-CPU stack setup
- Machine-mode start — privilege mode config, interrupt delegation, timer init
- Supervisor-mode main — hart 0 initializes subsystems, other harts wait
- Console/UART driver — polling TX/RX; UART hardware configured for interrupts
- PLIC interrupt controller — external interrupt routing and claim/complete
- Physical memory allocator — buddy allocator (
buddy-alloccrate) - Sv39 page tables — 3-level page table walk, map, unmap
- Kernel virtual memory (Kvm) — identity-map kernel, devices, trampoline
- User virtual memory (Uvm) — per-process page tables
- Synchronization — spinlocks,
OnceLock - Process control blocks — fixed pool of 64 processes with spinlock-protected state
- Trampoline & trap frames — user/kernel transition via shared trampoline page
- Trap handling — user traps (syscall, interrupt, fault) and kernel traps
- Context switch (
swtch) — callee-saved register save/restore - Scheduler — round-robin scheduling with sleep/wakeup
- Syscall dispatcher — parse a7 register for syscall number
- Console read/write — user-facing I/O with interrupt-driven RX
- fork() — clone process, copy memory
- wait() — wait for child exit, reparent logic
- exit(), kill(), getpid()
- sleep() — user-space sleep
- uptime() — return elapsed timer ticks since boot
- sbrk() — grow/shrink process heap
- VirtIO disk driver
- Buffer cache — block caching layer
- Disk interrupt handling
- Sleep locks — non-blocking locks that yield the CPU while waiting, for long-held resources
- Logging layer — write-ahead logging for crash recovery
- Superblock — filesystem metadata
- Inode layer — on-disk inode structure, read/write
- Directory layer — directory operations
- Path name resolution
- File descriptor abstraction and device table
- open(), close() — open a file by path, release a file descriptor
- read(), write() — read/write file data by file descriptor
- fstat() — query file metadata (type, size, inode number)
- link(), unlink() — create/remove a directory entry for an inode
- mkdir(), chdir() — create a directory, change working directory
- mknod() — create a special file bound to a device major/minor number
- dup() — duplicate a file descriptor to the lowest available slot
- exec() syscall — load ELF binary, set up new address space
- Cargo workspace restructuring — kernel/user crate split, per-crate build scripts and linker scripts
- User space crate — syscall wrappers, panic handler
- /init program — first userspace process (opens console, forks and execs shell)
- Shell — pipes, redirections, background jobs, built-ins (cd, exit)
- pipe() — create a unidirectional channel, returning a read/write file descriptor pair
- Console as device file
- Multi-hart scheduling