Thirty-seven chapters ago, you wrote your first x86-64 assembly instruction. Now you are going to write an operating system that boots on real (emulated) hardware.
In This Chapter
- The MinOS Project
- Architecture Review: What We Built
- MinOS Source Structure
- The Bootloader: boot/boot.asm
- Kernel Entry: kernel/entry.asm
- The GDT: Segments and Privilege
- The IDT: Handling Interrupts and Exceptions
- The Physical Memory Allocator
- The VGA Driver
- The Timer: PIT at 100Hz
- The Scheduler: Round-Robin Preemptive
- The Shell: Minimal Command Interpreter
- kernel_main: The Initialization Order
- Building MinOS
- Running MinOS in QEMU
- Three Capstone Tracks
- What You Have Proven
- Summary
Chapter 38: Capstone — A Minimal OS Kernel
The MinOS Project
Thirty-seven chapters ago, you wrote your first x86-64 assembly instruction. Now you are going to write an operating system that boots on real (emulated) hardware.
MinOS is a minimal but real operating system kernel. It is not a toy that prints "Hello World" and halts. It boots, sets up protected memory, handles interrupts, manages memory, runs processes, and presents a command shell. Every component runs on the hardware you have been studying. There is no magic layer beneath it — MinOS is the lowest layer.
MinOS demonstrates something that no amount of reading can substitute for: that you understand the machine at every level. The CPU initializes with BIOS assistance. The bootloader configures the GDT and switches to 64-bit mode. The kernel initializes the IDT, the page tables, and the VGA driver. The scheduler switches between processes. The shell reads from the keyboard. You wrote every instruction in this chain.
This chapter integrates everything built through the Chapter 28-30 OS Kernel Project exercises. If you completed those exercises, you already have the components; this chapter assembles them into a working system.
Architecture Review: What We Built
Through the OS Kernel Project exercises in Parts V and VI, you built:
From Chapter 26 (Interrupts): The IDT, exception handlers for CPU faults (#GP, #PF, #UD), and IRQ handlers for hardware interrupts. The idt_entry macro and setup_idt function.
From Chapter 27 (Memory Management): The physical memory bitmap allocator (alloc_page(), free_page()), the virtual memory manager (map_page(), unmap_page()), and the initial kernel page tables.
From Chapter 28 (Bootloader): The 512-byte MBR bootloader in real mode, including GDT setup, A20 line enablement, and the jump to long mode. The kernel loader that reads the kernel from disk.
From Chapter 29 (Device I/O): The VGA text driver (init_vga(), putchar(), puts(), scroll()), the keyboard interrupt handler with scancode-to-ASCII conversion, and the serial port (UART 8250) driver for debug output.
From Chapter 30 (Concurrency): Spinlocks using LOCK XCHG, atomic operations, and the framework for SMP-safe code.
From Chapter 25 (System Calls): The SYSCALL/SYSRET handler, the system call table, and the user-space calling convention.
Chapter 38 adds the final pieces: the scheduler, the shell, the complete Makefile, and the assembly that binds everything together.
MinOS Source Structure
minOS/
├── Makefile
├── boot/
│ └── boot.asm ← Bootloader (real mode → long mode, load kernel)
├── kernel/
│ ├── entry.asm ← Kernel entry point (set up GDT, IDT, paging)
│ ├── kernel.c ← Main kernel: init order, kernel_main()
│ ├── gdt.c / gdt.h ← GDT setup (5 entries: code, data, user code, data, TSS)
│ ├── idt.c / idt.h ← IDT and exception/IRQ handlers
│ ├── mm/
│ │ ├── pmm.c/h ← Physical memory manager (bitmap allocator)
│ │ └── vmm.c/h ← Virtual memory manager (page tables)
│ ├── drivers/
│ │ ├── vga.c/h ← VGA text mode driver
│ │ ├── keyboard.c/h ← Keyboard interrupt driver
│ │ ├── serial.c/h ← UART serial debug output
│ │ └── timer.c/h ← PIT (8253/8254) timer at 100Hz
│ ├── proc/
│ │ ├── scheduler.c/h ← Round-robin preemptive scheduler
│ │ └── process.c/h ← Process creation and management
│ ├── syscall/
│ │ ├── syscall.asm ← SYSCALL/SYSRET entry point
│ │ └── syscall.c/h ← System call handlers
│ └── shell/
│ └── shell.c/h ← Simple command shell
└── user/
└── init.c ← First user process
The Bootloader: boot/boot.asm
The bootloader fits in 510 bytes (512-byte MBR minus 2 bytes for the 0x55AA signature). It runs in 16-bit real mode and must:
- Set up a temporary GDT for the mode switch
- Enable the A20 address line
- Load the kernel from disk (LBA BIOS INT 13h)
- Switch to 32-bit protected mode, then to 64-bit long mode
- Jump to the kernel entry point
; boot/boot.asm (simplified key sections)
; NASM format, output: raw binary (MBR)
[BITS 16]
[ORG 0x7C00]
start:
; Set up segment registers to known values
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00 ; stack below bootloader
; Enable A20 (fast method: port 0x92)
in al, 0x92
or al, 0x02
out 0x92, al
; Load kernel: INT 13h LBA extended read
; (read sectors 1-63 from disk to 0x10000)
mov ah, 0x42 ; INT 13h extended read
mov dl, 0x80 ; drive 0
lea si, [dap] ; disk address packet
int 0x13
jc disk_error
; Load temporary GDT
lgdt [gdtr_temp]
; Enable protected mode (CR0.PE = 1)
mov eax, cr0
or eax, 1
mov cr0, eax
; Far jump to 32-bit code
jmp 0x08:protected_32
[BITS 32]
protected_32:
mov ax, 0x10 ; data segment
mov ds, ax
mov es, ax
mov ss, ax
mov esp, 0x90000
; Set up page tables for long mode
; (minimal identity mapping for first 2MB)
call setup_paging
; Enable long mode: EFER.LME = 1
mov ecx, 0xC0000080 ; EFER MSR
rdmsr
or eax, (1 << 8) ; LME bit
wrmsr
; Enable paging (CR0.PG = 1) → activates long mode
mov eax, cr0
or eax, 0x80000000
mov cr0, eax
; Far jump to 64-bit kernel entry
jmp 0x08:0x10000 ; kernel loaded at 0x10000
; Disk Address Packet for INT 13h
dap:
db 0x10 ; size of packet
db 0 ; reserved
dw 63 ; sectors to read
dw 0x0000 ; buffer offset
dw 0x1000 ; buffer segment (0x10000 linear)
dq 1 ; start LBA sector
; Temporary GDT (32-bit, for mode transition only)
gdt_temp:
dq 0 ; null descriptor
dq 0x00CF9A000000FFFF ; code (32-bit)
dq 0x00CF92000000FFFF ; data (32-bit)
gdtr_temp:
dw $ - gdt_temp - 1
dd gdt_temp
; Paging setup (identity map 0-2MB with 2MB pages)
setup_paging:
; ... (PML4, PDPT, PD setup) ...
; See Chapter 27 implementation
ret
; Boot signature
times 510-($-$$) db 0
dw 0xAA55
Kernel Entry: kernel/entry.asm
The kernel entry point runs in 64-bit long mode with the minimal bootloader paging:
; kernel/entry.asm — kernel entry in 64-bit long mode
[BITS 64]
[ORG 0x10000] ; loaded here by bootloader
global _kernel_start
extern kernel_main
_kernel_start:
; Set up proper 64-bit GDT
lea rax, [rel gdt64]
mov [rel gdtr64 + 2], rax
lgdt [rel gdtr64]
; Reload segment registers with 64-bit descriptors
mov ax, 0x10 ; kernel data selector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
; Reload CS via far return
push 0x08 ; kernel code selector
lea rax, [rel .reload_cs]
push rax
retfq ; far return: pops RIP and CS
.reload_cs:
; Set up proper kernel stack
mov rsp, kernel_stack_top
; Zero BSS
lea rdi, [rel bss_start]
lea rcx, [rel bss_end]
sub rcx, rdi
xor al, al
rep stosb
; Call C kernel entry
call kernel_main
; Should never return
hlt
; 64-bit GDT (5 entries + TSS requires 2 entries)
align 8
gdt64:
dq 0 ; 0x00: null
dq 0x00AF9A000000FFFF ; 0x08: kernel code (64-bit)
dq 0x00AF92000000FFFF ; 0x10: kernel data
dq 0x00AFFA000000FFFF ; 0x18: user code (64-bit, DPL=3)
dq 0x00AFF2000000FFFF ; 0x20: user data (DPL=3)
; TSS descriptor: 16 bytes (filled in by gdt.c)
dq 0 ; 0x28: TSS low
dq 0 ; 0x30: TSS high
gdtr64:
dw $ - gdt64 - 1
dq 0 ; base filled in by code above
; Kernel stack (8KB)
align 16
kernel_stack:
times 8192 db 0
kernel_stack_top:
The GDT: Segments and Privilege
The 64-bit GDT has 5 meaningful entries:
| Selector | Type | DPL | Purpose |
|---|---|---|---|
| 0x00 | Null | — | Required null descriptor |
| 0x08 | Code | 0 | Kernel code (64-bit, ring 0) |
| 0x10 | Data | 0 | Kernel data (ring 0) |
| 0x18 | Code | 3 | User code (64-bit, ring 3) |
| 0x20 | Data | 3 | User data (ring 3) |
| 0x28 | TSS | 0 | Task State Segment (16 bytes for 64-bit) |
The TSS holds the kernel stack pointer (RSP0) used when switching from user to kernel mode on interrupts. Without a valid TSS, interrupts from user mode cannot switch to the kernel stack correctly.
/* kernel/gdt.c */
#include "gdt.h"
static struct tss64 tss;
void gdt_set_tss_entry(uint64_t tss_base, uint32_t tss_limit) {
/* The TSS descriptor is 16 bytes (low 8 + high 8) */
/* Fill in at GDT offset 0x28 */
gdt[5] = ((tss_limit & 0xFFFF))
| ((tss_base & 0xFFFFFF) << 16)
| ((uint64_t)0x89 << 40) /* type: available TSS */
| ((uint64_t)((tss_limit >> 16) & 0xF) << 48)
| ((uint64_t)((tss_base >> 24) & 0xFF) << 56);
gdt[6] = (tss_base >> 32); /* high 32 bits of base */
}
void gdt_init(void) {
tss.rsp0 = (uint64_t)kernel_stack_top; /* kernel stack for ring 0 entry */
tss.iomap_base = sizeof(struct tss64); /* no I/O permission bitmap */
gdt_set_tss_entry((uint64_t)&tss, sizeof(struct tss64) - 1);
/* Load the TSS selector (0x28) into TR */
__asm__ volatile("ltr %%ax" :: "a"(0x28));
}
The IDT: Handling Interrupts and Exceptions
The IDT has 256 entries. MinOS handles: - Entries 0-31: CPU exceptions (divide error, page fault, general protection, etc.) - Entry 32-47: Hardware IRQs (remapped from PIC) - IRQ0 (entry 32): PIT timer - IRQ1 (entry 33): Keyboard - Entry 0x80: System call (INT 80h, legacy interface)
Each IDT entry must specify: the handler address, the segment selector, the gate type (interrupt gate), and the DPL.
/* kernel/idt.c — IDT entry setup */
void idt_set_gate(int vector, void *handler, uint8_t dpl) {
uint64_t addr = (uint64_t)handler;
idt[vector].offset_low = addr & 0xFFFF;
idt[vector].selector = 0x08; /* kernel code segment */
idt[vector].ist = 0;
idt[vector].type_attr = 0x8E | (dpl << 5); /* interrupt gate, present */
idt[vector].offset_mid = (addr >> 16) & 0xFFFF;
idt[vector].offset_high = (addr >> 32);
idt[vector].zero = 0;
}
void idt_init(void) {
/* Set up exception handlers */
idt_set_gate(0, &isr_divide_error, 0);
idt_set_gate(13, &isr_general_protect, 0);
idt_set_gate(14, &isr_page_fault, 0);
/* ... others ... */
/* Hardware IRQs */
pic_remap(0x20, 0x28); /* Remap PIC: IRQ0=0x20, IRQ8=0x28 */
idt_set_gate(0x20, &irq_timer, 0); /* PIT timer */
idt_set_gate(0x21, &irq_keyboard, 0); /* Keyboard */
/* Load IDT */
idtr.limit = sizeof(idt) - 1;
idtr.base = (uint64_t)idt;
__asm__ volatile("lidt %0" :: "m"(idtr));
/* Enable interrupts */
__asm__ volatile("sti");
}
The Physical Memory Allocator
The bitmap allocator from Chapter 27, adapted for MinOS:
/* kernel/mm/pmm.c */
#define BITMAP_SIZE (PHYSICAL_MEMORY_MAX / PAGE_SIZE / 8)
static uint8_t bitmap[BITMAP_SIZE];
void pmm_init(uint64_t mem_base, uint64_t mem_size) {
memset(bitmap, 0xFF, BITMAP_SIZE); /* mark all used */
/* Mark available memory as free */
uint64_t start_frame = (mem_base + PAGE_SIZE - 1) / PAGE_SIZE;
uint64_t end_frame = (mem_base + mem_size) / PAGE_SIZE;
for (uint64_t i = start_frame; i < end_frame; i++) {
bitmap[i / 8] &= ~(1 << (i % 8)); /* mark free */
}
/* Mark kernel pages as used */
for (uint64_t i = 0; i < KERNEL_END / PAGE_SIZE; i++) {
bitmap[i / 8] |= (1 << (i % 8)); /* mark used */
}
}
void *alloc_page(void) {
for (int i = 0; i < BITMAP_SIZE * 8; i++) {
if (!(bitmap[i / 8] & (1 << (i % 8)))) {
bitmap[i / 8] |= (1 << (i % 8));
return (void *)(uint64_t)(i * PAGE_SIZE);
}
}
return NULL; /* out of memory */
}
void free_page(void *addr) {
uint64_t frame = (uint64_t)addr / PAGE_SIZE;
bitmap[frame / 8] &= ~(1 << (frame % 8));
}
The VGA Driver
VGA text mode: 80×25 characters at physical address 0xB8000. Each character is 2 bytes: the ASCII character and an attribute byte (foreground/background color).
/* kernel/drivers/vga.c */
#define VGA_BASE 0xB8000
#define VGA_WIDTH 80
#define VGA_HEIGHT 25
#define VGA_WHITE_ON_BLACK 0x0F
static uint16_t *vga_buffer = (uint16_t *)VGA_BASE;
static int cursor_x = 0, cursor_y = 0;
void vga_putchar(char c) {
if (c == '\n') {
cursor_x = 0;
cursor_y++;
} else if (c == '\b') {
if (cursor_x > 0) cursor_x--;
} else {
int pos = cursor_y * VGA_WIDTH + cursor_x;
vga_buffer[pos] = (VGA_WHITE_ON_BLACK << 8) | c;
cursor_x++;
if (cursor_x >= VGA_WIDTH) { cursor_x = 0; cursor_y++; }
}
if (cursor_y >= VGA_HEIGHT) vga_scroll();
vga_update_cursor();
}
static void vga_scroll(void) {
/* Move rows 1..24 up to rows 0..23 */
memmove(vga_buffer, vga_buffer + VGA_WIDTH,
(VGA_HEIGHT - 1) * VGA_WIDTH * 2);
/* Clear last row */
for (int i = (VGA_HEIGHT-1)*VGA_WIDTH; i < VGA_HEIGHT*VGA_WIDTH; i++)
vga_buffer[i] = (VGA_WHITE_ON_BLACK << 8) | ' ';
cursor_y = VGA_HEIGHT - 1;
}
static void vga_update_cursor(void) {
uint16_t pos = cursor_y * VGA_WIDTH + cursor_x;
/* CRT controller ports: 0x3D4/0x3D5 */
outb(0x3D4, 0x0F);
outb(0x3D5, pos & 0xFF);
outb(0x3D4, 0x0E);
outb(0x3D5, (pos >> 8) & 0xFF);
}
The Timer: PIT at 100Hz
The Programmable Interval Timer (Intel 8253/8254) generates IRQ0 at a configurable rate:
/* kernel/drivers/timer.c */
#define PIT_CHANNEL0 0x40
#define PIT_CMD 0x43
#define PIT_FREQUENCY 100 /* Hz */
#define PIT_BASE_FREQ 1193182 /* PIT input frequency */
static volatile uint64_t ticks = 0;
void timer_init(void) {
uint16_t divisor = PIT_BASE_FREQ / PIT_FREQUENCY;
outb(PIT_CMD, 0x36); /* channel 0, mode 3 (square wave) */
outb(PIT_CHANNEL0, divisor & 0xFF);
outb(PIT_CHANNEL0, (divisor >> 8) & 0xFF);
}
void timer_handler(void) { /* called from IRQ0 interrupt handler */
ticks++;
scheduler_tick(); /* trigger preemption check */
pic_eoi(0); /* send End Of Interrupt to PIC */
}
uint64_t timer_get_ticks(void) { return ticks; }
The Scheduler: Round-Robin Preemptive
The MinOS scheduler supports two process slots (Track B) or more (Track C). Each timer tick checks if it is time to switch processes:
/* kernel/proc/scheduler.c */
#define MAX_PROCESSES 4
#define TIMESLICE_TICKS 10 /* 100ms at 100Hz */
struct process {
uint64_t rsp; /* saved stack pointer */
uint64_t rip; /* saved instruction pointer */
uint64_t rflags;
uint64_t rax, rbx, rcx, rdx;
uint64_t rsi, rdi, rbp;
uint64_t r8, r9, r10, r11, r12, r13, r14, r15;
int state; /* RUNNING, READY, BLOCKED, DEAD */
int pid;
char name[32];
};
static struct process processes[MAX_PROCESSES];
static int current_pid = 0;
static int ticks_remaining = TIMESLICE_TICKS;
/* Called from timer interrupt handler */
void scheduler_tick(void) {
ticks_remaining--;
if (ticks_remaining <= 0) {
ticks_remaining = TIMESLICE_TICKS;
schedule(); /* context switch */
}
}
void schedule(void) {
/* Save current process context (done by interrupt entry code) */
int next = (current_pid + 1) % MAX_PROCESSES;
while (processes[next].state != READY && next != current_pid)
next = (next + 1) % MAX_PROCESSES;
if (next == current_pid) return; /* no other runnable process */
current_pid = next;
/* Restore next process context (done by interrupt return code) */
/* The context switch is completed by the interrupt handler epilogue */
}
The actual context save/restore happens in assembly, in the interrupt handler entry/exit code:
; kernel/idt_entry.asm — interrupt handler with context save
%macro IRQ_HANDLER 1
irq_%1:
; Save all general registers
push rax
push rbx
push rcx
push rdx
push rsi
push rdi
push rbp
push r8
push r9
push r10
push r11
push r12
push r13
push r14
push r15
; Call C handler (which may call schedule())
mov rdi, %1 ; IRQ number
call irq_dispatch
; Restore registers
pop r15
pop r14
pop r13
pop r12
pop r11
pop r10
pop r9
pop r8
pop rbp
pop rdi
pop rsi
pop rdx
pop rcx
pop rbx
pop rax
iretq
%endmacro
When schedule() switches processes, the next IRETQ pops the saved context of the new process (from the new process's saved RSP). This is context switching via the interrupt stack.
The Shell: Minimal Command Interpreter
/* kernel/shell/shell.c */
static char cmd_buf[256];
static int cmd_pos = 0;
void shell_init(void) {
vga_puts("\nMinOS Shell - Type 'help' for commands\n");
vga_puts("minOS> ");
}
void shell_input(char c) {
if (c == '\n') {
cmd_buf[cmd_pos] = '\0';
vga_putchar('\n');
shell_execute(cmd_buf);
cmd_pos = 0;
vga_puts("minOS> ");
} else if (c == '\b' && cmd_pos > 0) {
cmd_pos--;
vga_puts("\b \b"); /* erase character on screen */
} else if (cmd_pos < 255) {
cmd_buf[cmd_pos++] = c;
vga_putchar(c);
}
}
static void shell_execute(const char *cmd) {
if (strcmp(cmd, "help") == 0) {
vga_puts("Commands: help, clear, uptime, meminfo, ps, reboot\n");
} else if (strcmp(cmd, "clear") == 0) {
vga_clear();
} else if (strcmp(cmd, "uptime") == 0) {
char buf[64];
uint64_t seconds = timer_get_ticks() / 100;
snprintf(buf, sizeof(buf), "Uptime: %llu seconds\n", seconds);
vga_puts(buf);
} else if (strcmp(cmd, "meminfo") == 0) {
pmm_print_info();
} else if (strcmp(cmd, "ps") == 0) {
scheduler_print_processes();
} else if (strcmp(cmd, "reboot") == 0) {
/* Triple fault: load null IDT and trigger fault */
__asm__ volatile("lidt %0; int3" :: "m"(null_idtr));
} else if (cmd[0] != '\0') {
vga_puts("Unknown command: ");
vga_puts(cmd);
vga_putchar('\n');
}
}
kernel_main: The Initialization Order
/* kernel/kernel.c */
void kernel_main(void) {
/* 1. VGA driver — first thing, so we can print */
vga_init();
vga_puts("[MinOS] Kernel starting...\n");
/* 2. Serial for debug (optional but useful) */
serial_init();
serial_puts("[debug] serial initialized\n");
/* 3. GDT — proper 64-bit GDT with TSS */
gdt_init();
vga_puts("[MinOS] GDT initialized\n");
/* 4. IDT — exceptions and IRQs */
idt_init();
vga_puts("[MinOS] IDT initialized\n");
/* 5. Physical memory manager */
uint64_t mem_size = detect_memory(); /* use BIOS e820 map (saved by bootloader) */
pmm_init(0x100000, mem_size - 0x100000);
vga_puts("[MinOS] Memory manager initialized\n");
/* 6. Virtual memory — set up kernel page tables */
vmm_init();
vga_puts("[MinOS] Virtual memory initialized\n");
/* 7. Timer — PIT at 100Hz */
timer_init();
vga_puts("[MinOS] Timer initialized (100Hz)\n");
/* 8. Keyboard */
keyboard_init();
vga_puts("[MinOS] Keyboard initialized\n");
/* 9. Scheduler — create initial processes */
scheduler_init();
/* Create kernel idle process (PID 0) */
/* Create shell process (PID 1) */
vga_puts("[MinOS] Scheduler initialized\n");
/* 10. Shell */
shell_init();
vga_puts("[MinOS] Boot complete. Shell ready.\n");
/* Enter idle loop — interrupts drive everything from here */
while (1) {
__asm__ volatile("hlt"); /* halt until next interrupt */
}
}
Building MinOS
# Makefile
CC = gcc
NASM = nasm
LD = ld
# Kernel C flags: no stdlib, no frame pointer, 64-bit
CFLAGS = -m64 -ffreestanding -fno-stack-protector -fno-pic \
-mno-red-zone -O2 -Wall -Wextra
# Linker script: place kernel at 0x10000
LDFLAGS = -T kernel.ld -nostdlib
BOOT_SRC = boot/boot.asm
KERNEL_C = kernel/kernel.c kernel/gdt.c kernel/idt.c \
kernel/mm/pmm.c kernel/mm/vmm.c \
kernel/drivers/vga.c kernel/drivers/keyboard.c \
kernel/drivers/serial.c kernel/drivers/timer.c \
kernel/proc/scheduler.c kernel/proc/process.c \
kernel/syscall/syscall.c kernel/shell/shell.c
KERNEL_ASM = kernel/entry.asm kernel/syscall/syscall.asm
all: minOS.img
boot.bin: $(BOOT_SRC)
$(NASM) -f bin -o $@ $<
kernel.bin: $(KERNEL_C) $(KERNEL_ASM)
$(NASM) -f elf64 -o entry.o kernel/entry.asm
$(NASM) -f elf64 -o syscall_entry.o kernel/syscall/syscall.asm
$(CC) $(CFLAGS) -c $(KERNEL_C)
$(LD) $(LDFLAGS) -o kernel.bin entry.o syscall_entry.o *.o
minOS.img: boot.bin kernel.bin
cat boot.bin kernel.bin > minOS.img
# Pad to sector boundary
truncate -s 1474560 minOS.img # 1.44MB floppy size
run: minOS.img
qemu-system-x86_64 -drive format=raw,file=minOS.img \
-serial stdio \
-m 64M
debug: minOS.img
qemu-system-x86_64 -drive format=raw,file=minOS.img \
-serial stdio \
-m 64M \
-s -S &
gdb -x gdb_minOS.gdb
clean:
rm -f *.o *.bin *.img
The linker script kernel.ld places the kernel at 0x10000 (where the bootloader loaded it) and defines BSS boundaries.
Running MinOS in QEMU
# Build
make
# Run
make run
# QEMU opens, MinOS boots, shell appears
# Debug (separate terminal)
make debug
# GDB connects to QEMU via :1234, set breakpoints in kernel_main
Expected boot output:
[MinOS] Kernel starting...
[MinOS] GDT initialized
[MinOS] IDT initialized
[MinOS] Memory manager initialized (62MB available)
[MinOS] Virtual memory initialized
[MinOS] Timer initialized (100Hz)
[MinOS] Keyboard initialized
[MinOS] Scheduler initialized
[MinOS] Boot complete. Shell ready.
MinOS Shell - Type 'help' for commands
minOS>
Three Capstone Tracks
Track A (Minimal — ~400 lines of assembly, ~800 lines of C): - Bootloader + long mode switch - VGA text output (80×25) - Keyboard input (polling or interrupt) - Simple command shell (no scheduler) - Memory: static allocation only
Track B (Standard — Track A + ~500 lines): - All of Track A - PIT timer at 100Hz - Round-robin preemptive scheduler - 2-4 concurrent kernel threads - Physical memory bitmap allocator - Kernel-mode context switch
Track C (Extended — Track B + ~1000 lines): - All of Track B - Virtual memory (user/kernel separation) - System call interface (SYSCALL/SYSRET) - Simple FAT12 filesystem (or RAM disk) - User-mode shell process (runs in ring 3) - User-mode programs (via ELF loading or simple format)
What You Have Proven
When MinOS boots and the shell prompt appears, you have demonstrated:
- You understand the x86-64 boot sequence at every instruction
- You wrote the interrupt descriptor table by hand
- You wrote the memory allocator that manages physical pages
- You wrote the scheduler that preempts processes
- You wrote the device drivers for VGA, keyboard, and timer
- You wrote the shell that reads input and executes commands
No textbook, course, or tutorial can give you this. You built it yourself, and you understand every instruction in it. This is what "demystified" means in practice.
There is no magic in MinOS. There is assembly language all the way down.
🛠️ Lab Exercise: After completing MinOS, add a
memtestshell command that writes a pattern to every free page and reads it back, verifying memory integrity. This is an excellent debug tool and demonstrates that you control the hardware completely.📐 OS Kernel Project: This IS the final OS Kernel Project chapter. All previous kernel project steps — IDT from Chapter 26, allocator from Chapter 27, bootloader from Chapter 28, drivers from Chapter 29, locks from Chapter 30 — are integrated here.
🔄 Check Your Understanding: 1. Why must the TSS be initialized before user-mode code can run? 2. What happens if the IDT is not initialized before
sti(enable interrupts)? 3. Why is the initialization order inkernel_mainimportant? What breaks if VGA comes after IDT? 4. How does the preemptive scheduler work without explicitschedule()calls from user code? 5. What is the minimum information needed to perform a context switch between two kernel threads?
Summary
MinOS is a complete, bootable operating system written in assembly and C that demonstrates every concept from the book in working form. The bootloader transitions from 16-bit real mode to 64-bit long mode. The kernel initializes the GDT (with TSS), IDT (with exception and IRQ handlers), physical memory manager, virtual memory, VGA driver, keyboard driver, PIT timer, preemptive scheduler, and command shell. Building it proves that every layer of the computer — from power-on to user input — is understood at the instruction level. There is no magic, only assembly instructions all the way down.