A bootable 16-bit operating system built entirely in x86 assembly for CSYE6230 Operating Systems at Northeastern University.
VertexOS is a real-mode operating system that boots from scratch using BIOS interrupts. It features a two-stage bootloader, an interactive shell with file management commands, persistent storage across reboots, and command history with tab completion. Built using the MikeOS approach (pure 16-bit assembly, BIOS interrupts, floppy disk image).
Boot.mp4
| Command | Description |
|---|---|
LIST |
Display all files with names, sizes, created and modified timestamps |
CREATE <n> |
Create a new file with duplicate checking and filename validation |
DELETE <n> |
Remove a file with error handling for non-existent entries |
DELETE_ALL |
Bulk delete all files with Y/N confirmation prompt |
RENAME <old> <new> |
Rename a file with validation and modified timestamp update |
HELP |
Display all available commands with color-coded output |
| Command | Description |
|---|---|
CLEAR |
Clear screen and reset cursor position |
STATUS |
Display system info: OS version, CPU mode, file count, memory, UTC time |
- Data persistence - File table and command history survive reboots via a separate data disk
- Command history - Last 10 commands stored, navigate with up/down arrows, persisted across sessions
- Tab completion - Type a prefix and press Tab to auto-complete commands, cycles through matches
- Colored output - VGA text mode colors: cyan headers, green success, red errors, yellow commands
- Animated boot sequence - Progress bars with percentage during system initialization
- File validation - Rejects invalid characters and requires file extensions
- Case-insensitive input - Automatic uppercase conversion
- UTC timestamps - Creation and modification dates via BIOS real-time clock
| Tool | Purpose |
|---|---|
| WSL2 (Ubuntu) | Linux environment inside Windows |
| NASM 2.16 | Assembler - converts .asm files to raw binary machine code |
| QEMU 8.2 | Virtual machine emulator - boots and runs the OS in a window |
| Make 4.3 | Build automation - one command to assemble and run |
| VS Code | Code editor connected to WSL2 via Remote-WSL extension |
myos/
├── boot.asm - All OS source code (boot sector + OS kernel)
├── Makefile - Build automation (assemble, run, clean, reset)
├── data.img - Persistent data disk (generated at first build, not tracked)
├── boot.bin - Compiled binary (generated, not tracked)
├── .gitignore - Excludes .bin and .img files from version control
└── README.md - This file
- Windows 10/11 with WSL2 and Ubuntu installed
- NASM, QEMU, and Make installed in WSL2
- Open Ubuntu from the Windows Start menu
- Install tools:
sudo apt update && sudo apt upgrade -y sudo apt install nasm qemu-system-x86 make -y - Clone and navigate to the project:
cd ~/myos
- Open in VS Code:
code .
| Command | What it does |
|---|---|
make run |
Assembles boot.asm, creates data.img if needed, and boots in QEMU |
make build |
Assembles boot.asm without booting |
make clean |
Deletes boot.bin (preserves data.img and saved data) |
make reset |
Deletes both boot.bin and data.img (full fresh start) |
┌─────────────────────────────────────────────────────┐
│ QEMU │
│ │
│ ┌────────────────┐ ┌───────────────┐ │
│ │ boot.bin │ │ data.img │ │
│ │ (OS Disk) │ │ (Data Disk) │ │
│ │ │ │ │ │
│ │ Sector 1: │ │ Sector 1: │ │
│ │ Boot sector │ │ File table │ │
│ │ │ │ │ │
│ │ Sectors 2-20: │ │ Sectors 2-3: │ │
│ │ OS code │ │ Cmd history │ │
│ └────────────────┘ └───────────────┘ │
│ ▲ ▲ │
│ │ Boot │ Read/Writ │
│ │ │ │
│ ┌──────────────────────────────────────────┐ │
│ │ RAM (0x7C00+) │ │
│ │ │ │
│ │ Boot sector → loads OS → jumps to OS │ │
│ │ OS: shell, commands, file table, │ │
│ │ history buffer, save buffer │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
-
Boot sector (Sector 1, 512 bytes): The BIOS loads this automatically at address
0x7C00. It sets up CPU segment registers, then uses BIOS interruptint 0x13to load 19 additional sectors from disk into memory at address0x7E00. It then jumps to that address. -
OS code (Sectors 2-20, loaded at 0x7E00): Contains the ASCII art banner, shell loop, keyboard input handler, command parser, all command implementations, utility functions, and data structures.
A boot sector is exactly 512 bytes. The last 2 bytes must be the boot signature (0xAA55), and the loader code takes some space, leaving very little room for actual OS functionality. By loading extra sectors, we get approximately 9.5 KB of space for OS code and data.
VertexOS uses two separate disk images:
- boot.bin (OS disk) - Contains the bootloader and all OS code. Rebuilt from source on every
make run. - data.img (Data disk) - Stores the file table and command history. Created once and never overwritten by builds, ensuring data survives code changes.
This separation solves a key problem: rebuilding the OS from source code would normally destroy any saved user data. By using a dedicated data disk, the build process only touches the OS disk while user data remains intact.
Address Contents
─────────────────────────────
0x0000 Interrupt Vector Table
0x7C00 Boot sector (loaded by BIOS)
0x7E00 OS code start (loaded by boot sector)
├── Shell loop and command parser
├── Command handlers
├── Utility functions (print_string, compare_strings, etc.)
├── String data (prompts, messages, ASCII art logo)
├── File table (8 entries x 28 bytes)
├── Input buffer (64 bytes)
├── History buffer (10 entries x 64 bytes)
├── Save buffers (512 + 1024 bytes for disk I/O)
└── Variables (color state, date/time, tab completion state)
0xB800 VGA text mode video memory
Each file entry is 28 bytes:
Offset Size Field
──────────────────────────
0 1 Active flag (1 = exists, 0 = deleted)
1 15 File name (null-terminated, max 14 chars)
16 6 Created timestamp (YY, MM, DD, HH, MM, SS in BCD)
22 6 Modified timestamp (YY, MM, DD, HH, MM, SS in BCD)
The table holds 8 entries (224 bytes total). Deletion marks entries as inactive rather than erasing data, similar to early FAT file systems.
Data is saved to the second floppy drive using BIOS disk write interrupts:
- Sector 1: Magic number (
0xBEEF) + file table - Sectors 2-3: Magic number (
0xFACE) + history count + history buffer
On boot, the OS reads these sectors and checks the magic numbers. If valid data is found, it loads the saved state. If not (first boot or after make reset), it uses the hardcoded default files.
| Interrupt | Function | Purpose |
|---|---|---|
int 0x10 |
ah=0x0E |
Print character to screen (teletype mode) |
int 0x10 |
ah=0x09 |
Write character with color attribute |
int 0x10 |
ah=0x02 |
Set cursor position |
int 0x10 |
ah=0x03 |
Get cursor position |
int 0x10 |
ah=0x06 |
Scroll/clear screen |
int 0x12 |
- | Get conventional memory size |
int 0x13 |
ah=0x02 |
Read sectors from disk |
int 0x13 |
ah=0x03 |
Write sectors to disk |
int 0x16 |
ah=0x00 |
Wait for and read keypress |
int 0x1A |
ah=0x02 |
Read time from RTC |
int 0x1A |
ah=0x04 |
Read date from RTC |
User types command → Enter pressed
│
▼
Save to history → Save history to disk
│
▼
Is input empty? ──yes──→ Show prompt again
│ no
▼
Compare against exact commands (HELP, CLEAR, LIST, STATUS)
│ no match
▼
Compare against prefix commands (CREATE, RENAME, DELETE_ALL, DELETE)
│ no match
▼
Print "Unknown command"
Prefix matching is used for commands that take arguments. DELETE_ALL is checked before DELETE to prevent DELETE_ALL from being caught by the DELETE prefix.
- Simplicity - No cross-compiler, linker scripts, GDT setup, or protected mode switching required
- Predictability - Every instruction does exactly one thing, no compiler optimizations or hidden behavior
- Direct hardware access - Every BIOS interrupt call is explicit and visible in the source code
- Educational value - Understanding what happens at the instruction level is the core goal of an OS course
The tradeoff is verbosity: operations that take one line in C require 3-5 lines in assembly.
Real mode is what the CPU starts in after power-on. Staying in real mode means direct access to BIOS interrupts for hardware I/O, no need for the Global Descriptor Table (GDT) or memory protection setup, a simpler memory model, and maximum compatibility with BIOS services. The limitation is 1 MB of addressable memory, which is more than sufficient for this project.
Initially, persistence was implemented by writing to a reserved sector on the boot disk. This broke every time the OS was rebuilt from source because NASM generates a fresh binary that overwrites the entire disk image. The dual-disk approach ensures the build process never touches user data.
A real file system (FAT12, ext2) would require implementing sector allocation, directory structures, cluster chains, and free space tracking. The in-memory table approach demonstrates the same concepts (creation, deletion, naming, metadata) while keeping the implementation achievable within the project timeline.
The most persistent class of bugs involved CPU registers being inadvertently overwritten:
- BX/BL conflict:
compare_stringsoriginally usedBLfor comparisons, which corruptedBX(sinceBLis the lower half ofBX). This caused file table pointer corruption during LIST and CREATE. Fixed by usingAHinstead. - DX corruption:
validate_filenameusedDXinternally, destroying the argument pointer stored inDXbydo_create. Fixed by saving and restoringDXaround the function call. - BIOS clobbering: Some BIOS interrupts modify registers unexpectedly. Resolved by pushing and popping registers around BIOS calls.
The 512-byte boot sector limit was hit early when the welcome banner alone exceeded available space. This forced the two-stage bootloader design. Later, as features were added, the sector count had to be increased from 4 to 19.
Assembly has no function boundaries. Code execution flows from one label to the next unless explicitly redirected with jmp or ret. The DELETE_ALL handler had a bug where the confirmed path fell through into the error handler because the error label was placed between the confirmation logic and the wipe loop.
VGA text mode teletype output (ah=0x0E) ignores the color attribute on SeaBIOS. The solution was a two-step approach: write the character with color using ah=0x09 (which does not advance the cursor), then advance the cursor using ah=0x0E.
The blinking cursor inherits its color from the character attribute at its position. After printing colored text, the cursor would display the wrong color. Fixed by writing an invisible space character with the desired color attribute at the cursor position after every print operation.
- No real file content storage (files exist as table entries only)
- No left/right cursor editing (input only supports typing at end and backspace)
- Fixed file table size (maximum 8 files, 14-character names)
- No scrollback (text that scrolls off screen is lost)
- UTC-only timestamps (no timezone conversion)
- Write file content to disk sectors for actual file storage
- Implement FAT12 file system for real disk-based file management
- Add cursor movement with mid-line insertion and deletion
- Switch to 32-bit protected mode and load a C kernel for advanced features
- Implement a VGA graphics mode GUI
- Add scrollback buffer using Page Up/Page Down
| File | Lines | Description |
|---|---|---|
| boot.asm | 2,603 | All OS source code |
| Makefile | 13 | Build automation |
| Total | 2,616 | Pure x86 assembly, no external libraries |
- MikeOS: Write Your Own OS guide - https://mikeos.sourceforge.net/write-your-own-os.html
- Nick Blundell, Writing a Simple Operating System from Scratch
- OSDev Wiki - https://wiki.osdev.org
- Intel x86 Architecture Reference Manual