[ BOOTING DECA-TINY-OS · KERNEL_SECTORS EQU 36 · REAL MODE x86 ]

Building an OS
from scratch.

A Real-Mode x86 Journey · 16-bit · 640K · FAT12

What happens when you start with a blank NASM file and don't stop until you have a bootloader, a filesystem, a shell, a BASIC interpreter, and a full application suite running on raw x86 hardware? Follow along.

30 Self-tests pass
36 Kernel sectors
28+ External apps
640K Real mode RAM
▶ Run the OS Read the story See the timeline →
qemu-system-i386 · COM1 · Deca-Tiny-OS
Deca-Tiny-OS Bootloader · Geometry: 80c/2h/18s
Loading kernel: .................... OK (36 sectors)
BIOS conventional memory: 639 KiB · top 9FC0:0000
SELFTEST start
· memory detection ........ OK
· high runtime buffers ..... OK
· FAT12 filesystem ......... OK
· app return trampoline .... OK
· API handle table ......... OK
SELFTEST pass=30 fail=0

Deca-Tiny-OS v0.30 · Real Mode 8086 · FAT12
Conventional RAM top: 9FC0:0000
Apps: FAT12 root, load 2000:0100, buffers 7000:9000+

] help
Shell built-ins: dir, cd, pwd, mkdir, df, stat, type,
touch, append, copy, del, ren, batch, status, where...
External apps: basic, edit, hexedit, more, head, wc, find...

] basic DEMO.BAS RUN
BASIC v1 · OK
Hello from Deca-Tiny-OS!
Loop: 1 2 3 4 5 Done.
OK

]

Why build an OS
from nothing?

Most people who learn programming never go lower than Python. Some go as far as C. A very few reach assembly. And then there's this project: start with a blank NASM file, a QEMU instance, and absolutely no shortcuts.

Deca-Tiny-OS is a real-mode x86 operating system built entirely in hand-written assembly language. It boots on real hardware and on QEMU, speaks FAT12, runs external .COM apps, features a CP/M-inspired shell, and even ships a tiny BASIC interpreter — all in 16-bit real mode, all within the classic 640K conventional memory envelope.

"Every byte of this system was written deliberately. There's no libc, no standard library, no OS below us. Just the BIOS, the bare metal, and NASM."

The project began with the most fundamental question in systems programming: how does a computer even start? The answer is a 512-byte boot sector, a master boot record that fits on the first sector of a disk, loaded by the BIOS at 0000:7C00. From that single sector, everything else follows.

The first working milestone was nothing glamorous: dots printing to the screen as the bootloader loaded the kernel sector by sector. The second was a flickering prompt. The third was dir actually listing files. But each of those milestones had to be earned — through BIOS geometry queries, CHS conversion bugs, FAT12 byte-order surprises, and register clobbers that crashed everything silently. Every bug is documented. Every fix is recorded.

This is a build-in-public story. The dev log runs to dozens of entries. Every feature slice has regression evidence: a SHA-256 image hash, a QEMU serial capture, a FAT cluster leak check. When it breaks, we fix it and write it down. That's the whole game.


Rules we follow
ruthlessly

Every feature slice must have: a build that passes, a QEMU smoke test that passes, a regression check that produces matching PowerShell and Python image hashes, and a dev log entry that records what changed and why. Nothing ships without evidence.

No silent regressions. When the bootloader sector count changes, the build fails at compile time. When a FAT cluster leaks, the regression script catches it. When an app overwrites the kernel, the memory guard fires. The system polices itself so that future work can be done with confidence.

boot.asm — sector gate that enforces build integrity x86 NASM
; Bootloader enforces kernel size at build time. ; If kernel.bin grows past this sector count, ; the image builder refuses to produce the image. kernel_sectors equ 36 kernel_start_lba equ 128 ; Load kernel from LBA 128, one sector at a time, ; with BIOS geometry query and CHS conversion. kernel_load_loop: call lba_to_chs mov ah, 0x02 mov al, 1 int 0x13 jc disk_retry

From blank file
to full OS

Every milestone earned through real debugging, real regressions caught, and real fixes documented. No skipping steps.

April 27, 2026 · Week 1
Boot FAT12

The Very First Boot

The journey started with a 512-byte boot sector and a kernel that printed dots to prove it was loading. Those dots were everything — each one meant a sector had crossed the BIOS disk I/O layer correctly. Getting even that working required BIOS geometry queries (int 13h ah=08h), proper LBA-to-CHS conversion, and a retry loop because early drafts corrupted their own loop counter mid-read.

The biggest early surprise: FAT12 stores bytes-per-sector as 00 02 in little-endian, but the initial validation code checked only the low byte (00) and decided every valid disk was invalid. Classic. Every shell command reported "filesystem unavailable" for days until that single-byte comparison bug was spotted.

Kernel size: 7 sectors Selftest: pass=8 fail=0 Apps: HELLO.COM, INFO.COM
April 27–28, 2026 · Week 1
Shell API

CP/M Shell & the int 60h API

With a kernel that could boot and list files, the next step was making it actually useful. A CP/M-inspired shell was born: table-driven command dispatch, case-insensitive matching, external app fallback, and VGA text scrolling so long output didn't wrap to the top and eat itself.

The int 60h kernel API was designed around this era: print string, read line, DMA buffer selection, file open/read/close. Apps don't touch kernel internals — they call a software interrupt and get a clean interface back. That design decision has paid dividends ever since. Every app written since then works the same way.

apps/hello.asm — classic first app
; Every app is a flat .COM binary loaded at 2000:0100 org APP_LOAD_OFFSET ; from memory_map.inc mov si, message mov ah, 0x00 ; int 60h: print string int 0x60 ret ; return to shell message: db 'Hello from Deca-Tiny-OS!', 13, 10, 0
API services: 12 defined VGA: Text scrolling
April 29, 2026 · Week 1
FAT12 Writes Write Safety

Writing to Disk: The Hard Part

Reading from a FAT12 disk is satisfying. Writing to it is terrifying. One wrong byte in the FAT and you've corrupted the entire filesystem. The approach taken here: build comprehensive rollback from day one, and stress-test every failure path before building on top of it.

The diskfull diagnostic fills every data cluster until the disk overflows, verifying that allocation rollback correctly releases partial chains. The rootfull diagnostic saturates all 224 root directory entries. The wrterr diagnostic injects simulated disk-write failures via a kernel countdown mechanism and verifies that no bytes leak.

The most insidious bug from this era: the FAT release function was called with the start cluster in AX, then immediately called fs_load_metadata which clobbered AX to 19. Every delete silently freed cluster 19 instead of the requested chain — which also happened to be where GFXDEMO.COM lived. That one took a while.

Write ops: create, delete, append, rename Rollback paths: data, FAT, root Diagnostics: diskfull, rootfull, wrterr, wrtfail
April 29–30, 2026 · Week 1–2
BASIC Language

Writing a BASIC Interpreter in Assembly

Here's a question that sounds absurd: can you write a BASIC interpreter in 16-bit x86 assembly, fit it in a 8KB .COM file, and have it actually be useful? The answer turned out to be yes — if you're disciplined about scope.

The interpreter was built in twelve slices: tokenizer first, then a REPL skeleton, then expression parsing, variables, input, program storage, execution, conditionals, loops, subroutines, and finally file I/O. Each slice had its own smoke test. Each slice produced a working artifact. Nothing was left in a half-finished state.

The end result: basic DEMO.BAS RUN loads a BASIC program from disk, runs it, handles loops and subroutines and user input, and returns cleanly to the shell. FOR/NEXT, GOSUB/ RETURN, IF...THEN, SAVE/LOAD — all working. All within 8KB.

samples/SUBS.BAS — bundled BASIC demo
10 PRINT "SUBS" 20 FOR I=1 TO 3 30 GOSUB 100 40 NEXT I 50 PRINT "DONE" 60 END 100 PRINT I 110 RETURN → Output: SUBS 1 2 3 DONE
BASIC.COM size: ~8KB Variables: A–Z (16-bit signed) Selftest: pass=21 fail=0
May 1, 2026 · Week 2
File Tools Text Tools

Building a Toolkit: Text Tools & File Management

An OS isn't useful if you can only run one thing at a time. The toolkit phase built out a full suite of Unix-inspired text tools — all as external .COM apps using the kernel's handle-based streaming API.

more: a paged text viewer with keyboard controls. head: shows the first N lines of a file. wc: counts bytes, lines, and words. find: case-sensitive literal search through a file. All of these stream through the file handle API — no whole-file loading, no 8KB buffer limits. They work on any size file.

The shell also grew: touch for zero-byte file creation, append file text for LF-terminated text appends from the command line, copy /overwrite with explicit overwrite semantics (default copy is always safe), stat for file metadata, df for disk usage. Every command with clear, specific error messages.

Shell commands: df, stat, touch, append, copy/overwrite Text tools: more, head, wc, find Selftest: pass=24 fail=0
May 2, 2026 · Week 2
640K Memory Directories

640K: Moving Everything Out of Segment Zero

The first 64KB of memory is precious real estate in a real-mode system. Bootloader at 0000:7C00. Interrupt vectors at 0000:0000. BIOS data. Kernel code. FAT buffers. File buffers. App load window. Stack. All competing for the same cramped space. Something had to give.

The 640K preparation arc was a methodical ten-slice migration. First, detect conventional memory via BIOS int 12h. Then centralize memory constants in kernel/memory_map.inc. Then add helper routines for far-pointer buffer operations. Then — one by one — move the disk buffer, FAT buffer, root directory buffer, and file staging buffer into high conventional memory at segment 7000h.

The payoff: apps now load at 2000:0100 with 36KB of app window. The kernel has 35KB of headroom before the app region. Runtime buffers live at 7000:9000+. Everything is segment-aware. The system is ready for the full 640KB address space.

App window: 2000:0100 → 36KB Buffer segment: 7000:9000+ Kernel region limit: 0000:E000 Selftest: pass=30 fail=0

The lessons that
cost the most

The bugs that took the longest. The design decisions that mattered most. The moments where everything clicked — or broke spectacularly.

Deep Dive · FAT12 · Debugging
The Register That Ate My Filesystem
For days, every file delete appeared to succeed but left the filesystem in a corrupted state. The cause: fs_load_metadata was clobbering AX — the very register that carried the cluster number to free. Every delete was silently freeing cluster 19. Every delete. That cluster happened to contain GFXDEMO.COM. The system "worked" until you ran the graphics demo and it crashed with garbage on screen. This is why you write regression scripts that walk the FAT and check every chain.
April 29, 2026 · FAT12 · Assembly · Debugging
The fix: preserve AX across the metadata call
; BROKEN: AX (start cluster) clobbered here fs_release_cluster_chain: call fs_load_metadata ; AX → 19 ! ; now walks and frees cluster 19, not ours
; FIXED: save start cluster before metadata load fs_release_cluster_chain: push ax ; save cluster call fs_load_metadata pop bx ; restore → walk this ; now correctly walks requested chain
System Design · API · Architecture
Why int 60h Was the Right Call
The entire app ecosystem uses a software interrupt to talk to the kernel. int 60h, AH=service, arguments in registers. It's the CP/M BDOS model, adapted for this tiny OS. The payoff: every app ever written works the same way. Moving apps to a new segment? The API handles the segment-boundary crossing. Changing buffer locations? Apps never knew where they were anyway. The abstraction earns its cost at every feature slice.
April 27, 2026 · 18 API services
Build System · Regression · Automation
Serial-Driven QEMU Smoke Tests
Every significant change gets verified by scripts/smoke-qemu.ps1: a PowerShell script that launches QEMU with COM1 exposed over TCP, waits for the selftest marker, then drives the shell by typing commands over the serial port and watching for expected output. Fresh image, mutated image, SHA-256 parity between PowerShell and Python builders. If any check fails, the build doesn't pass. This is what makes confident development possible.
April 29, 2026 · Automation · Testing
Assembly · BASIC · Interpreter
8KB Is Enough for a BASIC Interpreter
The constraint was the app load region: fit in 8KB, use only int 60h services, no kernel modifications. The approach: a tokenizer include shared with a diagnostic tool, a minimal recursive-descent expression parser (signed 16-bit, A–Z variables, full precedence), a bounded 12-line program buffer, and execution state for loops and subroutines. Tight but complete. Save and load work via the FAT12 temp-first write model.
April 29–30, 2026 · 12 development slices
Memory · Architecture · x86
Escaping the First 64KB
Real mode means every data access uses a segment:offset pair, and most early real-mode code defaults to segment zero for everything. The 640K arc was about making the kernel deliberately segment-aware: moving FAT buffers to 7000:9000, the file staging buffer to 7000:C000, apps to 2000:0100. The kernel API now marshals pointer arguments across segment boundaries so apps never know where any of it lives.
May 2, 2026 · 10-slice migration
Shell · FAT12 · Directories
Adding Directories Without Breaking Everything
FAT12 directories are files that happen to contain 32-byte directory entries. Adding mkdir meant: allocate a cluster, write initialized . and .. entries, create a root entry with attribute 0x10, and — critically — make sure the existing file API never treats a directory entry as a regular file. That last rule required splitting root lookup into "any entry" and "regular file only" variants and plumbing that distinction everywhere.
May 1, 2026 · Directories · FAT12

The full application
suite

28+ external applications, all written in x86 assembly, all under 8KB each, all talking to the kernel through int 60h.

basic — BASIC Interpreter
Full Tiny BASIC REPL with variables, loops, subroutines, conditionals, file save/load, and command-tail autorun. 8KB.
✏️
edit — Text Editor
Line-oriented text editor with temp-first save (EDITSAVE.TMP → rename), so the original file is never lost on failure.
🔬
hexedit — Binary Patcher
Stream-based binary patch tool: verify, patch, pair-patch, undo checkpoint, and scratch recovery. Never loads the whole file.
📄
more / head / wc / find
Unix-inspired text tools: paged viewer, first-N-lines, byte/line/word counts, literal search. All streaming, no size limits.
🖥️
gfxdemo / paint / snake
VGA mode 13h graphics apps: color bar demo, arrow-key drawing, and a keyboard-driven snake game. All using int 60h graphics API.
🔍
hexview — Hex Viewer
Read-only paged hex viewer with optional filename and offset arguments. N for next page, Q to quit.
⚖️
cmp — File Compare
Streaming two-file compare using two concurrent read handles. Reports CMP match, CMP differ size, or CMP differ offset=N.
💾
Diagnostic Suite
diskfull, rootfull, wrtfail, wrterr, handles, libtest — a complete set of stress diagnostics for every failure path.
📜
batch — Shell Scripts
Run .BAT scripts from the shell with comment handling, blank-line skip, stop-on-error mode, and AUTOEXEC.BAT at startup.

Where everything
lives in memory

640KB of conventional real-mode memory, carefully partitioned. After the 640K arc, everything has a permanent home.

0000:0000
Interrupt Vector Table / BDA
0000:1000
KERNEL (~18KB, 36 sectors)
0000:F000
Stack (grows down)
2000:0100
APP WINDOW (36KB) ← apps load here
2000:8F00
Return trampoline
7000:9000
Disk buffer (512B)
7000:9200
FAT buffer (512B)
7000:A400
Root directory buffer (3.5KB)
7000:C000
File / DMA staging buffer (8KB)
9FC0:0000
EBDA / Conventional memory top
A000:0000
VGA framebuffer (mode 13h graphics)

Recent commits,
real evidence

Every feature slice includes a SHA-256 image hash, a QEMU serial capture, and a FAT cluster leak check. This is what "done" means.

May 02, 2026
11:47 AM
640K smoke & doc polish — arc closed
Hardened serial smoke with exact meminfo and where markers for every memory region. Confirmed the real-mode 640K arc complete. Next: resume feature/cwd-file-commands.
SHA-256: 1a15ccffac774c13746892ca9d1c3cf40333afb3b229eac645d660eb45e93a2d · pass=30 fail=0 · 108 clusters, 41 root files
May 02, 2026
09:22 AM
App segment migration — apps now at 2000:0100
Moved APP_LOAD_SEGMENT to 2000h. Updated int 60h to marshal app DS:SI pointers across the segment boundary. Added KERNEL_REGION_LIMIT 0xE000 as an independent build guard.
SHA-256: 6eaec9c6ae79a310b67ec397c348e22b128160b09cbaee99a47827f4727595ac · pass=30 fail=0
May 02, 2026
06:15 AM
Streamed app loader — cluster-direct into app window
Removed whole-file staging buffer dependency for app loading. FAT clusters now stream directly to APP_LOAD_SEGMENT:APP_LOAD_OFFSET. Added BIGLOAD.COM (8238 bytes) to prove loading beyond the old 8KB file-buffer cap.
SHA-256: c9b484e0b87a131e68c465a59ec0e94f941c47326e2787dc97f24fcda2fef1c7 · pass=29 fail=0
May 01, 2026
04:30 PM
Subdirectory cd — one-level navigation working
cd name enters a root-level directory, cd .. returns to root. Extended the transient mkdir selftest to navigate into MKDIRTST, assert cwd_depth=1, print PWD /MKDIRTST, and return.
SHA-256: cb57f5eb5115c6384722319ed037267d4fcfd3312bc886d2df35769706fb2aca · pass=24 fail=0
May 01, 2026
01:15 PM
mkdir root — first FAT12 directory creation
Added mkdir name: allocate cluster, write initialized . and .. entries, create root entry with attribute 0x10. Split root lookup into any-entry vs regular-file-only paths.
SHA-256: cad64b258c2f3dab183dfc64d4de53a9e2ddfa254211c5da8be2d8313bec33cd · pass=24 fail=0 · 89 clusters, no leaks
April 30, 2026
09:45 AM
BASIC v1 complete — GOSUB, FOR/NEXT, SAVE/LOAD
Closed the BASIC interpreter arc with bounded four-frame GOSUB/RETURN and FOR/NEXT stacks, plain-text SAVE/LOAD via temp-first FAT12, command-tail autorun, and a bundled BASIC demo pack with SIGN.BAS, SQUARE.BAS, SUM3.BAS.
SHA-256: 3d3e2cf29af25bb0817493ac7a135da0967ece86f927dc6981e0af4b674914a5 · pass=21 fail=0

What comes
next

The project runs on deterministic feature arcs. Each arc is documented before it starts, and completed with regression evidence before the next begins. Here's the frontier.

01
feature/cwd-file-commands
Route touch, append, del, stat, and type through the current working directory so subdirectory-aware file operations work properly.
02
feature/path-qualified-read
Add simple dir name/file, type name/file, and stat name/file path-qualified reads for one-level subdirectory access.
03
feature/rmdir-empty-dir
rmdir name for empty root-level directories with clean failure for non-empty targets. FAT cluster release and root entry removal with full rollback.
04
feature/directories-smoke-doc-polish
Harden the directory smoke suite with exact marker checks for all cd/mkdir/dir/rmdir paths, fresh and mutated image replay, and FAT cluster leak validation.
05
Protected Mode (future)
The kernel is not memory-protected. Every app can touch every byte. Eventually: a GDT, a simple privilege switch, and proper segment isolation. But not before the real-mode story is complete.
06
Multi-level directory tree
FAT12 supports arbitrary directory depth. The shell currently handles one level. Eventually: full path resolution, .. at any depth, and proper FAT chain walking for subdirectory lookup.

Follow the build.

Every commit is documented. Every bug is written up. Every regression is caught before it ships. This is what building an OS in public looks like.