A minimal x86 hobby operating system built from scratch. Boots from real mode into 32-bit protected mode with paging, heap allocator, and a command-line shell. Uses NVFS (Noverix File System) — an extent-based filesystem that replaces FAT16.
| Area | Details |
|---|---|
| Boot | Multi-stage bootloader: Stage 1 MBR (512B) loads Stage 2 from LBA 1-16 via LBA/CHS → Stage 2 (8KB, loaded at 0x7E00) handles E801 memory detect, VBE init, A20 gate, GDT, and 32-bit PM transition. Forward rep movsd safe kernel copy. MBR includes valid partition table (type 0x83) for VMware/HW BIOS compatibility. |
| Disk I/O | ATA PIO (LBA28) read/write, dual-channel primary/secondary, IDENTIFY-based drive detection, sector-level access. |
| NVFS | Extent-based filesystem: superblock (sector 1), block bitmap (sectors 2–9), inode table (sectors 10–41, expandable), data blocks (sectors 42–32767). Each inode has 13 direct extents + 1 indirect block pointer (linked extents, up to 64 more extents). Shadow paging for crash-safe writes. Dynamic inode table expansion when full. File timestamps (ctime/mtime, seconds since boot) stored in inode. |
| VGA text mode | 80×25 text buffer, hardware cursor, terminal scrolling, hex/dec rendering. Fallback when VBE unavailable. |
| VBE graphics mode | 800×600×24bpp framebuffer (mode 0x115), bitmap font rendering (8×16), pixel-level draw, smooth scrolling. Automatic dispatch in screen driver when active. |
| PS/2 keyboard | Interrupt-driven ring buffer, shift/caps, command history with arrow keys, configurable typematic rate (PS/2 command 0xF3, rate shell command). |
| Interrupts | IDT with 32 exception ISRs and 16 IRQs. Full register dump on exception (ud2 crash command). |
| Timer (PIT) | Tick counter via IRQ0, sleep_ms() for delays, get_ticks() for time source. |
| Serial I/O | COM1 serial port — kernel logging, debug output, and shell input via -serial mon:stdio. |
| Memory | Page Frame Allocator (bitmap-based, 1 bit per 4KB frame, 8192 frames for 32MB). |
| Paging | 32-bit x86 two-level paging (PD + PT), identity map 0–32MB, map_page()/unmap_page()/get_page_mapping() for custom mappings, CR0.PG enabled. |
| Heap | malloc/free/realloc/calloc allocator at 0x800000 (2MB), boundary-tag first-fit, split/coalesce, serial OOM logging, heap_walk() debug dump. |
| PFA | Bitmap-based page frame allocator, single-frame + contiguous multi-frame allocation (alloc_frames/free_frames), get_free_frame_count() query. |
| Spinlocks | SMP-safe spinlocks with irqsave/irqrestore for all shared modules (PFA, heap, paging, screen, serial, keyboard). Lock ordering enforced. |
| Shared library | lib.c/h provides memcpy/memset/strlen/strcpy/strcmp used across all kernel modules, eliminating code duplication. |
| ACPI | RSDP scan (EBDA + BIOS ROM), RSDT walk, MADT parse. APIC IDs discovered for all CPUs. Identity-maps tables above 32MB. |
| APIC | LAPIC enabled via MSR 0x1B, mapped at 0xFEE00000. I/O APIC at 0xFEC00000 routes IRQ0/IRQ1. IPI (INIT+SIPI) for AP startup. Spurious vector 0xFF. LINT0 ExtINT mode for legacy PIC passthrough. |
| SMP | Per-CPU GDT (7 entries: null, code, data, ucode, udata, TSS, percpu) with %gs base for get_cpu_id(). AP trampoline at 0x70000. INIT+SIPI protocol. APs enter idle loop with pause. Up to 8 CPUs. |
| Scheduler | Pull-based SMP work queue: scheduler_submit()/scheduler_step()/scheduler_wait_all(). State machine (free→submitted→running→free). Tasks execute on any idle CPU. smp shell command for parallel testing. |
| PCI | PCI bus scan: config space read/write via I/O ports 0xCF8/0xCFC, vendor/device/class discovery, BAR parsing for memory and I/O regions. |
| RTL8139 NIC | Polling-mode RTL8139 driver: PCI probe, MAC address read, TX descriptors with round-robin selection, RX ring buffer with CAPR-0x10 compensation for QEMU compatibility. Chipset bit definitions from Linux 8139too.c. |
| Network stack | Ethernet/ARP/IP/ICMP: ARP cache (request/reply, subnet matching), ICMP echo (ping) with checksum, polling TX/RX via net_tick(), subnet check (netmask & dst_ip == netmask & src_ip). |
| ELF loader | Loads and executes ELF binaries from NVFS into user-space, basic syscall interface via software interrupt. |
| Ring-3 user mode | User tasks run at CPL=3 via IRET with correct segment selectors (CS=0x1B, SS=0x23, DS/ES/FS/GS=0x23). Kernel API exposed via function pointer table (clear_screen, print_string, draw_pixel, malloc/free, NVFS, etc.). User-safe wrappers use spinlock_lock (no cli) to avoid GPF. Timer-driven preemptive scheduler switches between kernel (PID 1) and user tasks. |
| Shell | Command history (UP/DOWN), inline editing (LEFT/RIGHT/Backspace), Ctrl+C to cancel line, path navigation with cd, cd .., cd ./.., ls <path>, cat, echo > file, echo >> file (append), ` |
| Tool | Purpose |
|---|---|
| NASM | Assembles the bootloader to flat binary |
Clang (with -m32 support) |
Compiles kernel C and assembly to 32-bit x86 code |
| GNU ld.bfd | Links ELF kernel image |
| GNU Make | Orchestrates the build |
| QEMU (system-x86_64) | Emulates the x86 machine |
| Python 3 | Runs tools/mknvfs.py to create NVFS disk image |
Linux (Debian/Ubuntu):
sudo apt install build-essential clang nasm qemu-system-x86 python3makeProduces build/os-image.bin — a 1.44 MB floppy image and nvfs_disk.img — a 16 MB NVFS disk.
| Target | Action |
|---|---|
make / make all |
Build os-image.bin + nvfs_disk.img |
make clean |
Remove build/, images |
make run-qemu |
IDE HDD + NVFS slave disk (QEMU, -vga std -smp 2 -serial mon:stdio) |
make run-qemu-nrx |
Single-disk HDD boot with NVFS (-vga std -smp 2 -serial mon:stdio) |
make iso |
Build build/os-image.iso |
make noverix.img |
Combined disk: dd to USB for real boot |
make run-qemu # floppy + separate NVFS (default)
make run-qemu-nrx # single disk HDD bootOr directly:
IDE HDD boot (primary master) + NVFS slave:
qemu-system-x86_64 -boot order=c \
-drive format=raw,file=build/os-image.bin,if=none,id=boot \
-device ide-hd,drive=boot,bus=ide.0,unit=0 \
-drive file=nvfs_disk.img,format=raw,if=none,id=nvfs \
-device ide-hd,drive=nvfs,bus=ide.0,unit=1 \
-m 128 -smp 2 -serial mon:stdioSingle-disk HDD (combined):
qemu-system-x86_64 -boot order=c \
-drive file=noverix.img,format=raw,if=none,id=boot \
-device ide-hd,drive=boot -m 128 -smp 2 -serial mon:stdioReal hardware (USB):
sudo dd if=noverix.img of=/dev/sdX bs=512nvfs_disk.imgis a 16 MB raw image formatted as NVFS.noverix.imgis a combined disk (bootloader + kernel + NVFS) for single-device boot.- All file operations persist across reboots.
- Use
-serial mon:stdiofor serial console output with QEMU monitor access via Ctrl-A, C.
Noverix v0.1
================
Type 'help' for commands.
/$ help
Noverix Shell
----------------
clear Clear screen
echo Print text or write file (echo text > file, echo text >> file for append)
cat Display file contents (cat reads pipe when no file)
ls List files with mtime (ls, ls <dir>)
cd Change directory
mkdir Create directory
rmdir Remove empty directory
rm Delete file
hex Print a number in hex
ver Show version
sleep Sleep for N milliseconds
ata List ATA drives
crash Trigger a crash (for testing)
reboot Reboot system
shutdown Power off
exec Load and run ELF executable (exec triangle.elf)
ping Send ICMP echo request (ping 10.0.2.2)
| Command | Syntax | Description |
|---|---|---|
help, ? |
help |
Show available commands |
echo |
echo <text> |
Print text to screen |
echo (write) |
echo text > file |
Write text to a file (creates or overwrites) |
echo (append) |
echo text >> file |
Append text to an existing file |
| pipe | cmd1 | cmd2 |
Pipe cmd1 output to cmd2 (cat/echo read pipe when no arg/file) |
cat |
cat <file> |
Display file contents (no file arg → reads from pipe) |
ls |
ls [path] |
List directory contents — decimal file sizes, [DIR] prefix + <DIR> for directories |
cd |
cd [path] |
Change directory. Supports / (root), .., ./.., . |
mkdir |
mkdir <path> |
Create directory |
rmdir |
rmdir <path> |
Remove empty directory |
rm |
rm <file> |
Delete a file |
clear |
clear |
Clear screen and reset cursor |
hex |
hex <num> |
Parse decimal number and print in hex |
ver |
ver |
Print OS version |
sleep |
sleep <ms> |
Sleep for N milliseconds (1–10000) |
ata |
ata |
List detected ATA drives with model strings |
crash |
crash |
Trigger ud2 exception with register dump |
reboot |
reboot |
Reset system via keyboard controller (port 0x64, 0xFE) |
shutdown, poweroff |
shutdown |
Power off via ACPI ports (0xB004, 0x604) |
exec |
exec <file> |
Load and run an ELF executable from NVFS |
ping |
ping <ip> |
Send ICMP echo request to IP address (e.g. ping 10.0.2.2) |
heap |
heap |
Dump heap allocator block state |
mem |
mem |
Show physical memory usage (total/used/free frames) |
pages |
pages |
Show page directory/table mappings |
rate |
rate [delay rate] |
Show or set keyboard repeat rate (delay:0=250ms..3=1000ms, rate:0=fast..31=slow) |
.
├── boot/
│ ├── bootloader.asm # Stage 1 MBR: loads Stage 2 from disk, 512B, w/ partition table
│ ├── boot_stage2.asm # Stage 2: E801, VBE, A20, GDT, PM trampoline, kernel load
│ └── ap_trampoline.asm # AP startup trampoline (16-bit→PM→paging→ap_main)
├── kernel/
│ ├── entry.S # Kernel entry (_start), BSS zeroing
│ ├── kernel.c # Shell loop, command parsing, readline + SMP test commands
│ ├── lib.c / lib.h # memcpy, memset, strlen, strcpy, strcmp
│ ├── elf.c / elf.h # ELF loader for user-space executables
│ ├── sync/
│ │ └── sync.h # Spinlocks, atomic ops, memory barriers
│ ├── scheduler/
│ │ ├── scheduler.c / .h # SMP work queue (pull-based, 64 slots)
│ ├── cpu/
│ │ ├── gdt.c / gdt.h # Per-CPU GDT with TSS + per-CPU data segment
│ │ ├── idt.c / idt.h # IDT entries, exception handler, register dump, APIC EOI
│ │ ├── interrupt.S # ISR/IRQ stubs (GAS macros)
│ │ ├── ports.h # inb/outb/inw/outw inline asm
│ │ ├── timer.c / timer.h # PIT driver, sleep_ms
│ │ ├── cpu.h # cpu_info_t, MAX_CPU, get_cpu_id via %gs:0
│ │ ├── ap_startup.c / .h # AP entry (ap_main), SIPI orchestration (start_aps)
│ ├── acpi/
│ │ ├── acpi.h # RSDP, SDT, RSDT, MADT, Processor, IOAPIC structs
│ │ ├── rsdp.c # RSDP scan (EBDA + BIOS ROM)
│ │ └── madt.c # MADT parse, CPU discovery
│ ├── apic/
│ │ ├── lapic.c / .h # LAPIC init, EOI, IPI send
│ │ └── ioapic.c / .h # I/O APIC IRQ routing, IMCR
│ ├── memory/
│ │ ├── pfa.c / pfa.h # Page Frame Allocator (bitmap, 32MB)
│ │ ├── paging.c / paging.h # Paging (PD/PT, identity map, CR0.PG)
│ │ ├── heap.c / heap.h # Heap allocator (malloc/free, boundary tags)
│ ├── net/
│ │ ├── net.c / net.h # Network stack: Ethernet, ARP, IP, ICMP, ping, polling TX/RX
│ └── drivers/
│ ├── pci.c / pci.h # PCI bus probe (config space, BAR parsing)
│ ├── rtl8139.c / rtl8139.h # RTL8139 NIC driver (polling TX/RX, CAPR compensation)
│ ├── ata.c / ata.h # ATA PIO driver (LBA28, IDENTIFY)
│ ├── nvfs.c / nvfs.h # NVFS extent-based filesystem driver
│ ├── keyboard.c / keyboard.h # PS/2 keyboard driver
│ ├── screen.c / screen.h # VGA text + VBE graphics dispatch
│ ├── graphics.c / graphics.h # VBE framebuffer: pixel, rect, char, scroll
│ ├── font.h # Generated bitmap font 8×16 (95 chars)
│ └── serial.c / serial.h # COM1 serial driver
├── build/ # Build artifacts
├── tools/
│ ├── mknvfs.py # NVFS disk formatter with directory packing (16MB)
│ ├── genfont.py # VGA 8×16 bitmap font → font.h generator
├── rootfs/ # User-space ELF executables packed into NVFS
│ └── triangle.elf # Triangle test program
├── tests/
│ ├── triangle.c # Triangle test source
│ ├── app.ld # App linker script
│ └── noverix.h # App header
├── LICENSE # GPLv3
├── noverix.img # Combined disk (boot + kernel + NVFS)
├── nvfs_disk.img # 16 MB NVFS disk image
├── linker.ld # ELF linker script
├── Makefile # Build system
├── README.md
└── CODEBASE_INDEX.md
| Address | Size | Contents |
|---|---|---|
0x0500 |
~256 B | PM trampoline (32-bit, copied from Stage 2) |
0x0600 |
256 B | VBE mode info buffer (during boot) |
0x1000 |
11 B | VBE info: LFB (4B) + width (2B) + height (2B) + pitch (2B) + bpp (1B) |
0x100C |
4 B | Total RAM in bytes (from E801, set by Stage 2) |
0x2000 |
variable | Kernel destination (executed from here) |
0x70000 |
~1 KB | AP trampoline: 16-bit → PM → paging → ap_main (data at 0x70200) |
0x7C00 |
512 B | Stage 1 MBR (bootloader) |
0x7E00 |
up to 8KB | Stage 2 bootloader (loaded by Stage 1 from LBA 1-16) |
0x90000 |
variable | 32-bit stack (grows downward) |
0xA0000 |
384 KB | Legacy/BIOS/VGA (reserved) |
0xB8000 |
4 KB | VGA text framebuffer (80×25 × 2 bytes, unused in VBE mode) |
0x100000 |
~31 MB | Free physical memory (managed by PFA + identity mapped) |
0xFEC00000 |
4 KB | I/O APIC (identity-mapped) |
0xFEE00000 |
4 KB | LAPIC (identity-mapped) |
0xFD000000 |
~3 MB | VBE LFB framebuffer (identity-mapped via map_page() at kernel init) |
BIOS → 0x7C00 (Stage 1 MBR, 512B)
├── Save boot drive, set up segments
├── Read Stage 2 from LBA 1-16 via LBA (CHS fallback)
└── Jump to 0x7E00
Stage 2 (0x7E00, up to 8KB)
├── Save boot drive from Stage 1 (via 0x7BFF)
├── Print "Noverix Stage 2" banner
├── INT 0x13: loads kernel sectors to 0x9000 (LBA 17+, LBA or CHS)
├── Print "Kernel loaded"
├── VBE init: INT 0x10 mode 0x115 (800×600×24bpp LFB), info at 0x1000
├── E801 extended memory detection, store at 0x100C
├── Copy PM trampoline to 0x0500
├── A20 gate enable (port 0x92)
├── GDT load, CR0 bit 0 set
└── Far jump to PM trampoline at 0x0500
PM Trampoline (32-bit, at 0x0500)
├── Reload segment registers with DATA_SEG (0x10)
├── Set ESP = 0x90000
├── Forward rep movsd: copy kernel 0x9000 → 0x2000
└── Call 0x2000 (_start)
_start → kernel_main
├── entry.S: BSS zeroing (bss_start → bss_end)
├── init_serial() — early debug logging
├── init_gdt() / init_idt() — protected mode + interrupts
├── init_screen() / init_keyboard() / init_timer()
├── pfa_init() — page frame allocator (bitmap, mark reserved frames)
├── init_paging() — identity map 32MB, enable CR0.PG
├── VBE init: read info at 0x1000 → map LFB into page tables → init_graphics()
├── heap_init() — malloc/free at 0x800000 (2MB)
├── acpi_init() — RSDP scan → RSDT → MADT → discover CPUs
├── lapic_init() / ioapic_init() — enable LAPIC, route IRQs
├── gdt_init_percpu(0) — per-CPU GDT/TSS/%gs for BSP
├── ata_init() — probe ATA drives
├── nvfs_mount() — mount NVFS volume
├── start_aps() — INIT+SIPI → APs boot, load per-CPU GDT, enter idle
├── scheduler_init() — init SMP work queue
├── pci_init() — scan PCI bus for devices
├── rtl8139_init() — probe and init RTL8139 NIC
├── net_init() — init network stack (IP, MAC, ARP cache)
└── Shell loop (calls net_tick() each iteration for polling RX)
- No standard library — kernel is
-ffreestanding -nostdlib. Customstrlen/strcpy/strcmp. - BSS zeroing in entry.S prevents crashes when static variables extend past loaded sectors.
- Paging uses identity mapping (virt = phys) for first 32MB. Page directory at dynamically allocated physical frame.
map_page()supports custom non-identity mappings. - Heap at 0x800000–0xA00000 (2MB, identity mapped). Boundary tag allocator: each block has a 4-byte header (size + LSB alloc flag) and 4-byte footer for O(1) backward merge. Minimum allocation 12 bytes.
- ATA drive detection — NVFS driver probes all 4 ATA positions (ch 0-1, dr 0-1) at each offset (0 and 2880), mounts the first NVFS signature found. Works with any drive order.
- NVFS extent-based design — each inode holds 13 direct extents + 1 indirect block pointer (linked extents: up to 64 more extents stored in an indirect sector). Files are read/written in a single sequential pass per extent — no cluster chain walking. Simpler and faster than FAT.
- Shadow paging — writes use the pattern: alloc new blocks → write data → persist inode → free old blocks. If power fails mid-write, old data remains intact. No journaling overhead.
- Dynamic inode table — when the 128 inodes are exhausted, the driver allocates a new block from the bitmap, expands the inode table, and updates the superblock. No more fixed inode limit.
- NVFS disk format — 16 MB (32768 sectors). Bitmap covers data blocks only. Root inode is always 0. Directory entries are 32 bytes (name[28] + inode[4]), 16 per sector.
- Serial input — shell accepts input from both keyboard and COM1 serial port. Use
-serial mon:stdiofor headless operation and scripting. - VBE Graphics — bootloader sets VBE mode 0x118 (1024×768×24bpp LFB) via INT 0x10, stores mode info at
0x1000(LFB, width, height, pitch, bpp). Kernel reads this info, maps the LFB into page tables viamap_page()(576 pages for 3MB framebuffer), and callsinit_graphics()fromgraphics.c. The screen driver (screen.c) dispatchesprint_char/clear_screen/scrollto either VGA text (0xB8000) or VBE framebuffer based onis_graphics_active(). Font is a generated 8×16 bitmap atkernel/drivers/font.hfromtools/genfont.py. Use QEMU's-vga stdto enable. - Security hardening — bounds checking on shell command parser (echo redirection), history buffer overflow protection, integer overflow guards in hex/sleep commands, heap underflow guard in free(), frame 0 marking fix in PFA, capture buffer bounds protection in screen driver, atomic interrupt handler registration (pushf/popf),
-Werrorin CFLAGS. - SMP startup — BSP discovers CPUs via ACPI MADT, starts APs via INIT+SIPI IPI protocol. AP trampoline at 0x70000 transitions from real mode through protected mode with paging to calling
ap_main(). Each CPU has its own GDT (7 entries: null, code, data, ucode, udata, TSS, percpu) and%gspoints to itscpu_info_tforget_cpu_id(). - Spinlocks — all shared modules (PFA, heap, paging, screen, serial, keyboard) use
irqsavespinlocks. Lock ordering:screen_lock → serial_lock,paging_lock → pfa_lock. No circular dependencies. - IRQ routing on SMP — QEMU routes ISA interrupts through PIC → LAPIC LINT0 (virtual wire mode), not through I/O APIC redirection entries. LINT0 must be ExtINT mode and PIC must stay unmasked for PIT/keyboard to work. All other I/O APIC redirections are masked.
- SMP scheduler — pull-based work queue (not preemptive). BSP submits tasks via
scheduler_submit(), idle CPUs pick them viascheduler_step(). AP idle loop usespause(nothlt) because I/O APIC only delivers IRQ0/IRQ1 to CPU 0. - Ring-3 user mode — ELF loader creates ring-3 tasks via IRET with correct segment selectors. Kernel API (screen, keyboard, heap, NVFS, graphics) exposed via function pointer table. User-safe API wrappers use no locks — at CPL=3, IF=1 means a timer ISR can preempt while a lock is held, leading to deadlock when another task tries the same lock via
spinlock_lock_irqsave(CLI + spin). Instead, per-pixel framebuffer writes and cursor_x/y access are lock-free (best-effort accuracy acceptable). Keyboard ISR usesspinlock_try_lockto avoid deadlock with user-heldkb_lock. VBE framebuffer mapped withPAGE_USER. Timer-driven preemptive round-robin scheduler switches between shell (PID 1) and user tasks (PID 2+). Per-process page directories isolate each user task's address space (0x800000–0xA00000). - Multi-stage boot — Stage 1 (512B MBR) loads Stage 2 from LBA 1-16 via LBA/CHS. Stage 2 (up to 8KB at 0x7E00) reads kernel from LBA 17+ (KERNEL_LBA = 1 + STAGE2_SECTORS), probes RAM via E801, sets VBE, A20, GDT, and transitions to 32-bit PM. Stage 2 has no 512B size limit — enables verbose debug output and easier maintenance. Boot drive passed from Stage 1 to Stage 2 via fixed memory address 0x7BFF.
- PCI driver —
pci_init()scans bus 0 for all 32 devices × 8 functions. Config space accessed via I/O ports 0xCF8/0xCFC. BAR0 parsed for memory-mapped or I/O-mapped region. Vendor/device/class info logged to serial. - RTL8139 NIC — Polling-mode driver initialized by
rtl8139_init()after PCI probe. Configures RCR (Accept All Packets), sets TxConfig (IFG96 + DMA burst + retry), reads MAC address from PROM. TX uses 4 descriptors with round-robin selection (tx_next_idx) to match QEMU's single-descriptorcurrTxDesc. RX uses ring buffer with CAPR compensation (QEMU internally adds 0x10 to CAPR; driver writesrx_offset - 0x10to avoid buffer-full deadlock). Bit definitions from Linux8139too.c:TxHostOwns=0x2000,TxStatOK=0x8000,TxUnderrun=0x4000,AAP=0x01,AB=0x08. - Network stack —
net_init()configures local IP, netmask, MAC.net_tick()(called from shell loop) polls NIC for RX packets, dispatches Ethernet type (ARP/IP), and sends pending TX packets. ARP cache stores resolved MAC/IP pairs. ICMP echo replies are handled automatically.pingshell command sends ICMP echo request with subnet check. All IP addresses in C code use host byte order;net_htonl/net_ntohlonly at the hardware boundary. QEMU user-mode networking via-nic user,model=rtl8139provides gateway (10.0.2.2) and DNS (10.0.2.3).