Skip to content

daedalus/myos

Repository files navigation

myos

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.

  ███╗   ███╗██╗   ██╗ ██████╗ ███████╗
  ████╗ ████║╚██╗ ██╔╝██╔═══██╗██╔════╝
  ██╔████╔██║ ╚████╔╝ ██║   ██║███████╗
  ██║╚██╔╝██║  ╚██╔╝  ██║   ██║╚════██║
  ██║ ╚═╝ ██║   ██║   ╚██████╔╝███████║
  ╚═╝     ╚═╝   ╚═╝    ╚═════╝ ╚══════╝

Quick start

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/ directory

The 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.


Requirements

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.


Project layout

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

Architecture

Boot sequence

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)

Memory map

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.

Subsystem dependency order

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)

Implemented syscalls

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.

VFS

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.


Debugging

QEMU + GDB

./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

Serial output

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.

Page fault output

*** KERNEL PANIC ***
Page fault at RIP=0xffffffff80101234  CR2=0x0000000000000000
  not-present | read | kernel
  RSP=0xffffffff80103ff0  RFLAGS=0x0000000000000246

Extending the kernel

Adding a syscall

  1. Add the number to syscall/syscall.c (#define SYS_FOO N)
  2. Implement static int64_t sys_foo(...) in the same file
  3. Register it: syscall_table[SYS_FOO] = (SyscallFn)sys_foo;

Adding a filesystem

  1. Implement InodeOps and FileOps vtables
  2. Write a yourfs_mount() that returns a Superblock*
  3. Call vfs_mount("/mountpoint", yourfs_mount()) from kmain

Adding a device driver

  1. Create drivers/yourdevice/yourdevice.c
  2. Initialise in kmain after interrupts are enabled
  3. For interrupt-driven devices: idt_register_handler(vector, handler)

Running tests

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_pmm

Current test coverage: PMM bitmap operations (allocation, free, double-free detection, OOM), string library (memset, memcpy, memcmp, strlen, strcmp, strchr).


Next steps

The kernel is a functional skeleton. The natural progression from here:

  1. execve wiring — connect elf_load() into the sys_execve syscall handler and set up the user-mode jump
  2. Port musl libc — compile musl with -static against the kernel's syscall ABI; write + exit is enough to run "Hello, world"
  3. mmap / brk — required by musl's allocator; implement with vmm_map backing
  4. Shell — once musl works, a minimal /bin/sh (e.g. dash compiled statically) should run
  5. Block device + ext2 — persistence; implement the FileOps vtable backed by a virtio-blk or ATA PIO driver
  6. SMP — each core needs its own GDT entry, TSS, LAPIC, and current pointer in GS.base
  7. Copy-on-write fork — replace the physical page copy in proc_fork with PTE write-protection + fault-driven duplication

License

MIT. Do whatever you want with it.

About

POSIX-like x86-64 kernel skeleton

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages