Files
frostbyte/README.md
T

210 lines
9.3 KiB
Markdown

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