Skip to content
0xmortuexPublic

About

A graphical x86 desktop OS written entirely in Mort, my own language: terminal, files, filesystem, TCP/IP stack. Boots in QEMU (BIOS/UEFI ISO).

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

MORT OS

license arch lang

I wrote a programming language, then wrote an operating system in it. MORT OS is a graphical x86 OS with a shell, a file browser, a filesystem that survives reboots and a TCP/IP stack that serves web pages, and every line above the boot stub is written in Mort, a language I built from scratch that compiles to C. Its interface is not a desktop: it is a geological cross-section you drill through.

The Borehole start screen: a dusk sky and a derrick over four rock strata labelled Machine, Objects, Run and Terminate, with M O R T carved down the right edge and a drill bit in the shaft
A real boot in QEMU: the start screen. Four strata spell M-O-R-T.

60-second quickstart

You need uv, git and QEMU (qemu-system-i386), on Linux or Windows. One command builds the kernel with the pinned Mort compiler and boots it to a shell on your terminal:

git clone https://github.com/0xmortuex/MortOS && cd MortOS
uv run --no-project --with ziglang --with cryptography python build.py boot

The build fetches the Mort compiler at the commit pinned in build.py (MORT_COMMIT), so a clean checkout always builds with the same compiler. At the ~ $ prompt try help, ls /, write hello.txt hi, cat hello.txt and exec /bin/hello.bin; Ctrl+A then X quits QEMU. Replace boot with boot-check to build, boot, and exit 0 as soon as the shell prompt is up.

To see the graphical Borehole interface instead, build a bootable ISO and boot it in a QEMU window (build.py iso fetches the Limine bootloader and, on Windows, xorriso; on Linux, install xorriso first):

uv run --no-project --with ziglang --with cryptography python build.py run-iso

It boots to the Borehole start screen (see docs/borehole-concept.md): press Enter for a shell, then try help, ls, write hello.txt hi and cat hello.txt. Esc climbs back out; M, O, R and T drill to the Machine, Objects, Run and Terminate strata. The ISO is a BIOS/UEFI hybrid built to be written to a USB stick, but I have only tested it in QEMU.


An operating-system kernel written in Mort — my own programming language. It boots in QEMU (and the ISO is built to boot from a USB stick, though I have only tested QEMU), runs in 32-bit protected mode, and draws the Borehole interface (docs/borehole-concept.md) to a linear framebuffer in a bitmap font. It has a real filesystem — write a file, reboot the machine, and it's still there — it runs real, interactive compiled programs (a .mx program compiled to a flat binary, loaded off the disk, talking to the kernel through int 0x80 syscalls), and it speaks TCP/IP: the mortnet stack is vendored into the kernel, so MORT OS gets its own IP over DHCP and runs a web server. Everything above the boot stub — the framebuffer renderer, PS/2 keyboard driver, interrupt handlers, ATA disk driver, the filesystem, the syscall layer, the RTL8139 network driver, the shell — is written in Mort.

Session & power

The bottom stratum, T, holds Lock, Sleep, Restart and Shut down. Lock seals the screen in granite until the account password is given (mort); Sleep leaves one banked ember until a key is pressed; Shut down powers the machine off (via ACPI on emulators, or an "It is now safe to turn off your computer" halt on bare metal). The clock at the top right reads the CMOS RTC. Everything is a shell command too: lock, sleep, restart, shutdown, power.

The Terminate stratum: Lock, Sleep, Restart and Shut down as beds of rock

The lock screen: the word SEALED carved in granite above a password slit

Networking — it serves a web page

net brings up the RTL8139 NIC and leases an address over DHCP; httpd then serves an HTML page on port 80. This is the mortnet stack — NIC driver, ARP, IPv4, ICMP, UDP, DHCP, DNS, TCP, HTTP, all written from scratch in Mort — vendored into net/ and compiled into the kernel. See docs/networking.md for which file implements each layer and how net/httpd actually drive it.

MORT OS networking: net leases an address over DHCP in the shell gallery

A web server, on an OS, over a TCP/IP stack — every layer in one language. Networking needs an RTL8139 NIC (QEMU emulates one). The rest of the OS does not need a network card.

The shell in its gallery, cut out of the shale stratum

What it does

  • The Borehole interface (ui/borehole.mx) — a multiboot linear framebuffer, an 8×16 bitmap font renderer, and one idea: everything is depth. Four strata hold the Machine (live readings and hardware actions), Objects (MortFS: directories are cavities you bore into, files are beds whose thickness grows with their size), Run (the shell, in a gallery cut from the rock) and Terminate. Opening a file pulls a core sample. Up/Down, Enter and Esc reach everything; M, O, R and T drill straight to a stratum. Scenes are composed off-screen and revealed behind the drill bit. See docs/borehole-concept.md. The shell renders through the same four console calls in both modes, and falls back to VGA text when no framebuffer is present (the bare -kernel path).

The Objects stratum: directories as cavities, files as beds of rock

A core sample: a file's real contents on a pale slab of rock

  • A real filesystem (MortFS) — an ATA PIO disk driver and an on-disk format, both written in Mort. ls, cat <file>, write <file> <text>, rm <file>, and run <file> (execute a file of shell commands). Files persist across reboots — write a note, reboot QEMU, cat it back.
  • Runs real, interactive compiled programs — exec <file> loads a Mort program (compiled to a flat binary) off the disk to 0x00A00000 and runs it. Programs share no symbols with the kernel; they call it through int 0x80 syscalls (args passed via a fixed mailbox, since Mort's asm() takes no operands). A read-line syscall polls the keyboard directly, so programs can take input too — exec /bin/ask.bin asks your name and greets you. Sample programs are in programs/. See docs/memory-map.md for every fixed physical address the kernel uses.
  • Users, login, and file permissions — two built-in accounts, root and mortuex, with djb2-hashed passwords (acct_init, kmain.mx:2904); the shell auto-logs-in as the normal user and starts in /home/mortuex (login_default/cwd_init, kmain.mx:2910/2916), with whoami, su, sudo, and passwd on top. Every MortFS entry carries an owner uid and Unix-style mode bits, enforced on entry creation, write, and rm by can_write (kmain.mx:2978). See docs/accounts.md for the model, and for which bits are enforced versus only displayed.
  • A real bootable ISO — a BIOS+UEFI hybrid ISO (Limine bootloader) built to be written to a USB stick, not just QEMU's -kernel shortcut (tested in QEMU only; not yet verified on physical hardware)
  • Interrupt-driven keyboard — a flat GDT, an IDT, remapped PICs; IRQ1 fires into a Mort handler (no polling)
  • A shell — command parsing, Backspace line editing, Shift-aware scancode→ASCII, and command history (Up/Down arrows, decoded from 0xE0 extended scancodes). See docs/shell.md for the full command reference.
  • PIT timer on IRQ0 (~100 Hz) with an uptime command
  • CPU exception handlers — per-vector stubs that report which fault occurred (try the crash command)
  • Terminal scrolling and a cursor that tracks input (a drawn underline in graphics, the hardware cursor in text mode)
  • Multiboot modules — the bootloader passes ISO files as modules; readme prints one, and a second acts as a boot script the kernel runs at startup like an /etc/rc
  • Tested in CI-style headless runs — test.py and test_fs.py boot the real kernel in QEMU, inject keystrokes through the monitor, and assert on VGA memory (including the write→reboot→cat persistence path)
  • PCI hardware detection, USB, and audio — hw_scan_pci (net/hardware.mx:12) walks the PCI bus for Ethernet/Wi-Fi/audio/USB controllers; a UHCI USB 1.1 host-controller driver resets the controller and enumerates one root-port device at boot (usb_boot_init, net/hci_usb.mx:195, called from kmain() at kmain.mx:3620), setting a Bluetooth-HCI flag if the enumerated device's class matches (net/hci_usb.mx:155); an Intel AC'97 driver does PCI discovery, mixer volume, and PCM-out DMA (ac97_init, net/audio.mx:9), alongside a PIT-driven legacy PC-speaker beeper (speaker_start/speaker_stop, net/hardware.mx:37/47). All of it is surfaced live in the Machine stratum (docs/settings.md). See docs/hardware.md for how the PCI scan, USB enumeration, and AC'97 init actually work.

How it fits together

See docs/architecture.md for the full boot chain, execution model, and how the subsystems below connect, cited to source.

kmain.mx    the kernel, in Mort        ─┐
            │  mortc --freestanding      │  Mort -> freestanding C -> 32-bit object
            ▼                            │
boot.s      multiboot header + _start   ─┤  assembled to a 32-bit object
            │                            │
linker.ld   places it at 1 MB           ─┘  linked ->  build/kernel.elf
            │
            ▼
qemu-system-i386 -kernel build/kernel.elf

Why does a hobby language compile to C instead of running on an interpreter? Because an interpreter can't boot. Mort emits freestanding-friendly C, so hello.mx and this kernel go through the exact same compiler.

Build & run

Requirements: Python 3, pip install ziglang cryptography (ziglang is the C cross-compiler; the Mort compiler imports cryptography), and QEMU for booting. The Mort compiler is fetched automatically into .mort/ at the commit pinned by MORT_COMMIT in build.py (or set MORT_HOME, or keep a ../Mort checkout, to build with your own compiler instead; the build prints a note when that differs from the pin).

python build.py check     # build + verify it's a valid 32-bit multiboot ELF
python build.py run       # build, then boot it fullscreen in QEMU (with the disk)
python build.py window    # same as run, but in a resizable window instead of fullscreen

The first thing on screen is the boot banner, printed at kmain.mx:3603, then the shell prompt — nothing else runs automatically on this bare -kernel path, since run_script(1) (kmain.mx:884) finds no multiboot module without an ISO and returns immediately:

MORT OS -- interrupt-driven. type 'help', Enter
~ $

(~ $ is the auto-logged-in user's home-directory prompt, drawn by draw_prompt, kmain.mx:709-727.)

That banner and prompt are what you're reading in the QEMU display window itself. All console output goes through either VGA text memory (put_index) or the drawn framebuffer console (draw_glyph), both funneled through put_cell_at, which also mirrors it to COM1 (a 16550 UART at 0x3F8, 115200 8N1). Add -serial stdio or a -serial tcp: port to QEMU to get the same shell on a serial line; the serial command prints its loss counters.

For scripted serial input on Windows, pace stdio byte by byte: QEMU's Windows stdio backend drops bytes from a one-shot write. Over TCP, a burst over about 512 bytes (the receive ring) can lose bytes unless the sender paces itself or waits for the prompt after each command; serial counts the drops. See ARCHITECTURE.md.

run auto-creates build/disk.img (16 MiB, MortFS) and attaches it, so files you write in the OS survive reboots. python mkfs.py build/disk.img wipes it clean (add --add host.txt:name.txt to seed files). Try it:

> write notes.txt hello from mort os
> cat notes.txt
> write job.txt echo hi
> write job.txt uptime
> run job.txt

Running programs

MORT OS runs real compiled programs, not just shell scripts. Sources live in programs/. The first time any disk-creating command runs — run, window, disk, or run-iso — ensure_disk() (build.py:341-365) compiles every programs/*.mx and seeds the resulting binaries onto build/disk.img, so after your first python build.py run the shell can already see them:

> exec /bin/hello.bin     # a real Mort program prints via syscall, then returns
> exec /bin/count.bin     # a second program, proving more than one builds and runs
> exec /bin/ask.bin       # an interactive program: it asks your name and greets you

python build.py prog compiles programs/*.mx into loose build/*.bin files for inspection, but — as the note below explains — it never touches disk.img, so it's not what makes exec able to find a program:

python build.py prog      # compile programs/*.mx -> build/*.bin

Note: program binaries are seeded onto build/disk.img only once, when ensure_disk() first creates it (build.py:341-365) — editing a program and re-running python build.py prog alone won't update what exec sees inside a running OS, since prog never touches the disk image. Delete build/disk.img to get a rebuilt program back onto disk; see docs/programs.md for the details.

python build.py disk (disk(), build.py:368-374) creates and seeds build/disk.img the same way run/window/run-iso do, but without building the kernel or starting QEMU — the standalone way to get (or rebuild, after deleting it) the disk image on its own:

python build.py disk      # create build/disk.img (seeded with programs) without booting

If the image already exists, it prints a one-line notice and leaves it alone rather than overwriting it, pointing at python mkfs.py build/disk.img as the explicit wipe path (build.py:369-372) — the same no-clobber behavior ensure_disk() has everywhere else.

See docs/programs.md for the syscall ABI and how to write your own program.

Automated tests (all drive the real kernel headless in QEMU):

python test.py smoke      # boot + shell basics (text-mode -kernel path)
python test_fs.py         # the disk stack, incl. write-reboot-cat persistence
python test_exec.py       # build programs, seed, boot, exec, check syscall output
python test_gfx.py        # boot the ISO, drive the Borehole interface, assert on screendumps and guest memory

See docs/testing.md for what each script actually checks and the keyboard/screen-scrape harness they share.

The Borehole interface comes up on the ISO path (run-iso), where Limine provides the framebuffer. The bare -kernel path has no framebuffer, so the kernel falls back to VGA text mode there — which is why the text-driven tests above still work.

A real bootable ISO

python build.py iso       # -> build/mort.iso (BIOS + UEFI hybrid, Limine + xorriso)
python build.py run-iso   # build the ISO, then boot it in QEMU

These two commands, and the ISO paths of test_gfx.py and test_serial.py, work on Windows and Linux; CI builds the ISO and boots it on windows-latest and ubuntu-latest. iso() in build.py fetches Limine at a pinned commit. On Windows it also fetches a pinned xorriso.exe and runs both .exe tools directly. On Linux, install xorriso first (apt install xorriso); Limine's host tool is built with Zig from the limine.c in the same download. macOS is untested. build/check/run/window/disk/prog only need Zig (via ziglang) and QEMU.

This path boots to the Borehole start screen (ui_start, ui/borehole.mx) instead of a bare prompt — press Enter to bore into the shell, where the banner and the startup script baked into the ISO (STARTUP_TXT, build.py:88-90, run via run_script(1)) have already executed:

MORT OS -- interrupt-driven. type 'help', Enter
~ $ echo running startup script loaded from disk...
running startup script loaded from disk...
~ $ about
MORT OS -- written in Mort, a language built from scratch
~ $

run-iso's QEMU window also behaves differently from run/window's, which this README never pointed out: _run() (build.py:403-421), used by both run and window, always passes -display gtk,zoom-to-fit=on,show-menubar=off (build.py:410-412) — a borderless window that scales the console to fit — and adds -full-screen for run specifically (build.py:413-414). run_iso() (build.py:377-384) passes neither: its QEMU invocation is just [qemu, "-cdrom", ISO, "-hda", DISK] (build.py:384), with no -display argument at all, so it boots in QEMU's own default display backend and window size, menu bar included, and never fullscreen. test_gfx.py boots the same ISO a third way — headless, with its own -display none (see docs/testing.md) — so this gap only affects a human running python build.py run-iso directly.

Write mort.iso byte-for-byte to a USB stick (e.g. Rufus in "DD image" mode) and it is built to boot from it. Not yet verified on physical hardware (QEMU only so far), so treat that as untested.

Roadmap

  • Everything above (the Borehole interface, ATA driver, MortFS, exec-ing real programs)
  • Space reclamation for rm (the fixed-extent bump allocator never reclaims, in v1 or v2; re-mkfs to compact)
  • More syscalls (file I/O from programs, spawn) and a richer program ABI
  • A mouse (PS/2 IRQ12) for the Borehole interface

Related

  • Mort — the language this kernel is written in: lexer, parser, type checker, and C code generator, from scratch in Python with zero libraries.

About

A graphical x86 desktop OS written entirely in Mort, my own language: terminal, files, filesystem, TCP/IP stack. Boots in QEMU (BIOS/UEFI ISO).

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages