A minimal, educational kernel written in C and RISC-V assembly. Boots directly in M-mode on QEMU with no BIOS, bootloader, or runtime. Supports user-mode processes, a virtual filesystem, a shell, and multiple programs.
# 1. Build kernel + user programs
make # default ARCH=riscv
make -C usr # build hello, init, sh, cat, bighello
# 2. Create disk image
python3 tools/mkfs.py disk.img \
/bin/init=usr/init.bin \
/bin/sh=usr/sh.bin \
/bin/hello=usr/hello.bin
# 3. Run
qemu-system-riscv64 -machine virt -bios none -kernel kernel.elf \
-nographic -smp 2 \
-drive file=disk.img,format=raw,if=none,id=blk \
-device virtio-blk-device,drive=blk
# With virtio-blk-pci (PCI path, the kernel tries this first)
qemu-system-riscv64 -machine virt -bios none -kernel kernel.elf \
-nographic -smp 2 \
-drive file=disk.img,format=raw,if=none,id=blk \
-device virtio-blk-pci,drive=blkQEMU loads kernel at 0x80000000
→ hart 0 wins atomic lottery, calls kmain()
→ traps, timers, memory allocator
→ mounts disk filesystem (superblock + inode table)
→ spawns /bin/init (PID 0, U-mode)
→ init spawns /bin/sh
→ sh prints "$ ", reads commands, spawns programs
Type hello in the shell to run the demo.
User programs are plain C, compiled as flat binaries with -Ttext=0 and
-mcmodel=medany (auipc-based position-independent addressing):
// myapp.c
#include "usr.h" // write, read, spawn, wait, exit, yield
void main(void) {
write(1, "Hello!\n", 7);
exit(0);
}riscv64-elf-gcc -nostdlib -ffreestanding -fno-builtin -Os \
-march=rv64gc -mabi=lp64d -mcmodel=medany \
-fno-pic -fno-PIE \
-Wl,-Ttext=0 -o myapp.elf crt0.S myapp.c
riscv64-elf-objcopy -O binary myapp.elf myapp.binAdd it to the disk image and type myapp in the shell:
python3 tools/mkfs.py disk.img \
/bin/init=usr/init.bin \
/bin/sh=usr/sh.bin \
/bin/myapp=myapp.binSector 0 holds the superblock (magic 0x4449534B, KSID) followed by up to 12
inodes (36 bytes each). Data blocks start at sector 1. The entire metadata fits
in a single 512-byte sector — a single blk_read(0, buf) at mount.
Sector 0: [magic][ninodes][nblocks][inode 0][inode 1]...[inode 11]
Sector 1+: data blocks (512 bytes each, one per file sector)
The kernel tries init paths in order and runs the first one found:
/bin/init (default)
/bin/sh (shell fallback)
/bin/hello (smoke test)
| nr | name | args | returns | description |
|---|---|---|---|---|
| 1 | write | fd, buf, len | bytes | write to fd (1 = stdout) |
| 2 | exit | code | — | terminate process |
| 3 | yield | — | — | give up CPU voluntarily |
| 7 | read | fd, buf, len | bytes | read from fd (0 = stdin) |
| 8 | spawn | path | child | load & run program |
| 9 | wait | pid | child | reap exited child |
# Terminal 1: start QEMU with GDB stub
qemu-system-riscv64 -machine virt -bios none -kernel kernel.elf -s -S &
# Terminal 2: attach GDB
riscv64-elf-gdb kernel.elf -ex "target remote localhost:1234"
# Common commands:
# break kmain — break at kernel entry
# break trap_handler — break on any trap
# info registers — dump all registersUse expect to script terminal interactions with the kernel shell. This is the
recommended way to test user programs — it sends characters one at a time over a
pseudo-terminal, matching the UART interrupt-driven input path.
Do not pipe input (e.g.
echo hello | qemu ...). Piped stdin delivers all data at once, which races with the kernel'sread()/yield()loop and causes the shell to miss input.
timeout 20 expect -c '
set timeout 10
spawn qemu-system-riscv64 -machine virt -bios none -kernel kernel.elf \
-nographic -smp 2 \
-drive file=disk.img,format=raw,if=none,id=blk \
-device virtio-blk-device,drive=blk
expect "$ "
send "hello\r"
expect "$ "
puts "=== hello ran, shell returned ==="
'timeout 20 expect -c '
set timeout 10
spawn qemu-system-riscv64 -machine virt -bios none -kernel kernel.elf \
-nographic -smp 2 \
-drive file=disk.img,format=raw,if=none,id=blk \
-device virtio-blk-device,drive=blk
expect "$ "
send "hello\r"
expect "$ "
send "x\r"
expect eof
'To verify boot without sending any input (e.g. in CI), use timeout:
timeout 5 qemu-system-riscv64 -machine virt -bios none -kernel kernel.elf \
-nographic -smp 2 \
-drive file=disk.img,format=raw,if=none,id=blk \
-device virtio-blk-device,drive=blkThis prints boot log and exits after 5 seconds. Look for $ in the output to
confirm the shell started.
make # default (RISC-V 64)
make ENABLE_DEBUG=1 # debug symbols (-g)
make SMP=4 # 4 simulated hartsMakefile # top-level build (kbuild-style)
arch/riscv/ # boot, drivers, trap handling, arch-specific subsystems
entry.S # multi-hart bootstrap (atomic lottery)
main.c # kmain: init sequence
trap.c / trap_entry.S # trap/interrupt handling
virtio.c # virtio-blk driver (interrupt-driven I/O)
plic.c / timer.c # interrupt controller, timer
vmm.c # Sv39 page tables, user_va2pa
usr.c # user-mode entry (PMP, satp, mret)
proc.c # process table, scheduler, swtch glue
syscall.c # ecall dispatch
blk.c # generic blk_read → virtio bridge
swtch.S # context switch (callee-saved)
uart.c / pci.c # UART, PCI bus scanning
include/ # kernel type and interface headers
include/arch/riscv/ # RISC-V ISA headers (csr.h, mmu.h, trap.h, ...)
sys/ # platform-independent kernel subsystems
diskfs.c # on-disk filesystem (superblock + inode cache)
pmm.c # physical page allocator (free list)
fs.c / file.c # inode table, file descriptor table
fdt.c # device tree parser
string.c / vsprintf.c # string, printf (from Linux 0.12)
usr/ # user programs (crt0, usr.h, hello, init, sh, cat)
tools/mkfs.py # disk image builder
scripts/ # build system internals
riscv64-elf-gcc,qemu-system-riscv64- Python 3 (for disk image builder)