210 lines
9.3 KiB
Markdown
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.
|