Project 13: A Self-Hosting, no_std, Async OS Core

Project 13: A Self-Hosting, no_std, Async OS Core

“The OS is the ultimate systems programming challenge - where every abstraction you’ve ever used dissolves, and you become the foundation upon which all other software stands.” - Linus Torvalds (paraphrased)


Project Metadata

  • Main Programming Language: Rust
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Difficulty: Level 5: Master
  • Knowledge Area: Operating Systems / Embedded Systems / Async Runtime / Kernel Development
  • Time Estimate: 2-3 months
  • Prerequisites:
    • Project 3: Custom Arena Allocator (memory management fundamentals)
    • Project 4: no_std Kernel Core (bare metal basics)
    • Project 5: Const Generic Matrix (type-safe compile-time programming)
    • Project 8: Building a Custom Runtime (async executor internals)
    • Project 10: Procedural Macro for Trait Reflection (metaprogramming)

What You Will Build

This is the ultimate test of your Rust journey. You will build a micro-kernel that:

  1. Runs in no_std mode on bare metal (QEMU x86_64)
  2. Uses a Custom Global Allocator that you wrote (from Project 3)
  3. Implements an Async Executor to handle hardware interrupts as futures (from Project 8)
  4. Uses Procedural Macros to define system calls (from Project 10)
  5. Employs Const Generics for type-safe memory maps (from Project 5)

A functioning micro-OS that can run its own shell and execute simple user programs. You will be one of the few developers on earth who has built an OS from scratch using modern memory-safe languages with async support at the kernel level.


Learning Objectives

By the end of this project, you will be able to:

  1. Explain the complete boot sequence from BIOS/UEFI through bootloader to kernel entry point, including CPU mode transitions
  2. Implement a custom global allocator that integrates with your kernel’s memory management subsystem
  3. Design and implement an interrupt-driven async executor that treats hardware interrupts as futures
  4. Create procedural macros for system call definitions that generate the necessary kernel/user space transition code
  5. Use const generics to create type-safe memory region mappings that catch errors at compile time
  6. Implement basic process/task scheduling using cooperative multitasking via async/await
  7. Build a complete interrupt handling infrastructure including IDT setup, ISRs, and interrupt controllers
  8. Design memory-mapped I/O abstractions for VGA, keyboard, and serial communication
  9. Create a functional shell that accepts user input and executes built-in commands
  10. Understand the trade-offs between micro-kernel and monolithic designs and why Rust’s safety guarantees change the equation

Deep Theoretical Foundation

This section provides the comprehensive theoretical knowledge required to build an operating system. Read this carefully before writing any code.

Part 1: What an Operating System Actually Does

At its core, an operating system provides three fundamental services:

THE THREE PILLARS OF AN OPERATING SYSTEM
================================================================================

                    +------------------------------------------+
                    |            USER PROGRAMS                  |
                    |   (Your shell, editors, compilers...)    |
                    +------------------------------------------+
                                      |
                                      | System Calls
                                      v
    +------------------------------------------------------------------+
    |                     OPERATING SYSTEM KERNEL                       |
    |                                                                  |
    |  +------------------+  +------------------+  +------------------+ |
    |  |   ABSTRACTION    |  |   PROTECTION     |  | RESOURCE MGMT    | |
    |  |                  |  |                  |  |                  | |
    |  | - Virtual Memory |  | - Memory Prot.   |  | - CPU Scheduling | |
    |  | - File Systems   |  | - User/Kernel    |  | - Memory Alloc   | |
    |  | - Device Drivers |  | - Capabilities   |  | - I/O Bandwidth  | |
    |  | - Processes      |  | - Sandboxing     |  | - Power Mgmt     | |
    |  +------------------+  +------------------+  +------------------+ |
    +------------------------------------------------------------------+
                                      |
                                      | Direct Hardware Access
                                      v
    +------------------------------------------------------------------+
    |                         HARDWARE                                  |
    |   CPU   |   RAM   |   Disk   |   Network   |   Keyboard   | ...  |
    +------------------------------------------------------------------+


PILLAR 1: ABSTRACTION
----------------------
The OS presents a simplified, uniform interface to complex hardware:

  Before OS:
    "Write byte 0x41 to port 0x3F8, wait for status register bit 5..."

  With OS:
    write(STDOUT, "A", 1);  // Just write a character!

  Examples:
    - Files abstract disk sectors and filesystem structures
    - Virtual memory abstracts physical RAM locations
    - Processes abstract CPU execution contexts
    - Sockets abstract network packet handling


PILLAR 2: PROTECTION
--------------------
The OS prevents programs from interfering with each other:

  Without protection:
    Program A: writes to address 0x8000 (its data)
    Program B: writes to address 0x8000 (A's data is now corrupted!)

  With protection:
    Program A: writes to VIRTUAL address 0x8000 -> PHYSICAL 0x10000
    Program B: writes to VIRTUAL address 0x8000 -> PHYSICAL 0x20000
    (Each program has isolated address space)

  Protection mechanisms:
    - Memory Management Unit (MMU) for page-based protection
    - CPU privilege levels (Ring 0 = kernel, Ring 3 = user)
    - System call gates for controlled kernel entry


PILLAR 3: RESOURCE MANAGEMENT
-----------------------------
The OS arbitrates access to shared resources:

  CPU Time:
    - Multiple programs want to run
    - OS schedules them fairly (or based on priority)
    - Preemptive: OS forcibly switches tasks
    - Cooperative: Tasks voluntarily yield (our async approach!)

  Memory:
    - Programs request memory (malloc/Box::new)
    - OS tracks what's allocated where
    - Reclaims memory when programs exit

  I/O Devices:
    - Single keyboard, many programs
    - OS delivers input to the "focused" program
    - Buffers output to prevent mixing

Book Reference: “Operating Systems: Three Easy Pieces” by Remzi & Andrea Arpaci-Dusseau provides an excellent free introduction to these concepts.

Part 2: Micro-Kernel vs Monolithic Kernel Architectures

Your OS will follow a micro-kernel design. Understanding the trade-offs is essential:

MONOLITHIC KERNEL ARCHITECTURE
================================================================================

    User Space
    +-----------------------------------------------------------------------+
    |  App 1  |  App 2  |  App 3  |  Shell  |  File Manager  |  Browser   |
    +-----------------------------------------------------------------------+
         |         |         |         |              |              |
         +----+----+---------+---------+--------------+--------------+
              |
              | System Calls (thin interface)
              |
              v
    +---------------------------------------------------------------------------+
    |                                                                           |
    |                         KERNEL (Ring 0)                                   |
    |                                                                           |
    |   +---------------+  +---------------+  +---------------+                 |
    |   | Process Mgmt  |  | Memory Mgmt   |  | File Systems  |                 |
    |   +-------+-------+  +-------+-------+  +-------+-------+                 |
    |           |                  |                  |                         |
    |   +-------+------------------+------------------+-------+                 |
    |   |                                                     |                 |
    |   |   +-------------+  +-------------+  +-------------+ |                 |
    |   |   | Disk Driver |  | Net Driver  |  | USB Driver  | |                 |
    |   |   +-------------+  +-------------+  +-------------+ |                 |
    |   |                                                     |                 |
    |   |   +-------------+  +-------------+  +-------------+ |                 |
    |   |   |    TCP/IP   |  |     VFS     |  |   ext4/NTFS | |                 |
    |   |   +-------------+  +-------------+  +-------------+ |                 |
    |   |                                                     |                 |
    |   +-----------------------------------------------------+                 |
    |                                                                           |
    +---------------------------------------------------------------------------+
         |
         | Direct Hardware Access
         v
    [HARDWARE]

    PROS:
    + Fast: All kernel code runs in same address space
    + Simple IPC: Direct function calls between components
    + Lower overhead: No context switches within kernel

    CONS:
    - Bug in ANY driver can crash entire system
    - Security: Compromise one part, compromise everything
    - Complexity: Millions of lines in kernel space
    - Testing: Hard to test individual components

    Examples: Linux, Windows NT kernel, macOS XNU (hybrid)


MICRO-KERNEL ARCHITECTURE (Your Design)
================================================================================

    User Space
    +-----------------------------------------------------------------------+
    |                                                                       |
    |  +---------+  +---------+  +---------+  +---------+  +---------+     |
    |  |  App 1  |  |  App 2  |  |  Shell  |  |  File   |  |  Net    |     |
    |  |         |  |         |  |         |  | Server  |  | Server  |     |
    |  +---------+  +---------+  +---------+  +---------+  +---------+     |
    |       |            |            |            |            |          |
    |       +------------+------------+------------+------------+          |
    |                                 |                                    |
    |                                 | IPC Messages                       |
    |                                 |                                    |
    +-----------------------------------------------------------------------+
                                      |
                                      v
    +---------------------------------------------------------------------------+
    |                                                                           |
    |                    MICRO-KERNEL (Ring 0) - MINIMAL                        |
    |                                                                           |
    |   +-------------------+  +-------------------+  +-------------------+     |
    |   |  Task Scheduling  |  |    IPC (Message   |  |  Memory Mapping   |     |
    |   |  (Async Executor) |  |     Passing)      |  |  (Page Tables)    |     |
    |   +-------------------+  +-------------------+  +-------------------+     |
    |                                                                           |
    |   +-------------------+  +-------------------+                             |
    |   | Interrupt Routing |  | Hardware Drivers  | <- Only essential ones    |
    |   | (As Futures!)     |  | (Timer, PIC only) |                             |
    |   +-------------------+  +-------------------+                             |
    |                                                                           |
    +---------------------------------------------------------------------------+
         |
         v
    [HARDWARE]

    PROS:
    + Reliability: Driver crash only affects that service
    + Security: Minimal trusted code base
    + Flexibility: Swap components without reboot
    + Testing: Each server is a separate program

    CONS:
    - IPC overhead: Messages cross address spaces
    - Complexity: More moving parts
    - Performance: Context switches cost cycles

    Examples: MINIX 3, L4, seL4, QNX, GNU Hurd
    Rust Examples: Redox OS, Theseus OS


WHY RUST CHANGES THE EQUATION
================================================================================

Traditional C/C++ kernels need micro-kernel isolation because:
  - Memory bugs (use-after-free, buffer overflow) are common
  - A bug in a driver can corrupt kernel data structures
  - No compile-time enforcement of safety

With Rust:
  - Memory safety is guaranteed at compile time
  - Interior mutability is explicit and controlled
  - Ownership prevents data races
  - We can have monolithic PERFORMANCE with micro-kernel RELIABILITY

Your kernel combines the best of both:
  - Micro-kernel DESIGN (modularity, clear interfaces)
  - Monolithic IMPLEMENTATION (single address space, no IPC overhead)
  - Rust SAFETY (bugs in drivers don't corrupt kernel)

This is the "single-address-space OS" approach used by Theseus OS.

Part 3: The Boot Process - BIOS/UEFI to Your Kernel

Understanding the boot sequence is crucial for knowing what state the hardware is in when your kernel starts:

THE COMPLETE BOOT SEQUENCE
================================================================================

[POWER BUTTON PRESSED]
         |
         v
+------------------+
|     PHASE 1      |
|   HARDWARE INIT  |
|                  |
| - CPU reset      |
| - POST (Power On |
|   Self Test)     |
| - Hardware       |
|   discovery      |
+--------+---------+
         |
         v
+------------------+
|     PHASE 2      |
|   FIRMWARE       |
|   (BIOS/UEFI)    |
|                  |
| BIOS (Legacy):   |
| - Load MBR from  |
|   first 512B of  |
|   disk to 0x7C00 |
| - CPU in Real    |
|   Mode (16-bit)  |
|                  |
| UEFI (Modern):   |
| - Load EFI app   |
|   from ESP       |
| - CPU already in |
|   Long Mode!     |
+--------+---------+
         |
         v
+------------------+
|     PHASE 3      |
|   BOOTLOADER     |
|                  |
| Tasks:           |
| 1. Switch CPU    |
|    modes if BIOS |
| 2. Set up basic  |
|    page tables   |
| 3. Detect memory |
|    (E820 map)    |
| 4. Load kernel   |
|    from disk     |
| 5. Set up stack  |
| 6. Jump to       |
|    kernel entry  |
+--------+---------+
         |
         v
+------------------+
|     PHASE 4      |
|   YOUR KERNEL    |
|                  |
| State when you   |
| take control:    |
| - Long Mode      |
|   (64-bit)       |
| - Paging enabled |
|   (identity map) |
| - Interrupts OFF |
| - Stack set up   |
| - No heap yet!   |
+------------------+
         |
         | Your code starts here
         v
    [KERNEL INIT]


CPU MODE TRANSITIONS (BIOS Boot Path)
================================================================================

+-------------+          +------------------+          +----------------+
|  Real Mode  |  ------> | Protected Mode   |  ------> |   Long Mode    |
|   (16-bit)  |          |     (32-bit)     |          |    (64-bit)    |
+-------------+          +------------------+          +----------------+
      |                         |                            |
      | 1 MB address            | 4 GB address               | 256 TB address
      | space                   | space                      | space
      |                         |                            |
      | No protection           | MMU enabled                | Full 64-bit
      | (8086 compat)           | Rings 0-3                  | features
      |                         |                            |
      +-------------------------+----------------------------+
                                |
                    Steps to transition:

    Real -> Protected:
      1. Disable interrupts (CLI)
      2. Load GDT (Global Descriptor Table)
      3. Set PE bit in CR0
      4. Far jump to flush pipeline

    Protected -> Long:
      1. Disable paging (CR0.PG = 0)
      2. Enable PAE (CR4.PAE = 1)
      3. Load PML4 address into CR3
      4. Enable long mode (IA32_EFER.LME = 1)
      5. Enable paging (CR0.PG = 1)
      6. Far jump to 64-bit code segment


THE BOOTLOADER CRATE
================================================================================

We use the `bootloader` crate which handles all the complexity above.
When your kernel's _start function runs:

  struct BootInfo {
      memory_map: MemoryMap,        // Which memory regions are usable
      physical_memory_offset: u64,  // Where physical memory is mapped
      framebuffer: Option<...>,     // For graphics output
      // ... more fields
  }

  #[no_mangle]
  pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! {
      // You have:
      // - 64-bit CPU mode
      // - Paging enabled (identity + offset mapping)
      // - Boot info telling you about memory
      // - A working stack

      // You need to:
      // - Set up your own GDT (optional, bootloader provides one)
      // - Set up IDT for interrupts
      // - Initialize your heap allocator
      // - Enable interrupts
      // - Start your async executor
  }

Part 4: Memory Management - Paging, Virtual Memory, and Page Tables

Memory management is the heart of any OS. You must understand paging to implement your allocator:

VIRTUAL MEMORY OVERVIEW
================================================================================

Without Virtual Memory:
+------------------------------------------------------------------+
|                      PHYSICAL RAM                                 |
|                                                                  |
|  Program A data  |  HOLE  |  Program B  |  Kernel  |  HOLE  |   |
|  0x1000-0x2000   |        |  0x3000-    |          |        |   |
|                  |        |    0x5000   |          |        |   |
+------------------------------------------------------------------+
  Problems:
    - Programs must know their physical location
    - Fragmentation wastes memory
    - No isolation (A can access B's memory)
    - Can't run more programs than fit in RAM


With Virtual Memory:
+------------------------------------------------------------------+
|               PROCESS A's VIEW                                    |
|                                                                  |
|  0x0000          |  Code  |  Heap  |  Stack  |                   |
|  NULL (unmapped) | 0x1000 | 0x8000 | 0xFFFF  |                   |
+------------------------------------------------------------------+
                         |          |         |
                         v          v         v
                    [ PAGE TABLE A ]
                         |          |         |
+------------------------------------------------------------------+
|               PHYSICAL RAM                                        |
|                                                                  |
|  Kernel  |  A's Code  |  B's Code  |  A's Heap  |  B's Stack  |  |
|  0x0000  |   0x5000   |   0x8000   |   0xC000   |   0x10000   |  |
+------------------------------------------------------------------+
                         ^          ^         ^
                         |          |         |
                    [ PAGE TABLE B ]
                         |          |         |
+------------------------------------------------------------------+
|               PROCESS B's VIEW                                    |
|                                                                  |
|  0x0000          |  Code  |  Heap  |  Stack  |                   |
|  NULL (unmapped) | 0x1000 | 0x8000 | 0xFFFF  |                   |
+------------------------------------------------------------------+

  Each process sees the SAME virtual addresses but they map to
  DIFFERENT physical addresses. The MMU (Memory Management Unit)
  translates addresses automatically on every memory access.


x86_64 PAGE TABLE STRUCTURE (4-Level Paging)
================================================================================

Virtual Address (48 bits used):
+------+--------+--------+--------+--------+---------------+
| Sign | PML4   |  PDPT  |   PD   |   PT   | Page Offset   |
| Ext. | Index  |  Index | Index  | Index  | (12 bits)     |
| 16b  | 9 bits | 9 bits | 9 bits | 9 bits | 4 KB pages    |
+------+--------+--------+--------+--------+---------------+
         |          |        |        |           |
         v          v        v        v           |
   +----------+----------+----------+----------+  |
   |   PML4   |   PDPT   |    PD    |    PT    |  |
   | (512 ent)| (512 ent)| (512 ent)| (512 ent)|  |
   |   Table  |   Table  |   Table  |   Table  |  |
   +----+-----+----+-----+----+-----+----+-----+  |
        |          |          |          |        |
        +----------+----------+----------+        |
                                    |             |
                               +----+-----+       |
                               |  Physical|<------+
                               |   Frame  | + offset = Physical Address
                               +----------+

Page Table Entry (64 bits):
+------------------------------------------------------------------+
| 63  | 62:52  | 51:12                    | 11:9 | 8:0              |
+------------------------------------------------------------------+
| NX  | Avail  | Physical Frame Number    | Avail| Flags            |
|     |        | (40 bits = 1 TB phys)    |      | P,R/W,U/S,...    |
+------------------------------------------------------------------+

Key Flags:
  P (Present):      Page is in physical memory
  R/W:              0 = Read-only, 1 = Read/Write
  U/S:              0 = Supervisor (kernel), 1 = User accessible
  PWT, PCD:         Cache control
  A (Accessed):     CPU sets this on access
  D (Dirty):        CPU sets this on write
  PS (Page Size):   For huge pages (2MB, 1GB)
  NX (No Execute):  Prevent code execution (security)


YOUR KERNEL'S MEMORY MAP
================================================================================

After boot, your kernel has this layout:

Virtual Address Space (48-bit, canonical form)
+------------------------------------------------------------------+
| 0xFFFF_8000_0000_0000 +------------------------------------+     |
|                       |  Higher-Half Kernel Mapping         |     |
|                       |                                    |     |
|                       |  Your kernel code and data live    |     |
|                       |  here in the "higher half"         |     |
|                       +------------------------------------+     |
|                       |  Physical Memory Map                |     |
|                       |  (bootloader maps all phys RAM at  |     |
|                       |   physical_memory_offset)          |     |
| 0xFFFF_FFFF_FFFF_FFFF +------------------------------------+     |
|                                                                  |
|                       [HUGE UNUSED HOLE]                         |
|                       (Non-canonical addresses fault)            |
|                                                                  |
| 0x0000_7FFF_FFFF_FFFF +------------------------------------+     |
|                       |  User Space                         |     |
|                       |  (future user programs)             |     |
|                       |  Stack grows down from high        |     |
|                       |  Heap grows up from low            |     |
| 0x0000_0000_0000_0000 +------------------------------------+     |
+------------------------------------------------------------------+

Physical Memory Map (from bootloader):
+------------------------------------------------------------------+
| Address Range          | Usage                                    |
+------------------------------------------------------------------+
| 0x0000_0000 - 0x0000_1000 | Reserved (Real Mode IVT, BDA)         |
| 0x0000_1000 - 0x0007_FFFF | Usable (conventional memory)          |
| 0x0008_0000 - 0x0009_FFFF | Partially usable (EBDA may be here)   |
| 0x000A_0000 - 0x000B_FFFF | VGA memory (NOT RAM)                  |
| 0x000C_0000 - 0x000F_FFFF | BIOS ROM, option ROMs                 |
| 0x0010_0000 - ??????????? | Usable (this is your RAM!)            |
| ??? (varies)              | ACPI tables, reserved regions         |
+------------------------------------------------------------------+

Book Reference: “Computer Systems: A Programmer’s Perspective” Chapter 9 provides excellent coverage of virtual memory.

Part 5: Interrupt Handling - IDT, ISRs, and Hardware Exceptions

Interrupts are how the hardware communicates with your kernel. They’re also the foundation of your async executor:

INTERRUPT OVERVIEW
================================================================================

What is an interrupt?
  - A signal that causes the CPU to stop what it's doing
  - Save current state (registers, flags, instruction pointer)
  - Jump to a predefined handler function
  - Restore state and continue (IRET instruction)

Types of Interrupts:
  +------------------------------------------------------------------+
  |  EXCEPTIONS (Synchronous)                                         |
  |  - Caused by CPU during instruction execution                     |
  |  - Examples: Division by zero, page fault, invalid opcode         |
  |  - Happen at a specific point in the code                         |
  +------------------------------------------------------------------+
  |  HARDWARE INTERRUPTS (Asynchronous)                               |
  |  - Caused by external devices                                     |
  |  - Examples: Keyboard press, timer tick, disk ready               |
  |  - Can happen at ANY time                                         |
  +------------------------------------------------------------------+
  |  SOFTWARE INTERRUPTS                                              |
  |  - Caused by INT instruction                                      |
  |  - Used for system calls (INT 0x80 on Linux x86)                 |
  |  - On x86_64, SYSCALL/SYSRET is preferred                        |
  +------------------------------------------------------------------+


INTERRUPT DESCRIPTOR TABLE (IDT)
================================================================================

The IDT is an array of 256 entries, one for each interrupt vector:

+-------+----------------------------------------------------------+
| Index | Description                                              |
+-------+----------------------------------------------------------+
|   0   | #DE - Divide Error                                       |
|   1   | #DB - Debug Exception                                    |
|   2   | NMI - Non-Maskable Interrupt                             |
|   3   | #BP - Breakpoint (INT 3)                                 |
|   4   | #OF - Overflow (INTO instruction)                        |
|   5   | #BR - Bound Range Exceeded                               |
|   6   | #UD - Invalid Opcode                                     |
|   7   | #NM - Device Not Available (FPU)                         |
|   8   | #DF - Double Fault (exception during exception)          |
|   9   | (Reserved - was Coprocessor Segment Overrun)             |
|  10   | #TS - Invalid TSS                                        |
|  11   | #NP - Segment Not Present                                |
|  12   | #SS - Stack-Segment Fault                                |
|  13   | #GP - General Protection Fault                           |
|  14   | #PF - Page Fault (THE IMPORTANT ONE!)                    |
|  15   | (Reserved)                                               |
|  16   | #MF - x87 FPU Error                                      |
|  17   | #AC - Alignment Check                                    |
|  18   | #MC - Machine Check                                      |
|  19   | #XF - SIMD Floating-Point Exception                      |
| 20-31 | (Reserved by Intel)                                      |
| 32-47 | IRQs 0-15 (Hardware interrupts, remapped)                |
|       |   32: Timer (PIT/APIC)                                   |
|       |   33: Keyboard (PS/2)                                    |
|       |   34-47: Other devices                                   |
| 48-255| Available for software use                               |
+-------+----------------------------------------------------------+


IDT ENTRY STRUCTURE (x86_64)
================================================================================

Each IDT entry is 16 bytes (128 bits):

+------------------------------------------------------------------+
| Offset 127:96 (high 32 bits of handler address)                   | +12
+------------------------------------------------------------------+
| Reserved (must be zero)                                           | +8
+------------------------------------------------------------------+
| Offset 31:16  |  P | DPL | 0 | Type | IST | Reserved             | +4
|   (bits 16-31 |  1 | 2b  | 0 | 4b   | 3b  |                       |
|    of handler)|    |     |   |      |     |                       |
+------------------------------------------------------------------+
| Segment Selector (16 bits) | Offset 15:0 (low 16 bits of handler)| +0
+------------------------------------------------------------------+

Fields:
  - Offset: 64-bit address of the interrupt handler function
  - Segment Selector: Code segment (usually kernel code segment)
  - Type:
      0xE = 64-bit Interrupt Gate (clear IF on entry)
      0xF = 64-bit Trap Gate (don't clear IF)
  - DPL: Descriptor Privilege Level (0 = kernel, 3 = user)
  - P: Present bit (must be 1)
  - IST: Interrupt Stack Table index (0-7, 0 = don't switch stack)


INTERRUPT HANDLING FLOW
================================================================================

1. Hardware raises interrupt (e.g., keyboard)
         |
         v
2. CPU receives interrupt signal
         |
         v
3. CPU looks up handler in IDT[vector_number]
         |
         v
4. CPU pushes state to stack:
   +------------------------------------------------------------------+
   | Stack after interrupt (before handler runs):                      |
   |                                                                  |
   |  +------------------+                                            |
   |  |       SS         | (Stack Segment, if privilege change)       |
   |  +------------------+                                            |
   |  |      RSP         | (Stack Pointer, if privilege change)       |
   |  +------------------+                                            |
   |  |     RFLAGS       | (CPU flags)                                |
   |  +------------------+                                            |
   |  |       CS         | (Code Segment)                             |
   |  +------------------+                                            |
   |  |      RIP         | (Instruction Pointer - where to return)    |
   |  +------------------+                                            |
   |  |   Error Code     | (For some exceptions only)                 |
   |  +------------------+  <-- RSP points here                       |
   +------------------------------------------------------------------+
         |
         v
5. CPU jumps to handler address from IDT
         |
         v
6. Handler runs (YOUR CODE):
   - Save additional registers (if needed)
   - Handle the interrupt
   - Acknowledge interrupt controller (PIC/APIC)
   - THIS IS WHERE YOU WAKE UP FUTURES!
         |
         v
7. Handler executes IRETQ
         |
         v
8. CPU pops saved state, resumes interrupted code


INTERRUPT CONTROLLER (8259 PIC)
================================================================================

Hardware interrupts go through the Programmable Interrupt Controller:

                   +----------+      +----------+
     IRQ 0-7  ---> |  Master  | <--- |  Slave   | <--- IRQ 8-15
     (Timer,       |   PIC    |      |   PIC    |      (RTC,
      Keyboard)    +----+-----+      +----+-----+       Mouse)
                        |                 |
                        |   Cascade       |
                        +-----------------+
                                |
                                v
                        +-------+-------+
                        |      CPU      |
                        |  INT pin      |
                        +---------------+

IRQ to Vector Mapping (after remapping):
  IRQ 0 (Timer)    -> Vector 32
  IRQ 1 (Keyboard) -> Vector 33
  IRQ 2 (Cascade)  -> (internal)
  ...
  IRQ 8 (RTC)      -> Vector 40
  ...

Why remap?
  - Default: IRQ 0-7 -> Vectors 0-7 (conflicts with CPU exceptions!)
  - Remapped: IRQ 0-7 -> Vectors 32-39, IRQ 8-15 -> Vectors 40-47

Book Reference: “Operating Systems: Three Easy Pieces” has excellent coverage of interrupt handling.

Part 6: Process Scheduling - Cooperative vs Preemptive

Your kernel uses cooperative multitasking via async/await, but understanding both models is important:

PREEMPTIVE MULTITASKING (Traditional)
================================================================================

    Task A running
         |
         | Timer interrupt fires!
         v
    +-----------------+
    | Context Switch  |  CPU forces switch regardless of
    | (involuntary)   |  what Task A was doing
    +-----------------+
         |
         v
    Task B running
         |
         | Timer interrupt fires!
         v
    +-----------------+
    | Context Switch  |
    +-----------------+
         |
         v
    Task A running
         ...

    Properties:
    + Fairness: No task can hog the CPU
    + Responsiveness: Tasks can't block the system
    - Complexity: Must save ALL CPU state
    - Overhead: Context switch every time slice
    - Synchronization: Need locks for shared data


COOPERATIVE MULTITASKING (Your Approach)
================================================================================

    Task A running
         |
         | Task A calls: keyboard_char().await
         v
    +-----------------+
    | Task A yields   |  "I'm waiting for keyboard"
    | (voluntary)     |  Task A returns Poll::Pending
    +-----------------+
         |
         v
    Executor polls Task B
         |
         | Task B calls: timer_delay(100ms).await
         v
    +-----------------+
    | Task B yields   |  "I'm waiting for timer"
    +-----------------+
         |
         v
    Executor polls Task C
         |
         | Task C has work to do (CPU-bound)
         | Task C runs until it awaits something
         |
    ... time passes ...
         |
    Keyboard interrupt fires!
         |
         v
    +-----------------+
    | Waker::wake()   |  Keyboard future marks Task A ready
    +-----------------+
         |
         v
    Executor polls Task A
         |
         | Task A receives the key
         | Task A continues running
         ...

    Properties:
    + Simple: No involuntary saves
    + Efficient: Only switch when waiting for I/O
    + No locks: Single-threaded, no data races
    + Natural: async/await is idiomatic Rust
    - Trust: Tasks must yield (CPU-bound tasks block everything)
    - Determinism: Scheduling depends on I/O patterns


WHY ASYNC IN THE KERNEL?
================================================================================

Most kernel work is I/O-bound:
  - Wait for disk to read sector
  - Wait for network packet to arrive
  - Wait for keyboard press
  - Wait for timer to expire

async/await is PERFECT for this:

  // Traditional blocking approach:
  fn read_sector(disk: &Disk, sector: u64) -> [u8; 512] {
      disk.start_read(sector);
      while !disk.is_ready() {
          // SPIN! Wastes CPU cycles!
      }
      disk.get_data()
  }

  // Async approach:
  async fn read_sector(disk: &Disk, sector: u64) -> [u8; 512] {
      disk.start_read(sector);
      DiskReadFuture { disk, sector }.await  // Yields to other tasks!
  }

The interrupt handler just wakes the future:

  extern "x86-interrupt" fn disk_handler(frame: InterruptStackFrame) {
      DISK_WAKER.wake();  // "Hey, disk data is ready!"
      // Executor will poll the DiskReadFuture next
  }

Part 7: System Calls - User Space to Kernel Space Transitions

Although your initial kernel won’t have true user-space processes, understanding system calls prepares you for the future:

SYSTEM CALL MECHANISM
================================================================================

User Space                     Kernel Space (Ring 0)
+------------------------+     +------------------------+
|                        |     |                        |
|  let fd = open(        |     |                        |
|    "/etc/passwd",      |     |                        |
|    O_RDONLY            |     |                        |
|  );                    |     |                        |
|         |              |     |                        |
+---------|--------------+     +------------------------+
          |
          | 1. User code prepares arguments
          |    (in registers: rdi, rsi, rdx, ...)
          |
          | 2. User code puts syscall number in rax
          |    (open = 2 on Linux)
          |
          | 3. SYSCALL instruction
          |
          v
+-------------------------------------------------------------+
|                    CPU SYSCALL MECHANISM                     |
|                                                             |
| - Loads kernel CS and SS from MSRs                          |
| - Saves user RIP in RCX                                     |
| - Saves user RFLAGS in R11                                  |
| - Clears RFLAGS.IF (disable interrupts)                     |
| - Jumps to kernel entry point (from LSTAR MSR)              |
+-------------------------------------------------------------+
          |
          v
+------------------------+     +------------------------+
|                        |     |                        |
|                        |     |  fn syscall_entry() {  |
|                        |     |    // Get syscall #    |
|                        |     |    match rax {         |
|                        |     |      2 => sys_open(),  |
|                        |     |      1 => sys_write(), |
|                        |     |      ...               |
|                        |     |    }                   |
|                        |     |    sysretq();          |
|                        |     |  }                     |
|                        |     |                        |
+------------------------+     +------------------------+


YOUR PROCEDURAL MACRO APPROACH
================================================================================

Instead of manually writing all that glue code, you'll use a macro:

  #[syscall(number = 42)]
  fn sys_read(fd: u64, buf: *mut u8, len: usize) -> isize {
      // Your implementation
      let file = get_file(fd)?;
      file.read(buf, len)
  }

The macro generates:

  // Entry in syscall table
  static SYSCALL_TABLE: [SyscallFn; 256] = {
      ...
      [42] = sys_read_wrapper,
      ...
  };

  // Wrapper that extracts arguments from registers
  fn sys_read_wrapper(frame: &mut SyscallFrame) -> i64 {
      let fd = frame.rdi;
      let buf = frame.rsi as *mut u8;
      let len = frame.rdx as usize;
      sys_read(fd, buf, len) as i64
  }

This is similar to how Linux's SYSCALL_DEFINE macros work!


SYSTEM CALL PATH DIAGRAM
================================================================================

+------------------------------------------------------------------+
|                        USER SPACE                                 |
|                                                                  |
|   User Program                                                   |
|        |                                                         |
|        | fn read(fd, buf, len)                                   |
|        |                                                         |
|        v                                                         |
|   libc wrapper (or your user library)                            |
|        |                                                         |
|        | mov rax, SYS_read  ; syscall number                     |
|        | mov rdi, fd                                             |
|        | mov rsi, buf                                            |
|        | mov rdx, len                                            |
|        | syscall                                                 |
|        |                                                         |
+--------|---------------------------------------------------------+
         |
         | CPU privilege transition (Ring 3 -> Ring 0)
         | RIP saved in RCX, RFLAGS saved in R11
         |
         v
+------------------------------------------------------------------+
|                       KERNEL SPACE                                |
|                                                                  |
|   syscall_entry:                                                 |
|        |                                                         |
|        | swapgs  ; switch to kernel GS base                      |
|        | mov [rsp_scratch], rsp  ; save user stack               |
|        | mov rsp, [kernel_stack] ; switch to kernel stack        |
|        |                                                         |
|        | push all registers  ; save user context                 |
|        |                                                         |
|        | call dispatch_syscall(rax, rdi, rsi, rdx, ...)          |
|        |         |                                               |
|        |         v                                               |
|        |   match syscall_number {                                |
|        |       SYS_read => sys_read(fd, buf, len),               |
|        |       SYS_write => sys_write(fd, buf, len),             |
|        |       ...                                               |
|        |   }                                                     |
|        |         |                                               |
|        |         | (return value in rax)                         |
|        |         v                                               |
|        | pop all registers                                       |
|        | sysretq  ; return to user space                         |
|        |                                                         |
+--------|---------------------------------------------------------+
         |
         | CPU privilege transition (Ring 0 -> Ring 3)
         | RIP restored from RCX, RFLAGS restored from R11
         |
         v
+------------------------------------------------------------------+
|                        USER SPACE                                 |
|                                                                  |
|        | (continues after syscall instruction)                   |
|        | result in rax                                           |
|        v                                                         |
|   User Program (check result, continue)                          |
|                                                                  |
+------------------------------------------------------------------+

Part 8: Async in the Kernel - Treating Interrupts as Futures

This is the most innovative part of your OS: using async/await at the kernel level:

THE KEY INSIGHT: INTERRUPTS ARE JUST EVENTS
================================================================================

Traditional kernel:
  Interrupt fires -> Handler runs -> Handler does all the work

Problem:
  - Handler must be fast (interrupts disabled!)
  - Complex work must be deferred (bottom halves, work queues)
  - Hard to compose, hard to reason about

Async kernel:
  Interrupt fires -> Handler wakes a Future -> Executor polls Future

Benefit:
  - Handler is minimal (just wake())
  - All logic is in normal async code
  - Easy to compose with async/await
  - Natural flow control


KEYBOARD EXAMPLE
================================================================================

Traditional approach:

  static mut KEY_BUFFER: [u8; 16] = [0; 16];
  static mut KEY_INDEX: usize = 0;

  extern "x86-interrupt" fn keyboard_handler(frame: InterruptStackFrame) {
      let scancode = inb(0x60);
      let key = decode_scancode(scancode);

      unsafe {
          KEY_BUFFER[KEY_INDEX] = key;
          KEY_INDEX += 1;
          // Wake up waiting process somehow... condition variable?
      }

      end_of_interrupt();
  }

  fn read_key() -> u8 {
      loop {
          if unsafe { KEY_INDEX > 0 } {
              // Get key from buffer...
          }
          // Spin or block somehow
      }
  }


Async approach:

  static KEYBOARD_WAKER: AtomicWaker = AtomicWaker::new();
  static KEY_READY: AtomicBool = AtomicBool::new(false);
  static KEY_VALUE: AtomicU8 = AtomicU8::new(0);

  extern "x86-interrupt" fn keyboard_handler(frame: InterruptStackFrame) {
      let scancode = inb(0x60);
      let key = decode_scancode(scancode);

      KEY_VALUE.store(key, Ordering::SeqCst);
      KEY_READY.store(true, Ordering::SeqCst);
      KEYBOARD_WAKER.wake();  // Just wake the future!

      end_of_interrupt();
  }

  pub struct KeyboardFuture;

  impl Future for KeyboardFuture {
      type Output = u8;

      fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<u8> {
          KEYBOARD_WAKER.register(cx.waker());

          if KEY_READY.swap(false, Ordering::SeqCst) {
              Poll::Ready(KEY_VALUE.load(Ordering::SeqCst))
          } else {
              Poll::Pending
          }
      }
  }

  // Now in your shell:
  async fn shell() {
      loop {
          let key = KeyboardFuture.await;  // Yields until key pressed!
          process_key(key);
      }
  }


ASYNC EXECUTOR IN KERNEL CONTEXT
================================================================================

Your executor runs the kernel's main loop:

  fn kernel_main() -> ! {
      // Initialize hardware...

      // Create executor
      let mut executor = Executor::new();

      // Spawn kernel tasks
      executor.spawn(shell());           // Interactive shell
      executor.spawn(timer_service());   // Periodic tasks
      executor.spawn(network_stack());   // Handle network

      // Run forever
      executor.run();  // Never returns!
  }

  impl Executor {
      fn run(&mut self) -> ! {
          loop {
              // Poll all ready tasks
              while let Some(task) = self.ready_queue.pop() {
                  let waker = self.create_waker(task.id);
                  let mut cx = Context::from_waker(&waker);

                  match task.future.poll(&mut cx) {
                      Poll::Ready(_) => {
                          // Task completed
                          self.tasks.remove(task.id);
                      }
                      Poll::Pending => {
                          // Task will be re-added by waker
                      }
                  }
              }

              // No tasks ready - wait for interrupt
              // HLT instruction pauses CPU until interrupt
              unsafe { asm!("hlt"); }

              // Interrupt fired! Loop back and check ready_queue
          }
      }
  }


THE FLOW: INTERRUPT TO FUTURE TO POLL
================================================================================

1. Keyboard interrupt fires
         |
         v
2. keyboard_handler() runs
   - Reads scancode
   - Stores key value
   - Calls KEYBOARD_WAKER.wake()
         |
         v
3. Waker adds KeyboardFuture's task to ready_queue
         |
         v
4. Handler returns (IRET)
         |
         v
5. Executor was in HLT, now wakes up
         |
         v
6. Executor sees task in ready_queue
         |
         v
7. Executor polls KeyboardFuture
         |
         v
8. poll() sees KEY_READY is true
         |
         v
9. poll() returns Poll::Ready(key_value)
         |
         v
10. shell() async fn continues with the key!


+------------------------------------------------------------------+
|                                                                  |
|   Timer   Keyboard   Disk   Network   ...                        |
|     |        |         |       |                                 |
|     v        v         v       v                                 |
|   +----+  +----+    +----+  +----+                               |
|   |IRQ0|  |IRQ1|    |IRQ14| |IRQ11|   Hardware Interrupts        |
|   +----+  +----+    +----+  +----+                               |
|     |        |         |       |                                 |
|     v        v         v       v                                 |
|   +------------------------------------------+                   |
|   |        Interrupt Handlers                 |                   |
|   |   (Minimal: read data, call wake())       |                   |
|   +------------------------------------------+                   |
|                         |                                        |
|                         v                                        |
|   +------------------------------------------+                   |
|   |            Waker Registry                 |                   |
|   |   Timer -> TimerWaker                     |                   |
|   |   Keyboard -> KeyboardWaker               |                   |
|   |   Disk -> DiskWaker                       |                   |
|   +------------------------------------------+                   |
|                         |                                        |
|                         | wake() called                          |
|                         v                                        |
|   +------------------------------------------+                   |
|   |           Ready Queue                     |                   |
|   |   [ Task 3: shell (keyboard) ]            |                   |
|   |   [ Task 1: timer_service ]               |                   |
|   +------------------------------------------+                   |
|                         |                                        |
|                         v                                        |
|   +------------------------------------------+                   |
|   |              Executor                     |                   |
|   |   while let task = ready_queue.pop() {   |                   |
|   |       task.poll(cx);                      |                   |
|   |   }                                       |                   |
|   |   hlt(); // Sleep until next interrupt   |                   |
|   +------------------------------------------+                   |
|                         |                                        |
|                         v                                        |
|   +------------------------------------------+                   |
|   |           Async Tasks                     |                   |
|   |   - shell(): accepts user input           |                   |
|   |   - timer_service(): periodic tasks       |                   |
|   |   - file_server(): handles file I/O       |                   |
|   +------------------------------------------+                   |
|                                                                  |
+------------------------------------------------------------------+

Part 9: Global Allocator Integration

Your kernel needs heap allocation for dynamic data structures. You’ll integrate the arena allocator from Project 3:

THE GLOBAL ALLOCATOR IN no_std
================================================================================

In std, you get allocation for free:
  let v = vec![1, 2, 3];  // Just works!

In no_std, there is no allocator by default:
  let v = vec![1, 2, 3];  // Error: no global allocator

You must provide one:

  #[global_allocator]
  static ALLOCATOR: MyKernelAllocator = MyKernelAllocator::new();


MEMORY REGIONS IN YOUR KERNEL
================================================================================

Physical Memory (from BootInfo):
+------------------------------------------------------------------+
|  0x0000 - 0x1000:  Reserved (Real Mode stuff)                    |
|  0x1000 - 0x9FFFF: Usable (but bootloader may use some)          |
|  0xA0000 - 0xFFFFF: VGA + ROM (not RAM)                          |
|  0x100000 - ???:   Your Kernel (code, data, bss)                 |
|  ??? - end:        FREE (this is your heap!)                     |
+------------------------------------------------------------------+

Virtual Memory Layout:
+------------------------------------------------------------------+
| 0xFFFF_8000_0000_0000 + phys_offset: |  Physical memory map     |
|                                       |  (all RAM accessible)    |
|                                       +--------------------------+
| 0xFFFF_8000_0000_0000 + kernel_addr: |  Kernel code & data      |
|                                       +--------------------------+
| 0xFFFF_8000_XXXX_XXXX:               |  HEAP_START              |
|                                       |  (Your allocator's pool) |
|                                       |  ...                     |
|                                       |  HEAP_END                |
+------------------------------------------------------------------+


ALLOCATOR INTEGRATION STEPS
================================================================================

Step 1: Find usable memory from BootInfo

  fn find_heap_region(boot_info: &BootInfo) -> (usize, usize) {
      let usable_regions = boot_info.memory_map
          .iter()
          .filter(|r| r.region_type == MemoryRegionType::Usable);

      // Find largest region not used by kernel
      let kernel_end = /* ... */;

      for region in usable_regions {
          if region.range.start_addr() >= kernel_end {
              return (region.range.start_addr(), region.range.end_addr());
          }
      }
      panic!("No usable memory for heap!");
  }


Step 2: Initialize your allocator

  // Your allocator from Project 3, adapted for kernel
  pub struct KernelAllocator {
      inner: spin::Mutex<BumpAllocator>,
  }

  impl KernelAllocator {
      pub const fn empty() -> Self {
          Self { inner: spin::Mutex::new(BumpAllocator::empty()) }
      }

      pub unsafe fn init(&self, heap_start: usize, heap_size: usize) {
          self.inner.lock().init(heap_start, heap_size);
      }
  }

  #[global_allocator]
  static ALLOCATOR: KernelAllocator = KernelAllocator::empty();

  pub fn init_heap(boot_info: &BootInfo) {
      let (start, end) = find_heap_region(boot_info);
      unsafe {
          ALLOCATOR.init(start, end - start);
      }
  }


Step 3: Implement GlobalAlloc trait

  unsafe impl GlobalAlloc for KernelAllocator {
      unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
          self.inner.lock().alloc(layout)
      }

      unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
          // Bump allocator: no-op! Memory freed when kernel reboots :)
          // For production: use a proper allocator (linked list, buddy, etc.)
      }
  }


Step 4: Enable the alloc crate

  // In your kernel's lib.rs or main.rs
  #![feature(alloc_error_handler)]

  extern crate alloc;

  use alloc::{boxed::Box, vec::Vec, string::String};

  #[alloc_error_handler]
  fn alloc_error(layout: Layout) -> ! {
      panic!("Allocation failed: {:?}", layout);
  }


KERNEL HEAP LAYOUT DIAGRAM
================================================================================

After initialization:

+------------------------------------------------------------------+
|                                                                  |
|  Virtual Address Space (Kernel's view)                           |
|                                                                  |
|  HEAP_START -----------------------------------------------+     |
|  |                                                         |     |
|  |   +---------------------------------------------------+ |     |
|  |   |              BUMP ALLOCATOR POOL                  | |     |
|  |   |                                                   | |     |
|  |   |  [Allocated][Allocated][Free........................]     |
|  |   |                        ^                          | |     |
|  |   |                        |                          | |     |
|  |   |                    bump_ptr                       | |     |
|  |   |                                                   | |     |
|  |   +---------------------------------------------------+ |     |
|  |                                                         |     |
|  HEAP_END -------------------------------------------------+     |
|                                                                  |
|  STACK (grows down) <---------------------------------+          |
|                                                                  |
+------------------------------------------------------------------+

Part 10: Type-Safe Hardware Access with Const Generics

Using const generics from Project 5, you can create type-safe abstractions for memory-mapped hardware:

THE PROBLEM: MAGIC NUMBERS EVERYWHERE
================================================================================

Traditional kernel code:

  // Writing to VGA buffer
  let ptr = 0xB8000 as *mut u16;
  unsafe { *ptr.add(offset) = character; }

  // Reading keyboard scancode
  let code = inb(0x60);

  // Writing to PIC
  outb(0x20, 0x20);

Problems:
  - Magic numbers (0xB8000, 0x60, 0x20)
  - No type safety (easy to confuse addresses)
  - No compile-time verification
  - No documentation in the code


THE SOLUTION: CONST GENERIC MEMORY REGIONS
================================================================================

Define memory regions as types:

  // Memory region with compile-time address and size
  pub struct MemoryRegion<const ADDR: usize, const SIZE: usize>;

  impl<const ADDR: usize, const SIZE: usize> MemoryRegion<ADDR, SIZE> {
      pub const fn base(&self) -> usize { ADDR }
      pub const fn size(&self) -> usize { SIZE }

      pub fn read<T>(&self, offset: usize) -> T
      where T: Copy
      {
          assert!(offset + core::mem::size_of::<T>() <= SIZE);
          unsafe {
              core::ptr::read_volatile((ADDR + offset) as *const T)
          }
      }

      pub fn write<T>(&self, offset: usize, value: T)
      where T: Copy
      {
          assert!(offset + core::mem::size_of::<T>() <= SIZE);
          unsafe {
              core::ptr::write_volatile((ADDR + offset) as *mut T, value);
          }
      }
  }


TYPE-SAFE VGA BUFFER
================================================================================

  // VGA buffer: 80x25 characters, 2 bytes each
  pub type VgaBuffer = MemoryRegion<0xB8000, { 80 * 25 * 2 }>;

  // Now you can't confuse it with other regions!
  fn write_to_vga(vga: &VgaBuffer, row: usize, col: usize, ch: u8) {
      let offset = (row * 80 + col) * 2;
      vga.write(offset, ch);            // Character
      vga.write(offset + 1, 0x0F_u8);   // Attribute
  }

  // This is a compile error:
  // fn mistake(vga: &VgaBuffer) {
  //     keyboard::read_scancode(vga);  // Wrong type!
  // }


TYPE-SAFE IO PORTS
================================================================================

  pub struct Port<const PORT: u16>;

  impl<const PORT: u16> Port<PORT> {
      pub fn read_u8(&self) -> u8 {
          unsafe {
              let value: u8;
              asm!("in al, dx", out("al") value, in("dx") PORT);
              value
          }
      }

      pub fn write_u8(&self, value: u8) {
          unsafe {
              asm!("out dx, al", in("dx") PORT, in("al") value);
          }
      }
  }

  // Define keyboard port
  pub type KeyboardDataPort = Port<0x60>;
  pub type KeyboardStatusPort = Port<0x64>;

  // Usage:
  const KEYBOARD_DATA: KeyboardDataPort = Port;

  fn read_scancode() -> u8 {
      KEYBOARD_DATA.read_u8()
  }


MEMORY MAP VERIFICATION AT COMPILE TIME
================================================================================

Use const generics to ensure regions don't overlap:

  pub struct MemoryMap<const REGIONS: usize> {
      regions: [(usize, usize); REGIONS],
  }

  impl<const REGIONS: usize> MemoryMap<REGIONS> {
      pub const fn new(regions: [(usize, usize); REGIONS]) -> Self {
          // Verify no overlaps at compile time
          let mut i = 0;
          while i < REGIONS {
              let mut j = i + 1;
              while j < REGIONS {
                  let (start_i, end_i) = regions[i];
                  let (start_j, end_j) = regions[j];

                  if start_i < end_j && start_j < end_i {
                      panic!("Memory regions overlap!");
                  }
                  j += 1;
              }
              i += 1;
          }
          Self { regions }
      }
  }

  // This fails at compile time if regions overlap!
  const MEMORY_MAP: MemoryMap<3> = MemoryMap::new([
      (0x00000, 0x07FFF),   // Conventional memory
      (0xB8000, 0xB8FA0),   // VGA buffer
      (0x100000, 0x1FFFFF), // Kernel + heap
  ]);

Part 11: Building a Simple Shell

The shell is your primary interface to the running OS:

SHELL ARCHITECTURE
================================================================================

                    +----------------------------------+
                    |           User Input             |
                    |      (Keyboard Interrupt)        |
                    +----------------+-----------------+
                                     |
                                     v
                    +----------------------------------+
                    |        Keyboard Future           |
                    |   Yields until key pressed       |
                    +----------------+-----------------+
                                     |
                                     v
                    +----------------------------------+
                    |         Line Buffer              |
                    |   Accumulate chars until Enter   |
                    +----------------+-----------------+
                                     |
                                     v
                    +----------------------------------+
                    |          Line Parser             |
                    |   Split into command + args      |
                    +----------------+-----------------+
                                     |
                                     v
                    +----------------------------------+
                    |        Command Dispatch          |
                    |   Match command name, call fn    |
                    +----------------+-----------------+
                                     |
                    +----------------+-----------------+
                    |                |                 |
                    v                v                 v
             +-----------+    +-----------+    +-----------+
             |   help    |    |   clear   |    |  echo     |
             +-----------+    +-----------+    +-----------+


SHELL IMPLEMENTATION
================================================================================

  pub struct Shell {
      buffer: String,
      history: Vec<String>,
  }

  impl Shell {
      pub async fn run(&mut self) {
          self.print_prompt();

          loop {
              let key = KeyboardFuture.await;

              match key {
                  b'\n' => {
                      self.print_newline();
                      self.execute_command().await;
                      self.buffer.clear();
                      self.print_prompt();
                  }
                  0x08 => {  // Backspace
                      if !self.buffer.is_empty() {
                          self.buffer.pop();
                          self.print_backspace();
                      }
                  }
                  ch if ch.is_ascii_graphic() || ch == b' ' => {
                      self.buffer.push(ch as char);
                      self.print_char(ch);
                  }
                  _ => {}  // Ignore other keys
              }
          }
      }

      async fn execute_command(&mut self) {
          let parts: Vec<&str> = self.buffer.split_whitespace().collect();

          if parts.is_empty() {
              return;
          }

          match parts[0] {
              "help" => self.cmd_help(),
              "clear" => self.cmd_clear(),
              "echo" => self.cmd_echo(&parts[1..]),
              "mem" => self.cmd_mem(),
              "tasks" => self.cmd_tasks(),
              "reboot" => self.cmd_reboot(),
              _ => self.print_unknown_command(parts[0]),
          }
      }
  }


SHELL COMMANDS
================================================================================

  fn cmd_help(&self) {
      println!("Available commands:");
      println!("  help   - Show this help");
      println!("  clear  - Clear the screen");
      println!("  echo   - Print arguments");
      println!("  mem    - Show memory info");
      println!("  tasks  - Show running tasks");
      println!("  reboot - Reboot the system");
  }

  fn cmd_mem(&self) {
      let allocator = &ALLOCATOR.inner.lock();
      println!("Heap: {}/{} bytes used",
               allocator.used(),
               allocator.capacity());
  }

  fn cmd_tasks(&self) {
      println!("Running tasks:");
      for (id, task) in EXECUTOR.tasks() {
          println!("  Task {}: {}", id, task.name);
      }
  }

  fn cmd_reboot(&self) {
      // Triple fault causes reboot
      unsafe {
          // Write bad value to keyboard controller
          outb(0x64, 0xFE);
      }
  }


SHELL OUTPUT EXAMPLE
================================================================================

+------------------------------------------------------------------+
|  RustOS v0.1.0                                                    |
|  Hardware initialized.                                            |
|  Memory: 128MB detected.                                          |
|  Async executor started with 3 tasks.                             |
|                                                                  |
|  RustOS> help                                                     |
|  Available commands:                                              |
|    help   - Show this help                                        |
|    clear  - Clear the screen                                      |
|    echo   - Print arguments                                       |
|    mem    - Show memory info                                      |
|    tasks  - Show running tasks                                    |
|    reboot - Reboot the system                                     |
|                                                                  |
|  RustOS> mem                                                      |
|  Heap: 4096/16777216 bytes used (0.02%)                           |
|                                                                  |
|  RustOS> tasks                                                    |
|  Running tasks:                                                   |
|    Task 0: shell                                                  |
|    Task 1: timer_service                                          |
|    Task 2: keyboard_echo                                          |
|                                                                  |
|  RustOS> echo Hello, async kernel!                                |
|  Hello, async kernel!                                             |
|                                                                  |
|  RustOS> _                                                        |
+------------------------------------------------------------------+

Part 12: Real-World Examples and Inspiration

Your kernel is inspired by several real-world projects:

REDOX OS
================================================================================
https://www.redox-os.org/

  - Full Unix-like OS written in Rust
  - Micro-kernel architecture
  - Runs real applications (browser, editor)
  - Active development since 2015

Key lessons:
  - Rust CAN build a complete OS
  - Micro-kernel design works well with Rust's module system
  - Safety doesn't mean slow


THESEUS OS
================================================================================
https://github.com/theseus-os/Theseus

  - Research OS from Rice/Yale
  - "Single-address-space" design
  - Exploits Rust's safety for fault isolation without MMU protection
  - Supports live evolution (swap components without reboot)

Key lessons:
  - Rust's type system can replace hardware protection
  - Single address space = fast IPC
  - Language-level isolation is viable


BLOG OS by Philipp Oppermann
================================================================================
https://os.phil-opp.com/

  - Excellent tutorial series
  - Covers x86_64 from boot to async/await
  - Uses similar async-in-kernel approach

Key lessons:
  - Step-by-step learning path
  - Detailed explanations of every concept
  - Active community for questions


EMBASSY
================================================================================
https://embassy.dev/

  - Async runtime for embedded systems
  - Uses similar interrupt-as-future pattern
  - Runs on microcontrollers (ARM Cortex-M)

Key lessons:
  - Async works on bare metal
  - Interrupt-driven futures are practical
  - Zero-cost abstractions matter

Real World Outcome

When complete, you will have a functioning micro-OS that boots in QEMU and provides an interactive shell. Here’s what the boot sequence and interaction looks like:

QEMU Boot Sequence
================================================================================

$ cargo run --release
   Compiling rustos v0.1.0
    Finished release [optimized] target(s) in 12.34s
     Running `bootimage runner target/x86_64-rustos/release/rustos`
Building bootimage for kernel...
Created bootimage at target/x86_64-rustos/release/bootimage-rustos.bin
Running: qemu-system-x86_64 -drive format=raw,file=bootimage-rustos.bin -serial stdio

================================================================================
                          QEMU DISPLAY
================================================================================

+------------------------------------------------------------------+
|                                                                  |
|  =====================================================           |
|  =          RustOS - Async Micro-Kernel v0.1.0       =           |
|  =       Built with Rust, Powered by Futures         =           |
|  =====================================================           |
|                                                                  |
|  [BOOT] Starting kernel initialization...                        |
|  [BOOT] CPU: x86_64 in Long Mode                                 |
|  [BOOT] Memory: 128 MB detected                                  |
|                                                                  |
|  [INIT] Setting up GDT...                       [OK]             |
|  [INIT] Setting up IDT...                       [OK]             |
|  [INIT] Remapping PIC (IRQ 0-15 -> 32-47)...    [OK]             |
|  [INIT] Initializing heap allocator...          [OK]             |
|         Heap: 16 MB at 0xFFFF_8001_0000_0000                     |
|                                                                  |
|  [ASYNC] Creating executor...                   [OK]             |
|  [ASYNC] Spawning kernel tasks:                                  |
|          - Task 0: shell                                         |
|          - Task 1: timer_service (1s tick)                       |
|          - Task 2: keyboard_handler                              |
|                                                                  |
|  [ASYNC] Enabling interrupts...                 [OK]             |
|  [ASYNC] Entering main executor loop...                          |
|                                                                  |
|  =====================================================           |
|                                                                  |
|  Welcome to RustOS!                                              |
|  Type 'help' for available commands.                             |
|                                                                  |
|  RustOS> _                                                        |
|                                                                  |
+------------------------------------------------------------------+


Serial Output (Terminal)
================================================================================

[SERIAL] RustOS boot log
[SERIAL] ================
[SERIAL] BIOS memory map:
[SERIAL]   0x00000000-0x0009FBFF: Usable (639 KB)
[SERIAL]   0x0009FC00-0x0009FFFF: Reserved (1 KB)
[SERIAL]   0x000F0000-0x000FFFFF: Reserved (64 KB)
[SERIAL]   0x00100000-0x07FDFFFF: Usable (126 MB)
[SERIAL]   0x07FE0000-0x07FFFFFF: Reserved (128 KB)
[SERIAL]
[SERIAL] Kernel loaded at: 0xFFFF800000100000
[SERIAL] Kernel end at:    0xFFFF800000180000
[SERIAL] Heap start at:    0xFFFF800001000000
[SERIAL] Heap size:        16777216 bytes (16 MB)
[SERIAL]
[SERIAL] IDT entries configured:
[SERIAL]   Vector 0 (DE): divide_error_handler
[SERIAL]   Vector 3 (BP): breakpoint_handler
[SERIAL]   Vector 6 (UD): invalid_opcode_handler
[SERIAL]   Vector 8 (DF): double_fault_handler
[SERIAL]   Vector 13 (GP): general_protection_handler
[SERIAL]   Vector 14 (PF): page_fault_handler
[SERIAL]   Vector 32 (Timer): timer_interrupt_handler
[SERIAL]   Vector 33 (Keyboard): keyboard_interrupt_handler
[SERIAL]
[SERIAL] Executor initialized with 3 tasks
[SERIAL] Entering async main loop...


Shell Interaction Examples
================================================================================

RustOS> help
Available commands:
  help    - Show this help message
  clear   - Clear the screen
  echo    - Echo arguments to screen
  mem     - Display memory usage
  tasks   - List active async tasks
  uptime  - Show system uptime
  panic   - Trigger a kernel panic (for testing)
  reboot  - Reboot the system

RustOS> mem
Memory Statistics:
==================
  Total RAM:      128 MB
  Kernel:         512 KB
  Heap Used:      4 KB / 16 MB (0.02%)
  Heap Free:      16380 KB

  Allocator: Bump (no deallocation)

RustOS> tasks
Active Async Tasks:
===================
  ID | Name              | State    | Polls
  ---+-------------------+----------+-------
   0 | shell             | Running  | 1542
   1 | timer_service     | Waiting  | 127
   2 | keyboard_handler  | Waiting  | 1541

  Total: 3 tasks, 1 running, 2 waiting

RustOS> uptime
System uptime: 2 minutes, 34 seconds
Timer interrupts: 15401
Keyboard interrupts: 1541

RustOS> echo Hello from async kernel!
Hello from async kernel!

RustOS> _


Async Task Execution Trace (Debug Mode)
================================================================================

[EXEC] Poll cycle 1542
[EXEC]   Polling Task 0 (shell)...
[EXEC]     KeyboardFuture::poll() -> Pending
[EXEC]     Registered waker for IRQ 33
[EXEC]   Task 0: Pending
[EXEC]   Polling Task 1 (timer_service)...
[EXEC]     TimerFuture::poll() -> Pending (124ms remaining)
[EXEC]   Task 1: Pending
[EXEC]   No tasks ready, entering HLT...

[INT] IRQ 33 (Keyboard) - Scancode: 0x23 ('H' pressed)
[INT]   Waking keyboard waker...
[INT]   Task 2 added to ready queue
[INT]   EOI sent to PIC

[EXEC] Woke from HLT
[EXEC] Poll cycle 1543
[EXEC]   Polling Task 2 (keyboard_handler)...
[EXEC]     Scancode: 0x23 -> Key: 'H'
[EXEC]     Sent to shell input buffer
[EXEC]     KeyboardFuture::poll() -> Ready('H')
[EXEC]   Task 2: Completed one iteration, re-spawned
[EXEC]   Polling Task 0 (shell)...
[EXEC]     Received key: 'H'
[EXEC]     Buffer: "H"
[EXEC]     VGA: Printed 'H' at (7, 8)
[EXEC]     Waiting for next key...
[EXEC]   Task 0: Pending
[EXEC]   Entering HLT...

Complete Project Specification

Core Requirements

  1. Boot and Initialize
    • Boot via bootloader crate on QEMU x86_64
    • Set up GDT and IDT
    • Initialize VGA text buffer output
    • Initialize serial port for debug output
  2. Memory Management
    • Parse memory map from bootloader
    • Initialize custom heap allocator (from Project 3)
    • Enable the alloc crate for dynamic allocation
  3. Interrupt Handling
    • Configure PIC to remap IRQs
    • Handle timer interrupt (IRQ 0)
    • Handle keyboard interrupt (IRQ 1)
    • Handle CPU exceptions (divide by zero, page fault, etc.)
  4. Async Executor
    • Implement minimal async executor (from Project 8)
    • Convert interrupt handlers to wake futures
    • Support multiple concurrent async tasks
  5. Type-Safe Hardware
    • Use const generics for memory-mapped regions (from Project 5)
    • Type-safe port I/O wrappers
  6. Syscall Infrastructure
    • Define syscall macro for kernel entry points (from Project 10)
    • Implement basic syscalls (write, read, exit)
  7. Interactive Shell
    • Read keyboard input via async future
    • Parse and execute built-in commands
    • Display output on VGA buffer

Solution Architecture

Project Structure

rustos/
+-- .cargo/
|   +-- config.toml              # Cross-compilation settings
+-- src/
|   +-- main.rs                  # Entry point and kernel initialization
|   +-- lib.rs                   # Crate root
|   +-- gdt.rs                   # Global Descriptor Table
|   +-- idt.rs                   # Interrupt Descriptor Table
|   +-- interrupts.rs            # Interrupt handlers
|   +-- pic.rs                   # PIC controller
|   +-- memory/
|   |   +-- mod.rs               # Memory module
|   |   +-- allocator.rs         # Kernel heap allocator
|   |   +-- frame.rs             # Physical frame allocator
|   |   +-- paging.rs            # Page table management
|   +-- async_runtime/
|   |   +-- mod.rs               # Async runtime module
|   |   +-- executor.rs          # Task executor
|   |   +-- task.rs              # Task structure
|   |   +-- waker.rs             # Waker implementation
|   +-- drivers/
|   |   +-- mod.rs               # Drivers module
|   |   +-- vga.rs               # VGA text buffer
|   |   +-- keyboard.rs          # Keyboard driver + future
|   |   +-- serial.rs            # Serial port
|   |   +-- timer.rs             # Timer driver + future
|   +-- syscall/
|   |   +-- mod.rs               # Syscall module
|   |   +-- macros.rs            # Syscall definition macro
|   |   +-- table.rs             # Syscall dispatch table
|   +-- shell/
|   |   +-- mod.rs               # Shell implementation
|   |   +-- commands.rs          # Built-in commands
|   +-- types/
|       +-- mod.rs               # Type-safe memory regions
|       +-- port.rs              # Const generic port I/O
|       +-- mmio.rs              # Memory-mapped I/O
+-- syscall_macros/              # Procedural macro crate
|   +-- Cargo.toml
|   +-- src/
|       +-- lib.rs               # #[syscall] macro implementation
+-- Cargo.toml                   # Main crate manifest
+-- x86_64-rustos.json           # Custom target specification

Key Module Implementations

Kernel Entry Point (src/main.rs):

#![no_std]
#![no_main]
#![feature(abi_x86_interrupt)]
#![feature(alloc_error_handler)]

extern crate alloc;

use bootloader::{BootInfo, entry_point};
use core::panic::PanicInfo;

entry_point!(kernel_main);

fn kernel_main(boot_info: &'static BootInfo) -> ! {
    // Initialize kernel subsystems
    rustos::init(boot_info);

    // Print welcome message
    println!("RustOS v0.1.0 - Async Micro-Kernel");
    println!("===================================\n");

    // Create and run the async executor
    let mut executor = rustos::async_runtime::Executor::new();

    executor.spawn(rustos::shell::run());
    executor.spawn(rustos::drivers::timer::timer_service());

    // Run forever
    executor.run()
}

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    // Print panic in red
    rustos::drivers::vga::set_color(0x4F);
    println!("\n\n!!! KERNEL PANIC !!!");
    println!("{}", info);

    // Halt forever
    loop {
        x86_64::instructions::hlt();
    }
}

#[alloc_error_handler]
fn alloc_error(layout: core::alloc::Layout) -> ! {
    panic!("Allocation failed: {:?}", layout);
}

Phased Implementation Guide

Phase 1: Freestanding Binary and Boot (from Project 4)

Goal: Get a minimal kernel that boots and prints to VGA.

Duration: 2-3 days

Steps:

  1. Create project structure:
    cargo new --lib rustos
    cd rustos
    
  2. Configure for no_std:
    # Cargo.toml
    [package]
    name = "rustos"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    bootloader = "0.9"
    volatile = "0.4"
    spin = "0.9"
    x86_64 = "0.14"
    
    [profile.dev]
    panic = "abort"
    
    [profile.release]
    panic = "abort"
    
  3. Create custom target and cargo config

  4. Implement VGA buffer writer

  5. Test boot in QEMU

Checkpoint: Kernel boots and displays “Hello, RustOS!” on screen.

Phase 2: Integrate Custom Allocator (from Project 3)

Goal: Enable heap allocation in the kernel.

Duration: 3-4 days

Steps:

  1. Parse memory map from BootInfo
  2. Identify usable memory regions
  3. Implement bump allocator with spin lock
  4. Register as global allocator
  5. Enable alloc crate
  6. Test with Vec and Box

Checkpoint: let v = vec![1, 2, 3]; works in kernel code.

Phase 3: Set Up Interrupt Handling (IDT)

Goal: Handle CPU exceptions and hardware interrupts.

Duration: 4-5 days

Steps:

  1. Define IDT structure using x86_64 crate
  2. Implement exception handlers:
    • Divide by zero
    • Breakpoint
    • Double fault (with separate stack)
    • Page fault
    • General protection fault
  3. Remap PIC to vectors 32-47
  4. Enable interrupts

Checkpoint: int3 triggers breakpoint handler; division by zero is caught.

Phase 4: Implement Keyboard Driver as Future

Goal: Create async keyboard input.

Duration: 3-4 days

Steps:

  1. Create keyboard interrupt handler
  2. Implement scancode decoding
  3. Create KeyboardFuture struct
  4. Implement Future trait for keyboard
  5. Use AtomicWaker for waking

Checkpoint: KeyboardFuture.await returns pressed keys.

Phase 5: Build Async Executor for the Kernel

Goal: Run multiple async tasks concurrently.

Duration: 5-7 days

Steps:

  1. Define Task structure (pinned future + id)
  2. Implement waker with ready queue
  3. Create Executor with:
    • Task storage (BTreeMap)
    • Ready queue (VecDeque)
    • spawn() method
    • run() main loop with HLT
  4. Connect interrupt handlers to wakers
  5. Test with timer and keyboard futures

Checkpoint: Multiple async tasks run, yielding on I/O, with HLT when idle.

Phase 6: Create Syscall Macro with Proc Macros

Goal: Define system calls declaratively.

Duration: 3-4 days

Steps:

  1. Create syscall_macros crate
  2. Implement #[syscall(number = N)] derive macro
  3. Generate syscall table entries
  4. Generate argument extraction wrappers
  5. Define initial syscalls: write, exit

Checkpoint: #[syscall(number = 1)] fn sys_write(...) compiles and registers.

Phase 7: Type-Safe Memory Regions with Const Generics

Goal: Create zero-cost abstractions for hardware access.

Duration: 2-3 days

Steps:

  1. Define MemoryRegion<const ADDR, const SIZE>
  2. Implement read/write with bounds checking
  3. Create Port for I/O
  4. Refactor VGA and keyboard to use typed regions
  5. Add compile-time overlap detection

Checkpoint: All hardware access uses type-safe wrappers.

Phase 8: Simple Shell with Command Parsing

Goal: Interactive command-line interface.

Duration: 3-4 days

Steps:

  1. Create Shell struct with input buffer
  2. Implement async run() method
  3. Add line editing (backspace, basic cursor)
  4. Parse commands and arguments
  5. Implement built-in commands:
    • help, clear, echo, mem, tasks, uptime, reboot

Checkpoint: Shell accepts and executes commands.

Phase 9: Load and Run Simple User Programs

Goal: Execute simple programs in kernel space.

Duration: 5-7 days (stretch goal)

Steps:

  1. Define simple program format (static binary)
  2. Load program into heap memory
  3. Create execution context (stack, registers)
  4. Jump to program entry point
  5. Handle program exit

Checkpoint: Simple “Hello, World” program runs and returns.


Testing Strategy

QEMU Automation

Create a test runner that uses QEMU’s exit device:

// tests/basic_boot.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(rustos::test_runner)]
#![reexport_test_harness_main = "test_main"]

use rustos::serial_println;

#[no_mangle]
pub extern "C" fn _start() -> ! {
    test_main();
    loop {}
}

#[test_case]
fn test_vga_output() {
    rustos::println!("Testing VGA...");
    // If we got here, VGA works
}

#[test_case]
fn test_allocator() {
    use alloc::vec::Vec;
    let mut v = Vec::new();
    for i in 0..100 {
        v.push(i);
    }
    assert_eq!(v.len(), 100);
}

Serial Output Capture for CI

# .github/workflows/test.yml
name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y qemu-system-x86
      - name: Install Rust nightly
        uses: actions-rs/toolchain@v1
        with:
          toolchain: nightly
          override: true
          components: rust-src, llvm-tools-preview
      - name: Install bootimage
        run: cargo install bootimage
      - name: Run tests
        run: cargo test
        timeout-minutes: 5

Integration Tests with Boot Verification

// tests/boot_test.rs
#[test_case]
fn test_boot_sequence() {
    // Verify memory map is valid
    assert!(rustos::memory::heap_start() != 0);
    assert!(rustos::memory::heap_size() > 0);

    // Verify IDT is loaded
    assert!(rustos::idt::is_loaded());

    // Verify interrupts are enabled
    assert!(x86_64::instructions::interrupts::are_enabled());
}

#[test_case]
fn test_keyboard_future() {
    use rustos::drivers::keyboard::KeyboardFuture;
    use core::future::Future;
    use core::pin::Pin;
    use core::task::{Context, Poll};

    // Simulate keyboard interrupt
    rustos::drivers::keyboard::inject_scancode(0x1E); // 'A'

    let mut future = KeyboardFuture::new();
    let waker = /* create test waker */;
    let mut cx = Context::from_waker(&waker);

    match Pin::new(&mut future).poll(&mut cx) {
        Poll::Ready(key) => assert_eq!(key, b'A'),
        Poll::Pending => panic!("Expected ready"),
    }
}

Common Pitfalls

Pitfall 1: Stack Overflow in Interrupt Handlers

Symptom: Double fault or triple fault (reboot) during interrupt.

Cause: Interrupt handler uses too much stack, overflows into guard page.

Solution:

  • Use separate interrupt stack (TSS with IST)
  • Keep handlers minimal - just wake futures
  • Don’t allocate in handlers
// BAD: Allocates in handler
extern "x86-interrupt" fn keyboard_handler(_: InterruptStackFrame) {
    let key = decode_key();
    BUFFER.lock().push(key);  // Lock + allocation!
}

// GOOD: Just wake
extern "x86-interrupt" fn keyboard_handler(_: InterruptStackFrame) {
    let scancode = inb(0x60);
    SCANCODE.store(scancode, Ordering::SeqCst);
    KEYBOARD_WAKER.wake();
    end_of_interrupt();
}

Pitfall 2: Allocator Not Initialized Before Use

Symptom: Panic in alloc or page fault when using Vec/Box.

Cause: Code tries to allocate before heap is set up.

Solution:

  • Initialize allocator early in boot
  • Don’t use alloc types in global statics
  • Use lazy_static or OnceCell for heap-dependent globals
// BAD: Tries to create Vec at static init
static BUFFER: Vec<u8> = Vec::new();  // PANIC!

// GOOD: Lazy initialization
lazy_static! {
    static ref BUFFER: Mutex<Vec<u8>> = Mutex::new(Vec::new());
}

Pitfall 3: Interrupt Reentrancy Issues

Symptom: Deadlock or corrupt data when interrupt fires during lock.

Cause: Interrupt handler tries to acquire lock held by interrupted code.

Solution:

  • Disable interrupts while holding locks
  • Use interrupt-safe locks (disable interrupts on acquire)
  • Design handlers to be lock-free
// BAD: Can deadlock
fn use_buffer() {
    let guard = BUFFER.lock();  // Acquire lock
    // ... interrupt fires here, handler also wants BUFFER ...
}

// GOOD: Disable interrupts
fn use_buffer() {
    x86_64::instructions::interrupts::without_interrupts(|| {
        let guard = BUFFER.lock();
        // Safe - interrupts disabled
    });
}

Pitfall 4: Page Table Misconfiguration

Symptom: Page fault when accessing heap or hardware.

Cause: Memory not properly mapped in page tables.

Solution:

  • Use bootloader’s physical memory map
  • Verify virtual addresses are in valid regions
  • Check present and writable bits in page entries
// Debug page table entries
fn debug_page(addr: usize) {
    let entry = get_page_table_entry(addr);
    serial_println!("Address: {:#x}", addr);
    serial_println!("  Present: {}", entry.is_present());
    serial_println!("  Writable: {}", entry.is_writable());
    serial_println!("  Physical: {:#x}", entry.physical_frame());
}

Pitfall 5: Forgetting End of Interrupt (EOI)

Symptom: Only first interrupt of each type works.

Cause: PIC waits for EOI before sending more interrupts.

Solution:

  • Send EOI at end of every hardware interrupt handler
  • For slave PIC interrupts, send EOI to both PICs
extern "x86-interrupt" fn keyboard_handler(_: InterruptStackFrame) {
    // Handle interrupt...

    // MUST send EOI!
    unsafe {
        PICS.lock().notify_end_of_interrupt(33);
    }
}

Extensions

Once the basic kernel is working, consider these extensions:

Extension 1: Preemptive Scheduling

Add timer-based preemption:

  • Save full CPU state on timer interrupt
  • Implement round-robin scheduler
  • Add priority levels

Extension 2: Simple Filesystem

Implement a RAM-based filesystem:

  • Define file/directory structures
  • Implement open, read, write, close syscalls
  • Add ls, cat, write commands to shell

Extension 3: Basic Networking

Add network stack:

  • Implement virtio-net driver
  • Add UDP socket support
  • Implement simple echo server

Extension 4: Multi-Core Support

Enable SMP:

  • Parse ACPI tables for CPU info
  • Start application processors
  • Implement per-CPU data structures
  • Add work-stealing scheduler

Extension 5: User-Space Processes

Full process isolation:

  • Separate address spaces per process
  • Implement proper syscall interface
  • Add fork/exec primitives
  • Create simple ELF loader

The Interview Questions They Will Ask

  1. “Describe the boot process from power-on to your kernel starting.”

    Answer guideline: Cover BIOS/UEFI firmware, bootloader responsibilities (CPU mode transitions, loading kernel), and what state the CPU is in when your code runs.

  2. “How does your async executor work without OS threads?”

    Answer guideline: Explain interrupt-driven waking, the Waker mechanism, polling loop with HLT, and how hardware events translate to future completion.

  3. “What happens when an interrupt fires while your kernel is in the middle of a syscall?”

    Answer guideline: Discuss interrupt nesting, stack switching, register saving, and how you ensure atomic operations and lock safety.

  4. “How do you prevent memory corruption between different kernel subsystems?”

    Answer guideline: Explain Rust’s ownership model, how it provides safety even without hardware protection, and compare to traditional C kernels that rely on MMU.

  5. “What are the trade-offs between your micro-kernel design and a monolithic kernel like Linux?”

    Answer guideline: Discuss performance (IPC overhead), reliability (fault isolation), complexity, and how Rust changes the equation.

  6. “How does your custom allocator integrate with the async runtime?”

    Answer guideline: Explain heap initialization, the GlobalAlloc trait, and how async task storage interacts with allocation.

  7. “Explain how you handle a page fault in your kernel.”

    Answer guideline: Cover the page fault handler, determining fault cause (not present, protection violation), and potential recovery strategies.

  8. “How do your procedural macros for syscalls work?”

    Answer guideline: Describe token parsing, code generation, syscall table population, and argument marshaling.

  9. “What challenges did you face with unsafe code, and how did you manage them?”

    Answer guideline: Discuss invariants, unsafe block scope minimization, documentation of safety requirements, and testing strategies.

  10. “How would you add multi-core support to your kernel?”

    Answer guideline: Cover SMP boot protocol, per-CPU data structures, atomic synchronization, and potential scheduler designs.


Books That Will Help

Topic Book Recommended Sections
OS Fundamentals “Operating Systems: Three Easy Pieces” by Remzi & Andrea Arpaci-Dusseau Full book - free online at ostep.org. Essential for understanding virtualization, concurrency, and persistence.
Advanced Rust “Rust for Rustaceans” by Jon Gjengset Full book. Especially Ch. 8 (Async), Ch. 9 (Unsafe), and Ch. 10 (Concurrency).
Low-Level Systems “The Secret Life of Programs” by Jonathan Steinhart Full book. Excellent for understanding what happens “under the hood” of computers.
Computer Architecture “Computer Systems: A Programmer’s Perspective” by Bryant & O’Hallaron Ch. 7 (Linking), Ch. 8 (Exceptional Control Flow), Ch. 9 (Virtual Memory).
x86 Specifics Phil Oppermann’s Blog OS https://os.phil-opp.com/ - The definitive tutorial for Rust OS development.

Additional Resources

Resource Description
OSDev Wiki https://wiki.osdev.org/ - Comprehensive OS development reference
Intel SDM Intel 64 and IA-32 Architectures Software Developer Manuals
Redox Book https://doc.redox-os.org/book/ - Design of a real Rust OS
Theseus Paper “Theseus: an Experiment in Operating System Structure and State Management”

Success Criteria

You have completed this project when:

  1. Your kernel boots in QEMU and displays initialization messages
  2. The heap allocator is functional and supports Vec, Box, String
  3. Keyboard and timer interrupts are handled via async futures
  4. The executor runs multiple concurrent tasks with proper yielding
  5. The shell accepts input and executes built-in commands
  6. Type-safe abstractions protect all hardware access
  7. Syscall macros generate valid dispatch code
  8. All tests pass in CI
  9. You can explain every layer from boot to shell command execution
  10. You’ve documented the design decisions and trade-offs

Summary

This project is the capstone of your Advanced Rust journey. You have integrated:

  • Memory management (Project 3’s allocator)
  • Bare-metal programming (Project 4’s no_std foundation)
  • Type-level programming (Project 5’s const generics)
  • Async runtime internals (Project 8’s executor)
  • Metaprogramming (Project 10’s procedural macros)

Into a cohesive, functional operating system kernel that demonstrates:

  • Modern OS design principles (micro-kernel, async I/O)
  • Rust’s unique safety advantages for systems programming
  • Practical application of every advanced Rust concept

You are now among the rare developers who can claim to have built an operating system from scratch. The skills you’ve developed - understanding of hardware, mastery of unsafe Rust, async programming at the lowest level, and system architecture - are the foundation of systems programming expertise.

Welcome to the world of OS development. The machine is truly yours.


This project is the Final Project of the Advanced Rust Ecosystem Deep Dive learning path.