Minimal x86 (i386) operating system that boots from BIOS, enters 32-bit protected mode, and runs ELF userspace programs from a FAT16 partition. Includes preemptive scheduling, paging, a VFS, PS/2 + VGA + serial I/O, ATA disk, PCI/e1000 networking (IPv4/UDP/ICMP), and a ported Lua 5.5 userspace.
Sources and references: see
SOURCES.mdfor datasheets, specs, and tutorials used (Intel SDM, OSDev Wiki, FAT/ELF specs, RFCs, etc.).
- Architecture
- Boot Flow
- Kernel Subsystems
- Userspace
- Networking Demo
- Prerequisites
- Building
- Running
- Project Layout
- Screenshots
- Roadmap
- Known Limitations
BIOS (16-bit real mode)
└─ stage1/boot.asm (MBR @ 0x7C00) ── BIOS int 0x13 load ─┐
▼
0x10000: kernel.bin (stage2)
┌──────────────────────────┐
│ loader.c: loader_start() │
│ PIC remap, IDT, PIT, │
│ FPU, serial, E820 mem, │
│ paging (4 MiB identity),│
│ PCI, net, GDT/TSS, │
│ scheduler, FAT16, init │
└──────────┬───────────────┘
│ spawn_elf("/init.elf")
▼
userspace (ring 0, paged)
init → getty ×4 → Lua / ping / UDP echo
- Target:
i386-elf, 32-bit protected mode, identity-paged with 4 MiB pages (paging.c), per-process 4 KiB page tables onexec/fork. - No long mode / no SMP. All code runs at CPL 0; isolation is via paging (
PAGE_USER) andTSS.esp0. - Syscalls via
int 0x80(idt.c:177), dispatched insyscall_dispatch(stage2/cpu/interrupts/idt.c:14).
- MBR (
stage1/boot.asm:1): BIOS loads sector 0 to0x7C00. SavesDLboot drive, clears screen (int 0x10 AH=0x00), detects memory viaint 0x15 EAX=0xE820(stage1/boot.asm:19), loadskernel.binsectors after the MBR viaint 0x13 AH=0x02(stage1/boot.asm:84), enables A20 (port 0x92), loads GDT (stage1/gdt.asm) and enters protected mode (CR0.PE). - Trampoline (
stage2/start_loader.asm): sets flat segments0x08/0x10, stack at0x90000, callsloader_start. - Kernel init (
stage2/loader.c:32): masks PIC,pic_remapto0x20/0x28,idt_init, exception ISRs,pit_init(divisor 1193 → ~1000 Hz),enable_fpu, keyboard/serial,cpuid_init,init_allocfrom E820 map at0x71000/0x70FF0,tty_init,paging_init,pci_enumerate,net_init,gdt_init+ltr,scheduler_init,fat16_init, thenspawn_elf("/init.elf")andhltidle loop.
Disk image (image.sh): boot.bin written to LBA 0, MBR partition table at 0x1BE (P1 boot stub + P2 type 0x06 FAT16), mkfs.fat -F16 on P2 and mcopy of build/*.elf and test files.
| Subsystem | Files | Notes |
|---|---|---|
| CPU / GDT / TSS | stage2/cpu/gdt.c:19, inc/cpu/gdt.h |
Flat 4 GiB segments 0x9A/0x92, TSS for esp0 on interrupt |
| Interrupts | stage2/cpu/interrupts/idt.c:158, isr.asm, irq.asm, isr.c, irq.c |
256-entry IDT, gate 0x80:DPL3=0xEF, EOI via PIC |
| PIC 8259A | stage2/cpu/pic/pic.c:31 |
Remap 0x20/0x28, mask per IRQ, cascade on IRQ2 |
| PIT 8253 | stage2/cpu/pit/pit.c:4 |
Mode 2, channel 0, divisor 1193 |
| Memory | stage2/mem.c:73, inc/mem.h |
E820 biggest usable region → bump allocator + free-list (kmalloc/kfree with coalesce/split), 8-byte align |
| Paging | stage2/paging.c:40 |
CR4.PSE, 1024×4 MiB identity; paging_create_directory/paging_map_page/paging_clone_directory for processes |
| ATA PIO | stage2/dev/disk.c:40 |
LBA28 PIO, ports 0x1F0/0x3F6, insw, per-disk mutex via scheduler_block |
| MBR / FAT16 | inc/mbr.h:7, stage2/fat16.c:196, stage2/vfs.c |
MBR parse, BPB, FAT chain 0xFFF8 EOC, LFN, root + data clusters, VFS driver |
| ELF32 | stage2/elf.c:12, inc/elf.h |
PT_LOAD only, filters vaddr<0x01000000 artifact, spawn_elf/exec_elf |
| Scheduler | stage2/scheduler.c:226, stage2/sched_switch.asm |
Round-robin, TCB linked list, TASK_READY/RUNNING/BLOCKED/EXITED, fork_current clones page tables, exec_replace builds new address space |
| VGA / TTY / Keyboard / Serial / RTC | stage2/dev/vga.c:8, tty.c, keyboard.c:45, serial.c, rtc.c |
VGA text 0xB8000, cursor 0x3D4/0x3D5, scancode set 1, 16550 UART, 4 TTYs |
| PCI / e1000 | stage2/dev/pci.c, stage2/dev/e1k.c:369 |
BAR decode (I/O vs 32/64-bit MMIO), bus mastering, 82540EM TX/RX rings |
| Net stack | stage2/net/eth.c, arp.c, ipv4.c, icmp.c, udp.c, socket.c:88, net.c:7 |
Ethernet II, ARP, IPv4 10.0.2.15/24 gw 10.0.2.2, ICMP echo, UDP, AF_INET SOCK_DGRAM/SOCK_RAW |
- Toolchain:
clang -target i386-elf -ffreestanding -fno-pic,ld.lld -m elf_i386,nasm(Makefile:1,link.ld:1). User ELFs linked at0x40000000+(LIBC_LDFLAGS,USER_LDFLAGS). - libc (
user/src/libc.c:1,user/include/): thinint 0x80wrappers (exit=1, fork=2, read=3, write=4, open=5, close=6, waitpid=7, exec=11, ioctl=54, dup2=63, socket=300…), minimalstring.h/stdio.h/stdlib.h/math.h(bumpmalloc64 KiB, nofree),setjmp/longjmpvia inline asm. - Programs:
user/src/init.c:5(spawnsgettyon TTY 1-4;initstays on TTY1),user/src/getty.c:3(→execv("/lua.elf")),user/lua/(Lua 5.5.0 supported, code inuser/luais the original tar fromhttps://www.lua.org/ftp/lua-5.5.0.tar.gz, built viauser/lua/Makefile),test_files/ping.c(raw ICMP),test_files/udp_lua.c/socket_echo.c(UDP echo),test_files/fork_test.c. - Syscalls:
fork(scheduler.c:471copies user pages +int 0x80frame),execv(elf.c:81→scheduler_exec_replace),waitpid,read/write/open/close/dup2via VFS,socket/bind/sendto/recvfrom,ioctl(TIOCSCTTY).
Kernel configures 10.0.2.15/24 via 10.0.2.2 at boot (stage2/net/net.c:7). init starts getty.elf on each tty, which in turn prints a short login string and launches lua.elf.
QEMU with forwarding (host 10007 → guest 7):
qemu-system-x86_64 -m 4G -drive file=image.img,format=raw \
-serial stdio \
-device e1000,netdev=n0 -netdev user,id=n0,hostfwd=udp::10007-:7 \
-object filter-dump,id=f1,netdev=n0,file=netdump.pcapFrom host:
printf 'hello from host\n' | nc -u -w 1 127.0.0.1 10008Expected serial log: NET:/IPV4: bring-up, ping: ICMP round-trips, RX a.b.c.d:src -> 10.0.2.15:7 for UDP.
- Build:
clang+ld.lld+llvm-objcopy(LLVM ≥ 16),nasm,make,mtools(mcopy),mkfs.fat(dosfstools). - Run:
qemu-system-x86_64withe1000support. - macOS:
brew install llvm nasm qemu mtools dosfstools(ensureclangis LLVM clang, not Apple clang wrapper, or setCC=/opt/homebrew/opt/llvm/bin/clang).
make # → kernel.elf / kernel.bin / boot.bin / build/*.elf / image.img
make DEBUG=1 # keep debug symbols in kernel.elf → kernel.sym
make cleanArtifacts: kernel.elf (linked at 0x10000 via link.ld:6), kernel.bin (flat binary incbin'd in stage1/boot.asm:146), boot.bin (512-byte MBR + stage2), image.img (MBR + FAT16).
# headless / serial only
qemu-system-x86_64 -m 4G -drive file=image.img,format=raw -serial stdio -display none
# with networking (no forward)
qemu-system-x86_64 -m 4G -drive file=image.img,format=raw -serial stdio \
-device e1000,netdev=n0 -netdev user,id=n0
# with UDP forward + pcap (recommended)
qemu-system-x86_64 -m 4G -drive file=image.img,format=raw -serial stdio \
-device e1000,netdev=n0 -netdev user,id=n0,hostfwd=udp::10007-:7 \
-object filter-dump,id=f1,netdev=n0,file=netdump.pcapIn QEMU: Alt+1..4 (via TTY handler keyboard.c:78) switches TTYs if forwarded through the emulated keyboard; serial stdio shows kernel INFO logs.
stage1/ BIOS MBR + GDT, disk load, switch to protected mode
stage2/ kernel: loader, cpu/{gdt,cpuid,interrupts,pic,pit}, mem, paging,
dev/{disk,serial,vga,tty,keyboard,pci,e1k,rtc},
net/{eth,arp,ipv4,icmp,udp,socket,net}, fat16, vfs, elf, scheduler
inc/ kernel headers
user/src/ libc, crt0.s, init, getty
user/lua/ Lua 5.5.0 (vendored)
user/include/ userspace libc headers
test_files/ ping, udp_lua, socket_echo, fork_test, demo programs
tools/ helper scripts
link.ld kernel link at 0x10000
image.sh MBR + FAT16 image creation
2025-10-20 — cube + time demo:
2026-08-06 — init + getty + Lua on TTYs:
Completed in this capstone:
- 16→32-bit protected mode + GDT/TSS (
stage1/gdt.asm,stage2/cpu/gdt.c:19) - CPU exceptions + IDT +
int 0x80syscalls (stage2/cpu/interrupts/) - 8259 PIC remap + per-IRQ handling (
stage2/cpu/pic/pic.c:31) - 8253 PIT tick (
stage2/cpu/pit/pit.c:4) - PS/2 keyboard scancode set 1 (
stage2/dev/keyboard.c:45) - VGA text mode + TTY layer (
stage2/dev/vga.c:8,tty.c) - 16550 serial / RS232 (
stage2/dev/serial.c) - ATA PIO LBA28 disk (
stage2/dev/disk.c:40) - MBR partitioning (
inc/mbr.h:7,image.sh:72) - FAT16 + VFS + LFN (
stage2/fat16.c:196) - x87 FPU enable (
loader.c:41) - E820 memory map + bump/free-list allocator (
stage2/mem.c:73) - 4 MiB identity paging + per-process paging (
stage2/paging.c:40) - ELF32 loader (
stage2/elf.c:12) - Preemptive scheduler +
fork/exec/waitpid(stage2/scheduler.c:226) - PCI enumeration + Intel e1000 driver (
stage2/dev/e1k.c:369) - IPv4/ARP/ICMP/UDP + BSD-like sockets (
stage2/net/) - Userspace:
init/getty/Lua/ping/UDP echo demos (user/src/init.c:5)
Partial / known gaps:
- libc —
user/src/libc.c:1is a minimal freestanding subset: bumpmalloc(64 KiB, nofree/realloccorrectness), nofseek/ftell, stubtime/mktime, nopthread/errnothread-safety. Suitable for Lua + demos, not POSIX-complete. - File I/O — FAT16 read-only (
fat16.c:374write=-1), no directories beyond root, nounlink/rename.
Planned / out-of-scope for this capstone:
- filesystem write support
- Proper
sbrk/mmap+freein kernel and userspace
- All tasks run at ring 0; process isolation relies only on paging and
TSS.esp0— no user-mode privilege separation. - Single-core, no APIC; PIC only. Timer is PIT, not LAPIC timer.
- Network is guest
10.0.2.15/24via QEMU user-mode NAT; no DHCP client, address is hardcoded (stage2/net/net.c:7). - Disk is PIO without DMA or interrupts; busy-waits on
0x1F7.
Full bibliography with URLs and spec chapters: SOURCES.md.

