From dd38cc6a3fa720290a0fc802fbb731a9aff7d100 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 28 Aug 2026 23:10:25 +0200 Subject: [PATCH] docs: add README and boot screenshot --- README.md | 209 ++++++++++++++++++++++++++++++++++++++++++++ docs/screenshot.png | Bin 0 -> 11775 bytes 2 files changed, 209 insertions(+) create mode 100644 README.md create mode 100644 docs/screenshot.png diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb94edc --- /dev/null +++ b/README.md @@ -0,0 +1,209 @@ +# 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. diff --git a/docs/screenshot.png b/docs/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..460f2b17ecbdafdc93b0212517028e7e4a9f501b GIT binary patch literal 11775 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 literal 0 HcmV?d00001