A bare-metal x86 hobby operating system that boots via GRUB, written from scratch in NASM assembly and C.
When booted, CIndy-OS initializes its own GDT, sets up the IDT, remaps the Programmable Interrupt Controller (PIC), and provides a fully interactive environment:
- Hardware Interrupts: Handles timer ticks (IRQ0) and keyboard input (IRQ1).
- CPU Exceptions: Catches and handles 32 different CPU exceptions (like Divide by Zero and Page Faults) preventing silent reboots.
- Interactive Shell: Features a command-line interface with an
argc/argvparser. - Built-in Commands: Try typing
help,about,version,timer,echo,meminfo, orreboot! - Memory Management: Parses the GRUB Multiboot memory map and provides a dynamic bump allocator (
kmalloc). - Read-Only File System: Uses an Initial Ramdisk (initrd) formatted as a USTAR archive to support file reading (
ls,cat). - ATA PIO Hard Drive Driver: Implemented 28-bit LBA sector reading and writing to a virtual IDE hard drive.
All output is written directly to the VGA text buffer at physical address 0xB8000, and input is processed directly from the PS/2 controller port 0x60.
CIndy-OS/
├── src/ # Core kernel source files
│ ├── boot.asm # Assembly entry point + Multiboot header + GDT setup
│ ├── kernel.c # Main kernel entry point (kernel_main)
│ ├── screen.c # VGA text buffer, printing, cursor control
│ ├── ports.c # Port I/O wrappers (inb, outb)
│ ├── idt.c # Interrupt Descriptor Table setup
│ ├── idt_load.asm # Assembly routine to load IDT register
│ ├── isr.asm # CPU exception handlers (ISR 0-31)
│ ├── interrupts.asm # Hardware interrupt stubs
│ ├── pic.c # Programmable Interrupt Controller remapping
│ ├── timer.c # Programmable Interval Timer (PIT) driver
│ ├── keyboard.c # PS/2 keyboard driver + input parsing
│ ├── memory.c # Memory management: bump allocator (kmalloc)
│ ├── fs.c # USTAR archive filesystem driver (ls, cat)
│ ├── string.c # String utility functions
│ └── ata.c # ATA PIO disk driver
├── include/ # Header files
│ ├── types.h # Type definitions (uint8_t, uint32_t, etc.)
│ ├── screen.h # Screen interface
│ ├── ports.h # Port I/O interface
│ ├── idt.h # IDT structures and functions
│ ├── interrupts.h # Interrupt declarations
│ ├── pic.h # PIC interface
│ ├── timer.h # Timer interface
│ ├── keyboard.h # Keyboard interface
│ ├── memory.h # Memory management interface
│ ├── fs.h # Filesystem interface
│ ├── string.h # String utilities
│ ├── ata.h # ATA interface
│ └── multiboot.h # Multiboot info structure
├── iso/ # ISO boot configuration
│ └── boot/
│ ├── kernel.bin # Compiled kernel (generated)
│ ├── initrd.tar # Initial ramdisk (generated)
│ └── grub/
│ └── grub.cfg # GRUB bootloader menu
├── fs/ # Filesystem content (packed into initrd.tar)
├── linker.ld # Linker script – places kernel at 1 MiB
├── Makefile # Build & run targets
├── README.md # This file
└── CIndy-os.iso # Bootable ISO output (generated)
BIOS/UEFI
↓
GRUB (reads CIndy-os.iso)
↓ loads kernel.bin
searches for Multiboot header in .multiboot section
↓
jumps to start (boot.asm)
↓
- Loads GDT for protected mode
- Sets up stack (16 KiB)
↓
calls kernel_main() (kernel.c)
↓
- Initializes IDT + CPU exception handlers
- Remaps PIC
- Enables hardware interrupts
- Initializes memory manager
- Loads initrd filesystem
↓
enters interactive shell loop
↓ (on halt)
cli + hlt loop (halts CPU)
- Multiboot Header: Defines magic (
0x1BADB002), flags, checksum for GRUB - GDT (Global Descriptor Table): Sets up flat memory model with code and data segments for 32-bit protected mode
- Stack Setup: Allocates 16 KiB stack in
.bsssection - ESP Register: Points stack pointer to top of stack before calling kernel
- Kernel Call: Jumps to
kernel_main()in C, preserving Multiboot parameters (magic, multiboot_info*) - Infinite Loop: Returns to
cli+hltif kernel ever returns
kernel_main(unsigned int magic, struct multiboot_info* mbd)— Main entry point- Initializes screen (clears, enables cursor)
- Sets up IDT with 32 CPU exception handlers
- Remaps PIC to reroute hardware interrupts
- Enables interrupts globally
- Initializes timer driver
- Parses Multiboot memory map and initializes bump allocator
- Loads initial ramdisk (initrd) from GRUB module
- Enters interactive shell loop with
hltinstruction
clear_screen()— Clears 80×25 VGA cells to blankprint(const char* msg)— Prints string at current cursor positionprint_colored(const char* msg, unsigned char color)— Prints with color attributeprint_int(int n)— Converts and prints integerprint_hex(unsigned int n)— Converts and prints hexadecimalenable_cursor(unsigned char start, unsigned char end)— Hardware cursor setup- Direct Hardware Access: Writes character + color bytes directly to VGA buffer at
0xB8000
idt_init()— Allocates and initializes IDTidt_set_gate(int n, unsigned int handler, unsigned char sel, unsigned char flags)— Registers interrupt handlersel: Segment selector (typically0x08for code segment)flags:0x8Efor interrupt gate (present + 32-bit)
- Uses assembly routine
idt_load()to load IDT register vialidtinstruction
- ISR 0-7: Fault exceptions (Divide by Zero, Debug, NMI, Breakpoint, Overflow, Bound Range, Invalid Opcode, Device Not Available)
- ISR 8-15: Fault/Abort exceptions (Double Fault, Coprocessor, Invalid TSS, Segment Not Present, Stack Segment, General Protection, Page Fault, Floating Point)
- ISR 16-31: Other exceptions (FPU Exception, Alignment Check, Machine Check, SIMD)
- Each handler pushes registers, calls C exception handler, and restores state
- Prevents "triple fault" reboots by catching and printing exception info
- IRQ 0 (Timer): Calls
timer_handler()for PIT ticks - IRQ 1 (Keyboard): Calls
keyboard_handler()for PS/2 input - Sends EOI (End Of Interrupt) signal to PIC before returning
pic_remap()— Remaps PIC to avoid conflict with CPU exceptions- ICW1-ICW4 initialization sequence via ports
0x20/0xA0 - Maps IRQ 0-7 → vectors 32-39, IRQ 8-15 → vectors 40-47
- ICW1-ICW4 initialization sequence via ports
init_timer(unsigned int freq)— Sets PIT frequency via port0x43+0x40- Divisor = 1193180 / freq (e.g., 100 Hz → 11931)
- Provides periodic interrupts for time tracking and scheduler hooks
handle_keyboard_interrupt(unsigned char scancode)— Converts PS/2 scancodes to ASCII- Handles shift key, caps lock, and special keys (Enter, Backspace)
- Buffers input for command parsing
parse_command(const char* cmd)— Parses shell commands withargc/argv- Built-in Commands:
help— Lists available commandsabout— Prints OS infoversion— Shows kernel versiontimer— Displays timer ticksecho <text>— Echoes back textmeminfo— Shows memory usagereboot— Reboots systemls— Lists files in initrdcat <file>— Reads and displays file contentswrite-test <text>— Writes text to Sector 1 of the hard driveread-test— Reads and displays Sector 1 from the hard drive
init_memory(struct multiboot_info* mbd, unsigned int magic)— Parses Multiboot memory map- Reads BIOS memory regions from
mbd->mmap_addr+mbd->mmap_length - Calculates available RAM
- Reads BIOS memory regions from
void* kmalloc(unsigned int size)— Bump allocator- Allocates contiguous memory from "kernel heap"
- Simple: just increments a pointer (no free/fragmentation)
- Suitable for bootloader and static kernel structures
ata_wait_ready()— Polls the disk controller status register until BSY and DRQ bits are clearata_read_sector(unsigned int lba, unsigned char* buffer)— Sends0x20command to read 512 bytes from LBAata_write_sector(unsigned int lba, unsigned char* buffer)— Sends0x30command and writes 512 bytes, followed by cache flush (0xE7)
init_fs(unsigned int initrd_address)— Parses USTAR tar archive from initrd- Reads file headers and metadata
- Supports
ls— lists all files in archive - Supports
cat <filename>— reads and displays file contents
- Read-Only: No write operations (stateless)
- Format: Standard USTAR tar format, loadable by GRUB as module
unsigned char inb(unsigned short port)— Reads byte from I/O portvoid outb(unsigned short port, unsigned char value)— Writes byte to I/O port- Uses GCC inline assembly (
in/outinstructions)
strlen(const char* str)— Returns string lengthstrcmp(const char* a, const char* b)— Compares stringsstrcpy(char* dest, const char* src)— Copies string- Standard C library equivalents for freestanding environment
- Defines
struct multiboot_infowith:- Memory map (base address, size, type of each region)
- Module list (bootloader-loaded files like initrd)
- Command-line arguments
- Boot loader name
- Follows GNU Multiboot 1.0 specification
- Sets kernel load address to 1 MiB (
0x100000) — standard for protected-mode kernels - Ensures
.multibootis first section in binary (GRUB scans first 8 KiB) - Aligns sections to 4 KiB page boundaries for MMU compatibility
- Separates
.text(code),.rodata(constants),.data(initialized data),.bss(uninitialized)
sudo apt-get install -y nasm gcc grub-pc-bin grub-common xorriso mtools qemu-system-x86| Tool | Purpose |
|---|---|
nasm |
Assemble .asm files → object files |
gcc |
Compile .c files → object files (freestanding, 32-bit) |
ld |
Link objects into kernel.bin using linker.ld |
grub-mkrescue |
Package kernel + GRUB into a bootable ISO |
mtools |
Required by grub-mkrescue to create the FAT image |
xorriso |
Creates the ISO filesystem |
qemu |
Run the ISO in a virtual machine |
makeThis runs the full pipeline:
1. Assemble boot.asm, idt_load.asm, isr.asm, interrupts.asm → .o files
2. Compile all .c files (kernel, screen, ports, idt, pic, keyboard, timer, string, memory, fs) → .o files
3. Link all object files using linker.ld → kernel.bin
4. Copy kernel.bin to iso/boot/kernel.bin
5. Create tar archive from fs/ directory → initrd.tar
6. Copy initrd.tar to iso/boot/initrd.tar (GRUB will load this as module)
7. Generate bootable ISO using grub-mkrescue → CIndy-os.iso
make run
# equivalent to:
qemu-system-i386 -cdrom CIndy-os.isomake run-curses # Run with curses (text-based) display
make run-debug # Run with debug console output- GDT (Global Descriptor Table) flat setup
- IDT (Interrupt Descriptor Table) + CPU exception handling (32 exceptions)
- Programmable Interval Timer (PIT) — hardware timer
- PS/2 keyboard driver with scancode-to-ASCII conversion
- Interactive shell with command parsing (
argc/argv) - Memory management — Multiboot memory parsing + bump allocator
- Read-only filesystem — USTAR tar archive support (
ls,cat)
- Virtual Memory / Paging — Enable paging for 4 GiB address space and memory protection
- Dynamic Memory Management — Implement malloc/free with fragmentation handling (heap allocator)
- Process Management — Context switching, process scheduler, task switching
- File System Writes — Writable filesystem (FAT12/32 or ext2
- Preemptive Multitasking — Multiple processes running concurrently
- System Calls — User mode vs. kernel mode, syscall interface
- Debugging Tools — GDB support, kernel debugger
CIndy-OS is a learning project, not production-grade. The focus is on:
- Understanding hardware: Direct hardware interaction (ports, interrupts, memory)
- Simplicity over efficiency: Clean, readable code over optimization
- Hands-on OS concepts: Paging, multitasking, I/O, memory management
- Minimal dependencies: No external bootloaders (GRUB) or kernel frameworks
- Educational value: Each component is self-contained and well-documented
The goal is to explore OS kernel design from first principles, not to build a feature-complete or performant OS. Future features will prioritize learning impact over complexity.
- OSDev Wiki — comprehensive OS development reference
- Multiboot Specification
- NASM Documentation — assembly syntax and directives
- James Molloy's Kernel Tutorial — OS fundamentals
- x86 Assembly Reference — instruction reference
- USTAR Tar Format — archive format spec
This is a personal learning project. Do whatever you want with it.