Back to Technology

x86 Assembly Series Part 24: Capstone Project - Mini OS

February 6, 2026 Wasil Zafar 60 min read

Build a minimal operating system from scratch: bootloader, protected mode entry, interrupt handling, keyboard driver, VGA text output, and a simple command shell—the ultimate assembly capstone!

Table of Contents

  1. Project Overview
  2. Stage 1: Bootloader
  3. Stage 2: Protected Mode
  4. Stage 3: Interrupts
  5. Stage 4: Keyboard Driver
  6. Stage 5: VGA Text Output
  7. Stage 6: Command Shell
  8. Building & Testing

Project Overview

Capstone Goal: Build a minimal OS that boots from BIOS, enters protected mode, handles keyboard interrupts, displays text on screen, and runs a simple shell. This combines everything from Parts 0-23!
Architecture

Mini OS Components

mini-os/
├── boot.asm          # Stage 1 bootloader (MBR)
├── loader.asm        # Stage 2 loader (protected mode)
├── kernel.asm        # Kernel entry point
├── idt.asm           # Interrupt Descriptor Table
├── keyboard.asm      # PS/2 keyboard driver
├── vga.asm           # VGA text mode output
├── shell.asm         # Command shell
├── linker.ld         # Linker script
└── Makefile          # Build automation

Stage 1: Bootloader

; boot.asm - First stage bootloader (512 bytes)
[BITS 16]
[ORG 0x7C00]

start:
    cli
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00
    sti

    ; Display boot message
    mov si, msg_boot
    call print_string

    ; Load stage 2 from disk
    mov ah, 0x02            ; BIOS read sectors
    mov al, 4               ; Read 4 sectors
    mov ch, 0               ; Cylinder 0
    mov cl, 2               ; Start at sector 2
    mov dh, 0               ; Head 0
    mov bx, 0x7E00          ; Load address
    int 0x13
    jc .disk_error

    ; Jump to stage 2
    jmp 0x0000:0x7E00

.disk_error:
    mov si, msg_error
    call print_string
    hlt

print_string:
    mov ah, 0x0E
.loop:
    lodsb
    test al, al
    jz .done
    int 0x10
    jmp .loop
.done:
    ret

msg_boot:  db "Mini OS Booting...", 13, 10, 0
msg_error: db "Disk Error!", 0

times 510 - ($ - $$) db 0
dw 0xAA55

Stage 2: Protected Mode Entry

Protected mode gives us 32-bit addressing, memory protection, and paging. The key is setting up a Global Descriptor Table (GDT).

GDT Layout for Mini OS:
┌─────────┬─────────┬─────────┬──────────┐
│  NULL   │  Code   │  Data   │  Limit   │
│  (0x00) │  (0x08) │  (0x10) │          │
└─────────┴─────────┴─────────┴──────────┘

Segment selectors: Code = 0x08, Data = 0x10
; loader.asm - Stage 2: Protected mode entry
[BITS 16]
[ORG 0x7E00]

loader_start:
    ; Print entering protected mode message
    mov si, msg_pm
    call print_string_16
    
    ; Enable A20 line (fast method)
    in al, 0x92
    or al, 2
    out 0x92, al
    
    ; Load GDT
    cli
    lgdt [gdt_descriptor]
    
    ; Set PE (Protection Enable) bit in CR0
    mov eax, cr0
    or eax, 1
    mov cr0, eax
    
    ; Far jump to flush pipeline and enter 32-bit code
    jmp 0x08:protected_mode

print_string_16:
    mov ah, 0x0E
.loop:
    lodsb
    test al, al
    jz .done
    int 0x10
    jmp .loop
.done:
    ret

msg_pm: db "Entering protected mode...", 13, 10, 0

; ===== GDT =====
align 8
gdt_start:
    ; NULL descriptor (required)
    dq 0
    
    ; Code segment: base=0, limit=4GB, 32-bit, ring 0
gdt_code:
    dw 0xFFFF       ; Limit low (0-15)
    dw 0x0000       ; Base low (0-15)
    db 0x00         ; Base middle (16-23)
    db 10011010b    ; Access: Present, Ring 0, Code, Executable, Readable
    db 11001111b    ; Flags: 4KB granularity, 32-bit + Limit high (16-19)
    db 0x00         ; Base high (24-31)
    
    ; Data segment: base=0, limit=4GB, 32-bit, ring 0
gdt_data:
    dw 0xFFFF       ; Limit low
    dw 0x0000       ; Base low
    db 0x00         ; Base middle
    db 10010010b    ; Access: Present, Ring 0, Data, Writable
    db 11001111b    ; Flags: 4KB granularity, 32-bit + Limit high
    db 0x00         ; Base high
gdt_end:

gdt_descriptor:
    dw gdt_end - gdt_start - 1  ; Size - 1
    dd gdt_start                 ; Address

; ===== 32-bit Protected Mode =====
[BITS 32]
protected_mode:
    ; Setup segment registers
    mov ax, 0x10        ; Data segment selector
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 0x90000    ; Stack below 1MB
    
    ; Clear screen
    call clear_screen_32
    
    ; Print welcome message
    mov esi, msg_welcome
    mov edi, 0xB8000    ; VGA buffer
    mov ah, 0x0F        ; White on black
    call print_string_32
    
    ; Jump to kernel
    jmp 0x10000         ; Kernel loaded here
    
clear_screen_32:
    mov edi, 0xB8000
    mov ecx, 80*25
    mov ax, 0x0720      ; Space, gray on black
    rep stosw
    ret

print_string_32:
.loop:
    lodsb
    test al, al
    jz .done
    stosw
    jmp .loop
.done:
    ret

msg_welcome: db "Welcome to Mini OS [32-bit Protected Mode]", 0

times 2048 - ($ - $$) db 0  ; Pad to 4 sectors

Stage 3: Interrupt Handling

The Interrupt Descriptor Table (IDT) tells the CPU where to jump when interrupts occur—hardware events (keyboard, timer) and software exceptions (divide by zero, page fault).

IDT Entry (8 bytes each):
┌──────────┬──────────┬────────┬────────┐
│ Offset  │ Selector │  Type  │ Offset │
│  0-15   │  16-31   │  Attr  │  16-31 │
└──────────┴──────────┴────────┴────────┘

Important Interrupts:
  0: Divide Error       8: Double Fault
 13: General Protection 14: Page Fault
 32: Timer (IRQ0)       33: Keyboard (IRQ1)
; idt.asm - Interrupt Descriptor Table setup
[BITS 32]

section .data

; IDT - 256 entries * 8 bytes = 2048 bytes
align 8
idt_start:
    times 256 dq 0      ; Initialize to zero
idt_end:

idt_descriptor:
    dw idt_end - idt_start - 1
    dd idt_start

; ISR stubs array for easy lookup
isr_stubs:
    dd isr0, isr1, isr2, isr3, isr4, isr5, isr6, isr7
    dd isr8, isr9, isr10, isr11, isr12, isr13, isr14, isr15
    dd isr16, isr17, isr18, isr19, isr20, isr21, isr22, isr23
    dd isr24, isr25, isr26, isr27, isr28, isr29, isr30, isr31
    dd irq0, irq1       ; Hardware interrupts 32-33

section .text

; Setup one IDT entry
; EAX = interrupt number, EBX = handler address
set_idt_entry:
    push edi
    mov edi, idt_start
    shl eax, 3          ; * 8 bytes per entry
    add edi, eax
    
    mov word [edi], bx      ; Offset low
    mov word [edi+2], 0x08  ; Code segment selector
    mov byte [edi+4], 0x00  ; Reserved
    mov byte [edi+5], 0x8E  ; Type: 32-bit interrupt gate, ring 0, present
    shr ebx, 16
    mov word [edi+6], bx    ; Offset high
    
    pop edi
    ret

; Initialize IDT with all ISR handlers
idt_init:
    ; Setup exception handlers (0-31)
    xor eax, eax
.setup_loop:
    push eax
    mov ebx, [isr_stubs + eax*4]
    call set_idt_entry
    pop eax
    inc eax
    cmp eax, 34         ; 0-31 exceptions + IRQ 0-1
    jl .setup_loop
    
    ; Remap PIC (8259) - move IRQs to 32-47
    call pic_remap
    
    ; Load IDT
    lidt [idt_descriptor]
    sti                 ; Enable interrupts
    ret

; Remap PIC to avoid conflicts with CPU exceptions
pic_remap:
    ; ICW1: Initialize
    mov al, 0x11
    out 0x20, al        ; Master PIC
    out 0xA0, al        ; Slave PIC
    
    ; ICW2: Vector offset
    mov al, 0x20        ; Master: IRQ 0-7 -> INT 32-39
    out 0x21, al
    mov al, 0x28        ; Slave: IRQ 8-15 -> INT 40-47
    out 0xA1, al
    
    ; ICW3: Cascade
    mov al, 0x04        ; Master: Slave on IRQ2
    out 0x21, al
    mov al, 0x02        ; Slave: Cascade identity
    out 0xA1, al
    
    ; ICW4: 8086 mode
    mov al, 0x01
    out 0x21, al
    out 0xA1, al
    
    ; Mask all interrupts except keyboard (IRQ1)
    mov al, 0xFD        ; 11111101 - only IRQ1 enabled
    out 0x21, al
    mov al, 0xFF        ; Disable all slave IRQs
    out 0xA1, al
    
    ret

; Macro to generate ISR stub (no error code)
%macro ISR_NOERRCODE 1
isr%1:
    push dword 0        ; Dummy error code
    push dword %1       ; Interrupt number
    jmp isr_common
%endmacro

; Macro for ISR with error code
%macro ISR_ERRCODE 1
isr%1:
    push dword %1       ; CPU already pushed error code
    jmp isr_common
%endmacro

; Generate ISR stubs
ISR_NOERRCODE 0     ; Divide Error
ISR_NOERRCODE 1     ; Debug
ISR_NOERRCODE 2     ; NMI
ISR_NOERRCODE 3     ; Breakpoint
ISR_NOERRCODE 4     ; Overflow
ISR_NOERRCODE 5     ; Bound Range
ISR_NOERRCODE 6     ; Invalid Opcode
ISR_NOERRCODE 7     ; Device Not Available
ISR_ERRCODE   8     ; Double Fault
ISR_NOERRCODE 9     ; Coprocessor Segment
ISR_ERRCODE   10    ; Invalid TSS
ISR_ERRCODE   11    ; Segment Not Present
ISR_ERRCODE   12    ; Stack Segment Fault
ISR_ERRCODE   13    ; General Protection Fault
ISR_ERRCODE   14    ; Page Fault
ISR_NOERRCODE 15    ; Reserved
ISR_NOERRCODE 16    ; x87 FPU Error
ISR_ERRCODE   17    ; Alignment Check
ISR_NOERRCODE 18    ; Machine Check
ISR_NOERRCODE 19    ; SIMD Exception
%assign i 20
%rep 12
ISR_NOERRCODE i
%assign i i+1
%endrep

; Common ISR handler
isr_common:
    pusha               ; Save all registers
    push ds
    push es
    push fs
    push gs
    
    mov ax, 0x10        ; Kernel data segment
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    
    ; Call C handler: void isr_handler(registers_t* regs)
    push esp            ; Pointer to register struct
    extern isr_handler
    call isr_handler
    add esp, 4
    
    pop gs
    pop fs
    pop es
    pop ds
    popa
    add esp, 8          ; Pop error code and interrupt number
    iret

; IRQ handlers
irq0:                   ; Timer
    push dword 0
    push dword 32
    jmp irq_common

irq1:                   ; Keyboard
    push dword 0
    push dword 33
    jmp irq_common

irq_common:
    pusha
    push ds
    push es
    push fs
    push gs
    
    mov ax, 0x10
    mov ds, ax
    mov es, ax
    
    push esp
    extern irq_handler
    call irq_handler
    add esp, 4
    
    ; Send EOI to PIC
    mov al, 0x20
    out 0x20, al
    
    pop gs
    pop fs
    pop es
    pop ds
    popa
    add esp, 8
    iret

Stage 4: Keyboard Driver

The PS/2 keyboard sends scan codes to port 0x60. We need to convert these to ASCII characters for our shell.

; keyboard.asm - PS/2 keyboard driver
[BITS 32]

section .data

; Keyboard buffer (circular)
KEY_BUFFER_SIZE equ 256
key_buffer:     times KEY_BUFFER_SIZE db 0
key_head:       dd 0
key_tail:       dd 0

; Scan code to ASCII lookup table (US QWERTY)
; Index = scan code, value = ASCII (0 = no mapping)
scancode_table:
    ;  0    1    2    3    4    5    6    7    8    9    A    B    C    D    E    F
    db 0,   27,  '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 8,   9    ; 0x00-0x0F
    db 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', 13,  0,   'a', 's' ; 0x10-0x1F
    db 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', "'", '`', 0,   '\', 'z', 'x', 'c', 'v' ; 0x20-0x2F
    db 'b', 'n', 'm', ',', '.', '/', 0,   '*', 0,   ' ', 0,   0,   0,   0,   0,   0   ; 0x30-0x3F

; Shift key scan code to ASCII
scancode_shift:
    db 0,   27,  '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', 8,   9
    db 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', 13,  0,   'A', 'S'
    db 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"', '~', 0,   '|', 'Z', 'X', 'C', 'V'
    db 'B', 'N', 'M', '<', '>', '?', 0,   '*', 0,   ' ', 0,   0,   0,   0,   0,   0

shift_pressed:  db 0

section .text

; Keyboard IRQ handler (IRQ1 = INT 33)
global keyboard_handler
keyboard_handler:
    push eax
    push ebx
    push esi
    
    ; Read scan code from keyboard controller
    in al, 0x60
    
    ; Check for key release (bit 7 set)
    test al, 0x80
    jnz .key_release
    
    ; Key press
    ; Check for shift key (scan codes 0x2A, 0x36)
    cmp al, 0x2A
    je .shift_press
    cmp al, 0x36
    je .shift_press
    
    ; Convert scan code to ASCII
    movzx ebx, al
    cmp byte [shift_pressed], 0
    jnz .use_shift
    mov al, [scancode_table + ebx]
    jmp .check_valid
.use_shift:
    mov al, [scancode_shift + ebx]
    
.check_valid:
    test al, al             ; Check if valid character
    jz .done
    
    ; Add to circular buffer
    mov esi, [key_head]
    mov [key_buffer + esi], al
    inc esi
    and esi, KEY_BUFFER_SIZE - 1  ; Wrap around
    mov [key_head], esi
    jmp .done
    
.shift_press:
    mov byte [shift_pressed], 1
    jmp .done
    
.key_release:
    ; Check for shift release
    and al, 0x7F            ; Clear release bit
    cmp al, 0x2A
    je .shift_release
    cmp al, 0x36
    je .shift_release
    jmp .done
    
.shift_release:
    mov byte [shift_pressed], 0
    
.done:
    pop esi
    pop ebx
    pop eax
    ret

; Read character from keyboard buffer
; Returns: AL = character, or 0 if buffer empty
global kbd_getchar
kbd_getchar:
    mov eax, [key_tail]
    cmp eax, [key_head]     ; Check if buffer empty
    je .empty
    
    movzx eax, byte [key_buffer + eax]
    mov ebx, [key_tail]
    inc ebx
    and ebx, KEY_BUFFER_SIZE - 1
    mov [key_tail], ebx
    ret
    
.empty:
    xor eax, eax
    ret

; Wait for and return a character
global kbd_getchar_blocking
kbd_getchar_blocking:
    call kbd_getchar
    test al, al
    jz kbd_getchar_blocking ; Busy wait
    ret
Note: This is a simple polling/interrupt driver. Production drivers handle extended scan codes (0xE0 prefix), caps lock, and multiple keyboard layouts.

Stage 5: VGA Text Output

VGA text mode uses a memory-mapped buffer at 0xB8000. Each character is 2 bytes: ASCII + attribute (color).

VGA Text Mode Memory Layout (80x25):

0xB8000: ┌─────┬─────┬─────┬───┬─────┬─────┐
         │Char │Attr │Char │...│Char │Attr │ Row 0 (160 bytes)
         └─────┴─────┴─────┴───┴─────┴─────┘
0xB80A0: Row 1...
0xB8F00: Row 24 (last row)

Attribute Byte:
 ┌───┬───────┬────────┐
 │ 7 │ 6-4   │  3-0   │
 ├───┼───────┼────────┤
 │Blk│BackRGB│ForeRGBI│
 └───┴───────┴────────┘

Colors: 0=Black 1=Blue 2=Green 3=Cyan 4=Red 5=Magenta 6=Brown 7=LightGray
        8=DarkGray 9=LightBlue A=LightGreen B=LightCyan C=LightRed
        D=LightMagenta E=Yellow F=White
; vga.asm - VGA text mode driver
[BITS 32]

section .data
    VGA_BUFFER equ 0xB8000
    VGA_WIDTH  equ 80
    VGA_HEIGHT equ 25
    
    cursor_x:   dd 0
    cursor_y:   dd 0
    vga_color:  db 0x07      ; Default: white on black

section .text

; Set text color
; Input: AL = color attribute
global vga_set_color
vga_set_color:
    mov [vga_color], al
    ret

; Clear screen
global vga_clear
vga_clear:
    push edi
    push ecx
    
    mov edi, VGA_BUFFER
    mov ecx, VGA_WIDTH * VGA_HEIGHT
    mov ah, [vga_color]
    mov al, ' '
    rep stosw
    
    ; Reset cursor
    mov dword [cursor_x], 0
    mov dword [cursor_y], 0
    call update_hardware_cursor
    
    pop ecx
    pop edi
    ret

; Print single character
; Input: AL = character
global vga_putchar
vga_putchar:
    push ebx
    push edi
    
    ; Handle special characters
    cmp al, 10              ; Newline
    je .newline
    cmp al, 13              ; Carriage return
    je .carriage_return
    cmp al, 8               ; Backspace
    je .backspace
    cmp al, 9               ; Tab
    je .tab
    
    ; Calculate buffer position: (y * 80 + x) * 2 + 0xB8000
    mov ebx, [cursor_y]
    imul ebx, VGA_WIDTH
    add ebx, [cursor_x]
    shl ebx, 1              ; * 2 bytes per character
    add ebx, VGA_BUFFER
    
    ; Write character + attribute
    mov ah, [vga_color]
    mov [ebx], ax
    
    ; Advance cursor
    inc dword [cursor_x]
    cmp dword [cursor_x], VGA_WIDTH
    jl .done
    
.newline:
    mov dword [cursor_x], 0
    inc dword [cursor_y]
    jmp .check_scroll
    
.carriage_return:
    mov dword [cursor_x], 0
    jmp .done
    
.backspace:
    cmp dword [cursor_x], 0
    je .done
    dec dword [cursor_x]
    ; Erase character at cursor
    mov al, ' '
    call vga_putchar
    dec dword [cursor_x]
    jmp .done
    
.tab:
    mov eax, [cursor_x]
    add eax, 8
    and eax, ~7             ; Align to 8
    mov [cursor_x], eax
    cmp eax, VGA_WIDTH
    jl .done
    jmp .newline
    
.check_scroll:
    cmp dword [cursor_y], VGA_HEIGHT
    jl .done
    call scroll_screen
    
.done:
    call update_hardware_cursor
    pop edi
    pop ebx
    ret

; Print null-terminated string
; Input: ESI = pointer to string
global vga_print
vga_print:
    push esi
    push eax
.loop:
    lodsb
    test al, al
    jz .done
    call vga_putchar
    jmp .loop
.done:
    pop eax
    pop esi
    ret

; Print string with newline
global vga_println
vga_println:
    call vga_print
    mov al, 10
    call vga_putchar
    ret

; Scroll screen up one line
scroll_screen:
    push esi
    push edi
    push ecx
    
    ; Copy lines 1-24 to 0-23
    mov edi, VGA_BUFFER
    mov esi, VGA_BUFFER + VGA_WIDTH * 2
    mov ecx, VGA_WIDTH * (VGA_HEIGHT - 1)
    rep movsw
    
    ; Clear last line
    mov ecx, VGA_WIDTH
    mov ah, [vga_color]
    mov al, ' '
    rep stosw
    
    ; Move cursor up
    dec dword [cursor_y]
    
    pop ecx
    pop edi
    pop esi
    ret

; Update hardware cursor position
update_hardware_cursor:
    push eax
    push ebx
    push edx
    
    mov ebx, [cursor_y]
    imul ebx, VGA_WIDTH
    add ebx, [cursor_x]
    
    ; Cursor low byte
    mov dx, 0x3D4
    mov al, 0x0F
    out dx, al
    mov dx, 0x3D5
    mov al, bl
    out dx, al
    
    ; Cursor high byte
    mov dx, 0x3D4
    mov al, 0x0E
    out dx, al
    mov dx, 0x3D5
    mov al, bh
    out dx, al
    
    pop edx
    pop ebx
    pop eax
    ret

Stage 6: Command Shell

A simple command-line shell that ties everything together: reading keyboard input, parsing commands, and executing actions.

; shell.asm - Simple command shell
[BITS 32]

section .data
    prompt:     db "MiniOS> ", 0
    cmd_buffer: times 256 db 0
    cmd_len:    dd 0
    
    ; Built-in commands
    cmd_help:   db "help", 0
    cmd_clear:  db "clear", 0
    cmd_echo:   db "echo", 0
    cmd_reboot: db "reboot", 0
    
    ; Help text
    help_text:
        db "Mini OS Shell Commands:", 10
        db "  help   - Show this help", 10
        db "  clear  - Clear screen", 10
        db "  echo   - Echo text", 10
        db "  reboot - Restart system", 10, 0
    
    unknown_cmd: db "Unknown command: ", 0
    newline:     db 10, 0

section .text

; Main shell loop
global shell_run
shell_run:
    ; Clear command buffer
    call clear_cmd_buffer
    
    ; Print prompt
    mov esi, prompt
    call vga_print
    
.input_loop:
    ; Get character from keyboard (blocking)
    call kbd_getchar_blocking
    
    ; Handle special keys
    cmp al, 13              ; Enter
    je .execute
    cmp al, 8               ; Backspace
    je .backspace
    
    ; Printable character - add to buffer
    mov ebx, [cmd_len]
    cmp ebx, 254            ; Buffer full?
    jge .input_loop
    
    mov [cmd_buffer + ebx], al
    inc dword [cmd_len]
    
    ; Echo character
    call vga_putchar
    jmp .input_loop
    
.backspace:
    cmp dword [cmd_len], 0
    je .input_loop
    dec dword [cmd_len]
    mov al, 8               ; Backspace
    call vga_putchar
    jmp .input_loop
    
.execute:
    ; Null-terminate command
    mov ebx, [cmd_len]
    mov byte [cmd_buffer + ebx], 0
    
    ; Print newline
    mov al, 10
    call vga_putchar
    
    ; Empty command?
    cmp dword [cmd_len], 0
    je shell_run
    
    ; Parse and execute command
    call parse_command
    jmp shell_run

clear_cmd_buffer:
    push edi
    push ecx
    mov edi, cmd_buffer
    xor eax, eax
    mov ecx, 256
    rep stosb
    mov dword [cmd_len], 0
    pop ecx
    pop edi
    ret

; Parse and execute command
parse_command:
    ; Compare with built-in commands
    mov esi, cmd_buffer
    
    ; Check "help"
    mov edi, cmd_help
    call strcmp
    test eax, eax
    jz .do_help
    
    ; Check "clear"
    mov edi, cmd_clear
    call strcmp
    test eax, eax
    jz .do_clear
    
    ; Check "echo " (with space)
    mov esi, cmd_buffer
    mov edi, cmd_echo
    push esi
    call strncmp_4          ; Compare first 4 chars
    pop esi
    test eax, eax
    jz .do_echo
    
    ; Check "reboot"
    mov esi, cmd_buffer
    mov edi, cmd_reboot
    call strcmp
    test eax, eax
    jz .do_reboot
    
    ; Unknown command
    mov esi, unknown_cmd
    call vga_print
    mov esi, cmd_buffer
    call vga_println
    ret
    
.do_help:
    mov esi, help_text
    call vga_print
    ret
    
.do_clear:
    call vga_clear
    ret
    
.do_echo:
    ; Print everything after "echo "
    mov esi, cmd_buffer
    add esi, 5              ; Skip "echo "
    call vga_println
    ret
    
.do_reboot:
    ; Triple fault reboot (crude but effective)
    lidt [null_idt]
    int 0                   ; Triple fault -> reboot
    ret

null_idt:
    dw 0
    dd 0

; String compare (returns 0 if equal)
; ESI = string1, EDI = string2
strcmp:
    push esi
    push edi
.loop:
    lodsb
    mov ah, [edi]
    inc edi
    cmp al, ah
    jne .not_equal
    test al, al
    jnz .loop
    xor eax, eax            ; Equal
    jmp .done
.not_equal:
    mov eax, 1
.done:
    pop edi
    pop esi
    ret

; Compare first 4 characters
strncmp_4:
    mov eax, [esi]
    cmp eax, [edi]
    jne .not_equal
    xor eax, eax
    ret
.not_equal:
    mov eax, 1
    ret
Challenge

Extend the Shell

Add these features to make your Mini OS more useful:

  1. date - Read RTC and display current time
  2. mem - Show memory map and usage
  3. color <fg> <bg> - Change text colors
  4. uptime - Track time since boot using PIT timer
  5. history - Keep last 10 commands with up/down navigation

Building & Testing

# Makefile
all: os.img

boot.bin: boot.asm
    nasm -f bin boot.asm -o boot.bin

kernel.bin: kernel.asm
    nasm -f bin kernel.asm -o kernel.bin

os.img: boot.bin kernel.bin
    cat boot.bin kernel.bin > os.img

run: os.img
    qemu-system-i386 -drive format=raw,file=os.img

debug: os.img
    qemu-system-i386 -drive format=raw,file=os.img -s -S &
    gdb -ex "target remote :1234" -ex "set architecture i8086"

clean:
    rm -f *.bin *.img
Series Complete! You've mastered x86 assembly from environment setup through building a mini operating system. Continue exploring: Linux kernel, game development, or reverse engineering!