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.
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.
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.
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.
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 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.
| Linear | Segment | Contents |
|---|---|---|
| 0x00500 | -- | Free. The boot stack grows down from 0x7C00. |
| 0x07C00 | 0000 | The 512-byte boot sector, where the BIOS puts it. |
| 0x10000 | 1000 | Kernel: code, data and .bss -- task stacks, window records, the event ring, every buffer. |
| 0x1A000 | 1000 | Loaded-program pool, offsets 0xA000 to 0xEFFF in the kernel's own segment. |
| 0x20000 | 2000 | Menu save-under heap: the pixels behind an open pull-down. |
| 0xA0000 | A000 | The 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.
| File | Owns |
|---|---|
| boot/boot.asm | The 512-byte boot sector: LBA-to-CHS translation, retrying BIOS reads. |
| kernel/kernel.asm | Constants, the boot sequence, the API jump table, the includes, the size assertion. |
| kernel/vga12.inc | Mode 12h planar primitives, save and restore, the drawing lock. |
| kernel/font.inc | Grabbing the 8x8 ROM font off the video card, transparent text drawing. |
| kernel/mouse.inc | The COM1 UART, the IRQ4 handler, packet decode, the save-under cursor. |
| kernel/sched.inc | The PIT hook, the context switch, spawn/yield/sleep, pre-emptive and cooperative mode, the watchdog. |
| kernel/events.inc | The interrupt-safe event ring queue. |
| kernel/wm.inc | Window records, z-order, frames, hit testing, the painter. |
| kernel/instance.inc | The instance table: app kinds, launch, close, CPU billing. |
| kernel/menu.inc | The menu bar and pull-down tracking. |
| kernel/ui.inc | The UI task: event pump, keyboard, drags, dispatch. |
| kernel/apps.inc | About, Note Pad, the Clock task, the Bounce task. |
| kernel/disk.inc | int 13h floppy reads, mounting os88fs, reading the directory. |
| kernel/loader.inc | .o88 validation, region allocation, relocation, launch. |
| kernel/files.inc | The Disk window: the file manager. |
| kernel/icons.inc | The 1-bit icon format, the draw routine, the built-in library. |
| kernel/desk.inc | Desktop drive icons: detect, paint, click to open. |
| kernel/dock.inc | The bottom dock strip, one tile per live instance. |
| kernel/taskmgr.inc | The Task Manager window: CPU, RAM, the instance list. |
| kernel/ctrl.inc | The 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.