2026-09-09 20:02:02 +02:00
2026-08-28 23:10:25 +02:00
2026-09-02 19:05:09 +02:00
2026-09-02 19:13:59 +02:00
2026-09-09 20:02:02 +02:00
2026-09-01 16:32:03 +02:00
2026-09-01 16:32:03 +02:00
2026-08-28 23:10:25 +02:00

frostbyte

A small hobby operating system kernel for x86_64, written in C and GAS assembly. It boots via Multiboot2/GRUB, switches from 32-bit protected mode into 64-bit long mode with its own page tables, runs a preemptive round-robin scheduler, and loads static ELF programs from a ramdisk into ring 3 that talk back through system calls.

Everything below is verifiable headlessly in QEMU.

What it does

  • Boots as a Multiboot2 kernel loaded by GRUB at 1 MiB, then builds its own paging structures and jumps into a higher-half 64-bit kernel at 0xFFFFFFFF80000000.
  • Talks over serial COM1 and the VGA text buffer through a shared kprintf.
  • Installs a full IDT (32 CPU exception handlers, 16 remapped hardware IRQs, and a ring-3-callable int 0x80 syscall gate).
  • Drives the PIT at 100 Hz and preempts between kernel and user threads on every tick.
  • Reads the PS/2 keyboard and feeds an in-kernel shell.
  • Manages physical RAM with a bitmap frame allocator built from the Multiboot2 memory map, maps pages on demand, and serves a real free-list heap with kfree and coalescing.
  • Loads a Multiboot2 module ramdisk in USTAR (tar) format, parses static x86_64 ELF executables from it, maps their segments as user pages, and runs them in ring 3 with their own kernel and user stacks via a TSS.
  • Mounts a VFS: the USTAR ramdisk read-only at / (directories and files) and a writable in-RAM tmpfs at /tmp, with per-process file descriptor tables.
  • Serves system calls: write, read, exit, getpid, yield, sbrk, sleep, plus open, close, lseek, stat, getdents, mkdir, unlink.

Layout

src/
  boot.S        multiboot2 header, 32-bit entry, long mode + paging bring-up
  isrStubs.S    per-vector interrupt stubs and the common save/restore path
  kmain.c       bring-up sequence and the demo threads
  serial.c      COM1 driver
  vga.c         80x25 VGA text driver with scrolling and hardware cursor
  console.c     kprintf over serial + VGA
  string.c      freestanding memset/memcpy/memmove/strcmp/...
  gdt.c         flat 64-bit GDT
  idt.c         IDT setup
  isr.c         exception panic + IRQ dispatch
  pic.c         8259 PIC remap, EOI, masking
  pit.c         programmable interval timer
  keyboard.c    PS/2 scancode set 1 to ASCII, ring buffer
  pmm.c         bitmap physical frame allocator
  paging.c      4-level page table walk, on-demand tables, user-range mapping
  heap.c        free-list heap allocator: kmalloc / kfree / krealloc, coalescing
  sched.c       preemptive scheduler, kernel + ring-3 threads, sleep/exit/sbrk
  gdt.c         kernel + user segments and the 64-bit TSS descriptor
  tss.c         task state segment, per-thread rsp0 for ring 3 -> ring 0
  multiboot.c   Multiboot2 module (ramdisk) discovery
  initrd.c      USTAR (tar) ramdisk header/content/directory reader
  vfs.c         path resolution, per-process descriptors, tmpfs, fd syscalls
  elf.c         static ELF64 loader (maps PT_LOAD segments as user pages)
  syscall.c     int 0x80 dispatch and the system calls
  shell.c       help / echo / ls / cat / mkdir / cd / pwd / mem / uptime / ps / clear
include/        headers (shared syscall.h is included by the kernel and userland)
user/
  crt.S         _start: call main, then exit
  ulib.c        userland syscall wrappers and print helpers
  hello.c       writes from ring 3, yields, exits
  counter.c     counts with sleep between steps
  sbrktest.c    grows its heap with sbrk and verifies the page
  fileread.c    reads a ramdisk file and round-trips a tmpfs file over syscalls
  user.ld       links each program above 1 GiB with headers in the first segment
fsroot/         sample text files packed into the ramdisk (etc/, home/)
tools/mktar.c   host tool that packs the user ELFs and text files into a USTAR image
tools/ci-test.sh  headless QEMU boot gate: greps serial for the filesystem proof
linker.ld       higher-half layout, low boot segment
Makefile        build + run + test targets
iso/boot/grub/grub.cfg

Requirements

System GCC 13 with freestanding flags (no cross compiler needed), GNU ld, plus grub-mkrescue, xorriso, mtools and qemu-system-x86_64.

Build

make

This compiles every src/*.c and src/*.S and links the kernel with linker.ld into build/frostbyte.elf. It also builds the userland programs under user/ as standalone static ELF64 executables, packs them together with the fsroot/ text files into a USTAR build/initrd.img with the host mktar tool, and wraps kernel plus ramdisk into a bootable os.iso with grub-mkrescue. GRUB hands the ramdisk to the kernel as a Multiboot2 module.

make test boots the ISO headless in QEMU and greps the serial log for the proof that a ring-3 program read a ramdisk file and round-tripped a tmpfs file.

Run

Windowed (see the VGA text screen and type into the shell):

make run

Headless with the kernel log on your terminal over serial:

make runserial

To capture the serial log to a file:

qemu-system-x86_64 -cdrom os.iso -serial file:boot.log -display none -m 128M -no-reboot

Proof

docs/boot-serial.log is a captured headless boot. Abridged:

========================================
  frostbyte kernel  x86_64  long mode
  multiboot2 / grub / freestanding gcc
========================================

[boot] multiboot2 magic verified (36d76289)
[gdt]  loaded flat 64-bit segments
[idt]  256 vectors installed (32 exceptions, 16 irqs)
[pic]  remapped to 0x20-0x2f, irq0 and irq1 unmasked
[pmm]  130559 KiB usable, 32736 frames, 32463 free after kernel
[heap] bump allocator at 0x0000000000111000, 8192 KiB
[paging] mapped 0x0000000040000000 -> phys 911000, readback OK
[pit]  timer at 100 Hz
[kbd]  ps/2 keyboard driver ready
[sched] 4 threads: idle, shell, workerA, workerB

[boot] enabling interrupts, starting preemptive scheduler
frostbyte> [workerA] tick=50 iterations=8650613
[workerB] tick=91 iterations=15943273
[workerA] tick=150 iterations=29911570
[workerB] tick=191 iterations=34964279

workerA and workerB never yield; they run tight loops on separate kernel stacks. Both advancing while the timer ticks climb is the proof that the timer interrupt preempts and the scheduler round-robins between them.

docs/userspace-serial.log is a captured headless boot of the userspace layer. The kernel mounts the ramdisk, loads three ELF programs into ring 3, and they answer over the syscall interface. Abridged:

[heap]   coalesce PASS, 8191 KiB free of 8192 KiB, blocks=1
[initrd] fbar mounted, 3 files
[user] loaded 'hello' pid=4 entry=0x00000000400000b7 stackTop=0x0000000048000000
frostbyte> hello: greetings from ring 3, pid=4
counter: pid=5 step=0
sbrktest: pid=6 requesting 8192 bytes via sbrk
hello: resumed after yield, exiting cleanly
[user] pid=4 'hello' exited with code 7
sbrktest: heap page write and read-back verified OK
counter: finished counting
[user] pid=5 'counter' exited with code 0

The hello: line only reaches the serial port through the write syscall from ring 3, so its presence is the proof that the ring 0 -> ring 3 transition, the int 0x80 gate, and the dispatcher all work. As a counterfactual, routing int 0x80 to a vector the dispatcher never sees erases every user line and leaves a ring-3 page fault (the fault frame reports cs=1b, confirming the code really ran unprivileged).

docs/boot-serial.log and docs/shell-serial.log show the v1 kernel path: the two never-yielding worker threads advancing on separate stacks (scheduler proof) and the shell answering keystrokes injected over the QEMU monitor.

docs/screenshot.png is a QEMU framebuffer dump, showing the VGA text mode output that the serial log cannot display.

Design notes

  • Higher-half kernel: the boot segment is identity mapped low so GRUB can enter it in 32-bit, and the rest of the kernel is linked at -2 GiB (-mcmodel=kernel). boot.S maps the first 1 GiB both identity and at the higher half, then far-jumps into 64-bit code.
  • Context switching happens inside the timer IRQ. The common interrupt stub saves all registers to the interrupted thread's stack, hands the stack pointer to the scheduler, and reloads the stack pointer the scheduler returns. A fresh thread starts from a hand-crafted interrupt frame, so iretq drops straight into its entry point with interrupts enabled.
  • Userspace runs in the kernel's address space but under ring 3. Each user program is linked at its own base above 1 GiB (so it sits outside the identity-mapped low region) and its segments are mapped as user pages. The int 0x80 frame is a normal interrupt frame, so the same save/restore stub serves both IRQs and syscalls; iretq returns to ring 3 because the saved cs has RPL 3.
  • Every thread that can enter ring 3 owns a separate kernel stack. The scheduler writes that stack's top into the TSS rsp0 on each switch, so a trap taken in ring 3 lands on the right kernel stack.
  • The heap is an address-ordered free list with boundary coalescing and block splitting, which is what makes kfree and reuse under fragmentation work.

Not in this version

Per-process address spaces (all user programs currently share one page table and are separated by link base), a persistent on-disk filesystem, networking, and SMP are the next steps. The current VFS reads a USTAR ramdisk and a RAM-only tmpfs; fork/exec, an ATA/AHCI block driver behind the VFS, and a TCP/IP stack come after that.

S
Description
No description provided
Readme
941 KiB
0 Stars 1 Watchers 0 Forks
Languages
C 95.1%
Makefile 2.6%
Shell 1%
C++ 0.6%
Assembly 0.5%
Other 0.2%