A POSIX-like x86-64 kernel built from scratch. Boots via Limine, runs in QEMU, and implements the core subsystems needed to eventually host a shell.
███╗ ███╗██╗ ██╗ ██████╗ ███████╗
████╗ ████║╚██╗ ██╔╝██╔═══██╗██╔════╝
██╔████╔██║ ╚████╔╝ ██║ ██║███████╗
██║╚██╔╝██║ ╚██╔╝ ██║ ██║╚════██║
██║ ╚═╝ ██║ ██║ ╚██████╔╝███████║
╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝
git clone <this-repo> myos
cd myos
chmod +x build.sh
./build.sh # install deps, build cross-compiler, compile, produce ISO
./build.sh run # all of the above + boot in QEMU
./build.sh debug # boot with GDB stub frozen at entry on :1234
./build.sh test # run native unit tests only (no cross-compiler needed)
./build.sh clean # remove build/ directoryThe build script is self-contained. On a fresh Ubuntu/Debian, Arch, Fedora, or macOS machine it will install all system packages, build a cross-compiler, fetch Limine, and produce a bootable ISO — no manual setup required.
The build script handles installation automatically. For reference, the manual list:
| Tool | Version | Purpose |
|---|---|---|
x86_64-elf-gcc |
≥ 12 | Cross-compiler (kernel C code) |
x86_64-elf-ld |
≥ 2.40 | Cross-linker |
nasm |
≥ 2.15 | Assembler (boot stubs, IDT, context switch) |
xorriso |
any | ISO image creation |
qemu-system-x86_64 |
≥ 7.0 | Testing |
gdb / x86_64-elf-gdb |
any | Debugging |
git |
any | Fetching Limine |
On macOS, brew install x86_64-elf-gcc x86_64-elf-binutils nasm xorriso qemu covers everything.
myos/
├── arch/x86_64/
│ ├── boot/ GDT, TSS, kernel entry point (entry.asm)
│ ├── irq/ IDT, interrupt stubs (256 entries), PIT timer, page fault
│ └── mm/ 4-level page table walker (PML4 → PDPT → PD → PT)
├── kernel/
│ ├── kmain.c Boot sequence — wires all subsystems together
│ ├── mm/ Physical memory manager (bitmap), kernel heap (kmalloc)
│ ├── proc/ Process lifecycle (fork/exit/wait), ELF loader
│ └── sched/ Round-robin scheduler, context switch (assembly)
├── fs/
│ ├── vfs/ Virtual file system — path resolution, mount table
│ └── ramfs/ In-memory filesystem (no disk required)
├── syscall/ Dispatch table + syscall/sysret entry (assembly)
├── drivers/tty/ Serial (COM1) driver — used for all kernel output
├── lib/ kprintf, kernel string library (no libc)
├── include/ Headers (kernel/, arch/x86_64/, posix/, drivers/)
├── tests/ Native userspace unit tests (no cross-compiler needed)
├── linker.ld Places kernel at 0xFFFFFFFF80000000 (higher half)
├── limine.cfg Bootloader config
├── Makefile Alternative to build.sh for incremental builds
└── build.sh Full self-contained build + setup script
BIOS/UEFI
└─ Limine (bootloader)
└─ entry.asm (_start) sets up 32 KiB boot stack
└─ kmain()
├─ serial_init() COM1, 38400 baud
├─ gdt_init() kernel/user segments + TSS
├─ idt_init() 256 interrupt gates
├─ page_fault_init() #PF handler → kpanic with CR2 dump
├─ pmm_init() bitmap allocator over Limine memmap
├─ vmm_init() higher-half mapping, enable NX, load CR3
├─ kmalloc_init() kernel heap at 0xFFFF900000000000
├─ sti enable hardware interrupts
├─ timer_init(1000 Hz) PIT + PIC remap → vector 32
├─ syscall_init() dispatch table (256 entries)
├─ syscall_arch_init() EFER/STAR/LSTAR/SFMASK MSRs
├─ vfs_init() + ramfs mount ramfs at /
├─ proc_init() PID table
├─ sched_init() round-robin run queue
└─ sched_yield() → init_thread (pid 1)
Virtual address space (x86-64, 48-bit canonical)
0x0000000000000000 ── user space bottom
(process text, data, heap, stack)
0x00007FFFFFFFFFFF ── user space top
user stack at 0x00007FFFFFFFE000 downward
── non-canonical gap ──
0xFFFF800000000000 ── kernel space starts
0xFFFF900000000000 ── kmalloc heap (256 MiB)
0xFFFF910000000000 ── per-process kernel stacks
0xFFFFFFFF80000000 ── kernel image (higher half, −2 GiB)
0xFFFFFFFF80100000 ── _kernel_start (text, rodata, data, bss)
0xFFFFFFFFFFFFFFFF ── top
Physical memory is bitmap-managed. One bit per 4 KiB page, supporting up to 32 GiB of RAM. The bitmap itself is placed just after the kernel image.
Each layer depends only on layers above it in this list:
Serial output (no dependencies — first thing initialised)
GDT / TSS (needed for privilege level transitions)
IDT (needs GDT selectors for gate descriptors)
PMM (needs nothing except the memory map)
VMM (needs PMM to allocate page table pages)
kmalloc (needs VMM to map heap pages)
Interrupts enabled
PIT timer (needs IDT to register handler)
Syscall interface (needs GDT for STAR MSR selectors)
VFS + ramfs (needs kmalloc)
Process table (needs kmalloc, VMM)
Scheduler (needs process table)
| Tier | Syscalls | Status |
|---|---|---|
| 1 | write, exit, exit_group |
✅ |
| 2 | getpid, getppid |
✅ |
| 3 | open, close, read, lseek |
✅ |
| 4 | fork, execve, wait4 |
Skeleton (fork/exit/wait in proc.c, execve needs wiring) |
| 5+ | mmap, brk, signals, threads |
Stub (returns -ENOSYS) |
All unimplemented syscalls return -ENOSYS and log the syscall number.
The VFS implements a Unix-style namespace:
vfs_open("/etc/motd")
└─ path_walk()
├─ tokenise path into components
├─ resolve each component via dentry cache
│ └─ on miss: call inode->i_ops->lookup()
└─ return File* with ops vtable
Filesystems implement two vtables — InodeOps (lookup, create, mkdir, unlink) and FileOps (read, write, seek) — and register a Superblock via vfs_mount(). Currently only ramfs is implemented; adding ext2 or procfs is a matter of implementing the two vtables.
./build.sh debug
# In another terminal:
x86_64-elf-gdb build/myos.elf \
-ex "target remote :1234" \
-ex "break kmain" \
-ex "continue"Useful GDB commands for kernel debugging:
(gdb) info registers # all GP registers
(gdb) x/10i $rip # disassemble around current instruction
(gdb) x/20gx $rsp # dump stack (64-bit words)
(gdb) p *current # print current process PCB
(gdb) break interrupt_dispatch # catch all interrupts
(gdb) watch kernel_pml4 # watch for CR3 changes
All kernel output goes to COM1 (QEMU -serial stdio). The kprintf format string supports: %d, %u, %x, %X, %p, %s, %c, %lld, %llu, %llx, width and zero-padding (%016llx).
A kernel panic dumps all 16 GP registers plus RIP, CS, RFLAGS, RSP, SS, then halts with cli; hlt.
*** KERNEL PANIC ***
Page fault at RIP=0xffffffff80101234 CR2=0x0000000000000000
not-present | read | kernel
RSP=0xffffffff80103ff0 RFLAGS=0x0000000000000246
- Add the number to
syscall/syscall.c(#define SYS_FOO N) - Implement
static int64_t sys_foo(...)in the same file - Register it:
syscall_table[SYS_FOO] = (SyscallFn)sys_foo;
- Implement
InodeOpsandFileOpsvtables - Write a
yourfs_mount()that returns aSuperblock* - Call
vfs_mount("/mountpoint", yourfs_mount())fromkmain
- Create
drivers/yourdevice/yourdevice.c - Initialise in
kmainafter interrupts are enabled - For interrupt-driven devices:
idt_register_handler(vector, handler)
The unit tests compile and run natively (no cross-compiler, no QEMU):
./build.sh test
# or manually:
gcc -std=c11 -Wall -o /tmp/test_pmm tests/test_pmm.c && /tmp/test_pmmCurrent test coverage: PMM bitmap operations (allocation, free, double-free detection, OOM), string library (memset, memcpy, memcmp, strlen, strcmp, strchr).
The kernel is a functional skeleton. The natural progression from here:
execvewiring — connectelf_load()into thesys_execvesyscall handler and set up the user-mode jump- Port musl libc — compile musl with
-staticagainst the kernel's syscall ABI;write+exitis enough to run "Hello, world" mmap/brk— required by musl's allocator; implement withvmm_mapbacking- Shell — once musl works, a minimal
/bin/sh(e.g. dash compiled statically) should run - Block device + ext2 — persistence; implement the
FileOpsvtable backed by a virtio-blk or ATA PIO driver - SMP — each core needs its own GDT entry, TSS, LAPIC, and
currentpointer in GS.base - Copy-on-write fork — replace the physical page copy in
proc_forkwith PTE write-protection + fault-driven duplication
MIT. Do whatever you want with it.