← Portfolio Home
Builds / Operating Systems

Hermes OS

A 32-bit x86 operating system kernel written from scratch — no libc, no host runtime, no framework. Boots under GRUB, sets up its own descriptor tables, services its own hardware interrupts, manages its own memory, and drops you into an interactive shell.

● Working — boots in QEMU Interactive shell verified NASM + freestanding C i386 protected mode
2,500+lines of code
15source files
10subsystems
9shell commands
0external deps

Live console — real captured output

qemu-system-i386 — oskernel.elf — serial0
=== Hermes OS Shell ===
Type 'help' for available commands.

$ help
Available commands:
  help        Show this help message
  hello       Print hello world
  echo        Echo text to screen
  uptime      Show system uptime
  ps          List running tasks
  ls          List files in filesystem
  cat         Display file contents
  mem         Show memory info
  reboot      Reboot the system
$ hello
Hello, World! Welcome to Hermes OS.
$ echo hermes os
hermes os
$ uptime
Uptime: 15.7 seconds (1575 ticks)
$ ps
PID  STATE  NAME
---  -----  ----
 0    READY demo1
 1    READY demo2
 2    READY demo3
$ ls
NAME                        TYPE      SIZE
----                        ----      ----
README                      FILE  665
VERSION                     FILE  60
HELLO                       FILE  37
docs                        DIR   0
$ cat README
====================================
  Hermes OS - x86 Kernel from Scratch
====================================

This is a minimal x86 operating system kernel
built from scratch featuring:

  [BOOT] Multiboot-compliant bootloader
  [GDT]  Protected mode with segmentation
  [IDT]  Interrupt handling (32 IRQs + 16 exceptions)
  [PIC]  Programmable Interrupt Controller
  [TIM]  PIT timer with 100Hz scheduling tick
  [KBD]  PS/2 keyboard driver
  [MMU]  Physical memory manager + paging
  [SCH]  Preemptive round-robin scheduler
  [FS]   Simple in-memory filesystem
  [SH]   Interactive command shell

Commands: help, hello, echo, uptime, ps, ls, cat, mem, reboot

$ mem
Free memory blocks: 15784 (63136 KB)
$

Captured from the serial port of a real qemu-system-i386 boot, with keystrokes injected as genuine PS/2 scancodes through the QEMU monitor — the same path a physical keyboard would take.

Boot flow

GRUB → multiboot header → _start (32-bit) → GDT → IDT → PIC remap → PIT 100 Hz → PS/2 keyboard → PMM + paging → filesystem → shell

Subsystems

Bootboot/boot.asm

Multiboot header, GRUB-loadable, enters 32-bit protected mode at 1 MB

GDTkernel/gdt.c

5 descriptors — null, ring-0 code/data, ring-3 code/data

IDTkernel/idt.c

256 gates: 32 CPU exceptions + 16 hardware IRQs

PICkernel/irq.c

8259A remapped to vectors 32–47, per-IRQ handler registry

Timerkernel/timer.c

PIT channel 0 at 100 Hz, tick counter driving uptime

Keyboardkernel/keyboard.c

8042 PS/2 init, scancode→ASCII, shift/caps, ring buffer

Memorykernel/pmm.c

Bitmap physical allocator + x86 paging, 16 MB identity-mapped

Schedulerkernel/scheduler.c

Round-robin task table, TCBs, assembly context switch

Filesystemkernel/fs.c

In-memory file table with read/list/stat

Shellkernel/shell.c

9 built-in commands, line editing, VGA + serial output

Three bugs that actually had to be debugged

The interrupt handler read garbage

Every IRQ is dispatched as irq_handler(registers_t*), but the assembly stub called irq_handler without pushing the frame pointer. The C code then read “int_no” out of whatever happened to be on the stack and indexed the handler table with it — a page fault at 0xc014d3d0, recursing until the stack hit an unmapped page and triple-faulted. Fix: push esp before the call in both the ISR and IRQ stubs.

Two bits of the PIC mask

0xFB looks like “enable IRQ0 and IRQ1” but the 8259A mask is inverted — 1 means masked. That value actually disabled both the timer and the keyboard. Correct mask: 0xFC.

The CPU slept through every keystroke

The idle loop used hlt, which only wakes on an interrupt. With IRQ0 and IRQ1 masked and the UART’s receive interrupt off, nothing ever woke it — so polled serial input was never read. Keeping the 100 Hz timer unmasked fixes it, and additionally required enabling the 8042 controller’s keyboard-interrupt bit (config byte bit 0) during init.

Build & run

# toolchain: nasm, clang (freestanding), x86_64-elf-ld, qemu
make                                  # → oskernel.elf (~37 KB)
qemu-system-i386 -kernel oskernel.elf -m 64

# headless, with the shell on your terminal:
qemu-system-i386 -kernel oskernel.elf -m 64 -serial stdio -display none

Shell commands

CommandWhat it doesProves
helpList all commandsShell command table
helloPrint a greetingVGA + serial output
echo …Echo arguments backLine parsing
uptimeTicks and seconds since bootPIT + IRQ0 firing
psTask table with stateScheduler structures
lsFiles with type and sizeFilesystem directory scan
cat <file>Print file contentsFilesystem read path
memFree blocks and KBBitmap allocator
rebootTriple-fault resetCPU control
Built by Isaac · Hermes OS · source in ~/Documents/projects/drafts/oskernel