Skip to content

mivinci/Kernel

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

89 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

A Bare-Metal Kernel

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.

Quick Start

# 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=blk

What Happens

QEMU 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.

Building Your Own Programs

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.bin

Add 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.bin

Disk Format

Sector 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)

Custom Init

The kernel tries init paths in order and runs the first one found:

/bin/init   (default)
/bin/sh     (shell fallback)
/bin/hello  (smoke test)

Syscalls

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

Debugging with GDB

# 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 registers

Interactive Testing with expect

Use 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's read() / yield() loop and causes the shell to miss input.

Run a single command

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 ==="
'

Run and exit

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
'

Non-interactive smoke test

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=blk

This prints boot log and exits after 5 seconds. Look for $ in the output to confirm the shell started.

Build Options

make                     # default (RISC-V 64)
make ENABLE_DEBUG=1      # debug symbols (-g)
make SMP=4               # 4 simulated harts

Directory Layout

Makefile                # 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

Requirements

  • riscv64-elf-gcc, qemu-system-riscv64
  • Python 3 (for disk image builder)

About

A kernel.

Resources

Stars

Watchers

Forks

Contributors