How os8088 works

os8088 is 14,376 bytes of machine code: 11,239 lines of hand-written real-mode assembly, 19 of which are live kernel modules totalling 9,950 lines. This page is the whole design in one read -- the constraint the machine imposes, the context switch that makes multitasking possible, the single lock that keeps twelve programs from scribbling over each other, how pixels reach the screen, and how a program on a floppy ends up running next to the kernel in the same segment.

Each section links to a deep dive: multitasking, the serial mouse, mode 12h graphics, and loadable programs.

Five instances
Five overlapping os8088 windows on a dithered gray desktop: an empty Note Pad, two cascaded Clock windows with the front one reading 00:00:20, and two cascaded Bounce windows with a black square inside the front one. Two floppy drive icons sit at the right edge and five tiles fill the dock along the bottom.
Everything below is in service of this picture. Two Clocks and two Bounces, each its own pre-empted task, cascaded 16 pixels down and right from its kind's template, each with a tile in the dock. The Note Pad in front owns no task at all -- it runs entirely inside window callbacks.

The constraint

Start with the constraint, because everything else follows from it. An Intel 8086 has no memory protection, no privilege levels, no MMU, and -- critically -- no way to address more than 64KB with a single pointer. Memory is reached through segments: a 16-bit segment register picks a 16-byte-granular base, and a 16-bit offset indexes within it. The machine os8088 targets has 256KB of RAM total, roughly a third of which is claimed the instant you turn on a 640x480 graphics mode.

There is no operating system underneath to ask for anything. When the BIOS hands control to the 512-byte boot sector, the CPU is executing raw instructions with interrupts off and nothing else is running anywhere in the machine. Every abstraction a modern programmer takes for granted -- a thread, a window, a heap, a loadable module -- has to be invented from scratch, in 14,376 bytes, on a CPU that cannot even shift a register by an immediate count other than one.

The context switch

The multitasking trick is smaller than most people expect, and it hinges on a design decision made at the very top of the file. Everything -- the kernel and every task and every program loaded from floppy -- runs in what is called the tiny model: one single 64KB segment, with CS = DS = SS = 0x1000. Code, data, and every stack in the system live in the same address space. That sounds reckless, and it means a buggy program can scribble over the kernel, but it buys the thing that makes the whole design possible: a context switch is just swapping the stack pointer.

The PC's programmable interval timer fires an interrupt 18.2 times a second. The handler chains to the BIOS's own clock routine first, so timekeeping stays honest, then pushes nine registers onto whatever stack the interrupted task was using, stores that stack pointer into an 8-byte task record, scans round-robin for the next task marked ready, loads its stack pointer, pops the nine registers back, and executes iret. The CPU returns from the interrupt into a completely different program, and neither program has any idea it happened.

Because all the stacks share one segment, no segment register has to change -- the switch is about thirty instructions. Twelve tasks, 1,536 bytes of stack each. That single mechanism is what lets a clock tick and a ball bounce while you are holding a mouse drag on a machine from 1978.

Pre-emptive multitasking, in detail

One lock, and the cursor that respects it

Pre-emption at any instant creates the obvious hazard: two tasks halfway through programming the VGA's Graphics Controller at once, and the screen is garbage. Rather than lock per-object or per-window, os8088 takes the brutally simple route of one global drawing mutex. Any task that wants to draw takes the lock, draws, and releases it; a task that finds the lock held yields its timeslice and tries again. Note what the lock deliberately does not do -- it does not stop pre-emption. Background tasks keep getting scheduled during a window drag; they simply block when they reach the point of drawing.

The subtle part is the mouse cursor. The cursor is drawn by the mouse's own interrupt handler, which saves the 16x16 patch of screen underneath it into a 192-byte buffer so it can be restored when the pointer moves. But an interrupt can fire while a task is inside the lock, mid-draw -- so the handler checks the lock first, and if it is held, it merely records the new coordinates, sets a dirty flag, and returns without touching a single pixel. The deferred redraw happens on the next unlock.

That one rule -- the interrupt handler never draws over a held lock -- is what keeps the arrow from smearing across a half-painted window, and it is why releasing the lock is specified to show the cursor before clearing the flag, not after.

The serial mouse driver

Drawing, with no safety net

Drawing itself has no safety net either, because there is no room for one. An off-screen buffer for a 640x480 16-color screen is 153,600 bytes -- more than half the machine's entire memory -- so every pixel is composited directly onto the visible display, exactly as the original Macintosh did.

Mode 12h is planar: the four bits of each pixel's color live in four separate 64KB banks that all answer to the same address, and you select which ones a write affects through hardware registers. Fills are done by loading the color into the VGA's Set/Reset register and letting the card write four planes in one bus cycle; partial bytes at the edges of a rectangle go through the Bit Mask register; drag outlines and menu highlights use the card's XOR mode, so drawing the same shape twice erases it and restores whatever was underneath for free.

Every routine is required to leave the graphics hardware in a documented default state on return, because there is no driver layer to re-establish it, and one routine that forgets will corrupt the next one's output in a way that looks like a completely unrelated bug.

VGA mode 12h, in detail

XOR drag outline
A Note Pad window reading 'Drag me by the title bar' with a hollow one-pixel rectangle the same size as the window offset down and to the right of it, drawn straight over the Disk window underneath, which lists MINES at 1593 bytes and HELLO at 171 bytes.
XOR is why this costs nothing. The outline is toggled on and off once per timer tick, so the lock drops between frames and the background tasks still get to paint. Drawing the rectangle a second time restores the Disk window's pixels underneath without anyone having saved them.

Loadable programs

The last piece is the one with the cleverest build-time trick. A package like the 1,593-byte Minesweeper is a flat binary that loads into the kernel's own segment, in a 20,480-byte pool at offsets 0xA000 to 0xEFFF, and calls the kernel through a fixed table of 26 near jumps at address 0x0010. That shared-segment arrangement is why a loaded program's paint and click handlers can be called as ordinary near function pointers, indistinguishable from a built-in's.

But it creates a problem: with no relocation information, a binary assembled to run at 0xA000 can only ever run at 0xA000, so you could never have two programs -- or two copies of one -- resident at once. The fix is to make the assembler tell on itself. The Makefile assembles each package twice, at two different base addresses 0x800 apart, and a Python tool diffs the two binaries word by word. A word whose value went up by exactly 0x800 must be an embedded address, and needs the load offset added at load time. A word that went down by 0x800 must be the relative displacement of a call into the kernel -- the call site moved but the kernel did not, so it needs the offset subtracted. Everything unchanged is ordinary data.

The tool ships the resulting list as a relocation table appended to the file; the loader walks it, patches each word, then zeroes the program's uninitialized data over the top of the table, since it is dead the moment it has been applied. That is how a 1978 CPU with no linker, no dynamic loader and no memory manager ends up running several relocatable programs side by side, each closable, each freeing its memory with a single byte store to the instance table.

The .o88 package format

Two packages resident
A Minesweeper window with a gray 9 by 9 grid of unopened cells and a counter reading 10, and below it a smaller window titled Hello reading 'Hello from a .o88 package!'. Three tiles sit in the dock, the leftmost drawn inverted because the Disk window that loaded them is minimized.
Two programs off one floppy, relocated to different addresses. MINES and HELLO were each dropped into the first free region of the 20,480-byte pool and patched on the way in. The Disk window that launched them has been minimized; its dock tile draws inverted.

The memory map

Where everything lives in the 8086's one megabyte of address space. A build-time assertion fails the build if the kernel image plus its .bss ever reaches offset 0xA000, where the loaded-program pool starts.

os8088 memory map: linear address, segment and contents
LinearSegmentContents
0x00500--Free. The boot stack grows down from 0x7C00.
0x07C000000The 512-byte boot sector, where the BIOS puts it.
0x100001000Kernel: code, data and .bss -- task stacks, window records, the event ring, every buffer.
0x1A0001000Loaded-program pool, offsets 0xA000 to 0xEFFF in the kernel's own segment.
0x200002000Menu save-under heap: the pixels behind an open pull-down.
0xA0000A000The VGA planar framebuffer, 80 bytes per row.

The modules

Nineteen live files, 9,950 lines. There is no linker: kernel.asm %includes the rest and NASM emits one flat binary.

The os8088 source modules and what each one owns
FileOwns
boot/boot.asmThe 512-byte boot sector: LBA-to-CHS translation, retrying BIOS reads.
kernel/kernel.asmConstants, the boot sequence, the API jump table, the includes, the size assertion.
kernel/vga12.incMode 12h planar primitives, save and restore, the drawing lock.
kernel/font.incGrabbing the 8x8 ROM font off the video card, transparent text drawing.
kernel/mouse.incThe COM1 UART, the IRQ4 handler, packet decode, the save-under cursor.
kernel/sched.incThe PIT hook, the context switch, spawn/yield/sleep, pre-emptive and cooperative mode, the watchdog.
kernel/events.incThe interrupt-safe event ring queue.
kernel/wm.incWindow records, z-order, frames, hit testing, the painter.
kernel/instance.incThe instance table: app kinds, launch, close, CPU billing.
kernel/menu.incThe menu bar and pull-down tracking.
kernel/ui.incThe UI task: event pump, keyboard, drags, dispatch.
kernel/apps.incAbout, Note Pad, the Clock task, the Bounce task.
kernel/disk.incint 13h floppy reads, mounting os88fs, reading the directory.
kernel/loader.inc.o88 validation, region allocation, relocation, launch.
kernel/files.incThe Disk window: the file manager.
kernel/icons.incThe 1-bit icon format, the draw routine, the built-in library.
kernel/desk.incDesktop drive icons: detect, paint, click to open.
kernel/dock.incThe bottom dock strip, one tile per live instance.
kernel/taskmgr.incThe Task Manager window: CPU, RAM, the instance list.
kernel/ctrl.incThe Control Panel window: item list and settings pages.

Four more .inc files -- video, keyboard, string and gfx -- are still in the tree but are no longer included anywhere. They are relics of the text shell that came before the GUI, kept for the history rather than the code.

Read the source

None of the above is a substitute for the code, which is short enough to read in an afternoon. Start with SPEC.md: it is the binding contract, not documentation written afterwards. Every symbol name, register convention, constant and data layout is pinned there, and it is updated before an interface changes, never after -- which is the only reason a system with one flat namespace, no memory protection and twelve tasks sharing a segment stays comprehensible at all.

Then read kernel/sched.inc for the context switch and kernel/vga12.inc for the drawing lock. Between them they explain most of the rest.