Project 7: Interrupt Descriptor Table (IDT)
Build a complete interrupt handling system with IDT, ISR stubs, exception handlers, and hardware IRQ support—the foundation for preemptive multitasking and hardware event processing.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Expert (Level 4) |
| Time Estimate | 2 weeks |
| Language | C with x86 Assembly |
| Alternative | Rust |
| Prerequisites | Project 6 (Protected Mode), understanding of CPU exceptions |
| Key Topics | IDT structure, ISR stubs, 8259 PIC, CPU exceptions, hardware IRQs |
| Software/Tools | GCC cross-compiler (i686-elf-gcc), NASM, QEMU, GDB |
| Main Book | “Operating Systems: Three Easy Pieces” by Arpaci-Dusseau |
Table of Contents
- 1. Learning Objectives
- 2. Theoretical Foundation
- 3. Project Specification
- 4. Solution Architecture
- 5. Implementation Guide
- 6. Testing Strategy
- 7. Common Pitfalls & Debugging
- 8. Extensions & Challenges
- 9. Real-World Connections
- 10. Resources
- 11. Self-Assessment Checklist
- 12. Submission / Completion Criteria
1. Learning Objectives
By completing this project, you will:
- Master the x86 interrupt architecture: Understand how the CPU handles synchronous (exceptions) and asynchronous (hardware) interrupts
- Implement the Interrupt Descriptor Table: Create and load an IDT with 256 entries for all possible interrupt vectors
- Write ISR stubs in assembly: Create interrupt service routine entry points that properly save and restore CPU state
- Program the 8259 PIC: Remap and configure the Programmable Interrupt Controller for hardware IRQs
- Handle CPU exceptions: Implement handlers for divide-by-zero, page faults, general protection faults, and other exceptions
- Process hardware interrupts: Handle timer ticks (IRQ0), keyboard input (IRQ1), and other hardware events
- Understand the stack during interrupts: Know exactly what the CPU pushes, what you push, and how to return cleanly
- Build the foundation for preemptive multitasking: Understand how timer interrupts enable context switching
2. Theoretical Foundation
2.1 Core Concepts
What Are Interrupts?
Interrupts are signals that cause the CPU to temporarily stop what it’s doing, save its state, execute a handler function, and then resume the original work. They are the fundamental mechanism for:
- Hardware events: Keyboard presses, timer ticks, disk I/O completion
- CPU exceptions: Divide by zero, page faults, invalid opcodes
- System calls: Software-triggered interrupts (INT instruction)
+-----------------------------------------------------------------------+
| INTERRUPT CLASSIFICATION |
+-----------------------------------------------------------------------+
| |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ INTERRUPTS │ |
| │ │ │ |
| │ ┌──────────────────────┴──────────────────────┐ │ |
| │ │ │ │ |
| │ ▼ ▼ │ |
| │ ┌─────────────────────┐ ┌─────────────────────┐ │ |
| │ │ EXCEPTIONS │ │ EXTERNAL (IRQs) │ │ |
| │ │ (Synchronous) │ │ (Asynchronous) │ │ |
| │ │ │ │ │ │ |
| │ │ Caused by executing │ │ Caused by hardware │ │ |
| │ │ instructions │ │ devices │ │ |
| │ └──────────┬──────────┘ └──────────┬──────────┘ │ |
| │ │ │ │ |
| │ ┌────────┴────────┐ ┌────────┴────────┐ │ |
| │ │ │ │ │ │ |
| │ ▼ ▼ ▼ ▼ │ |
| │ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ |
| │ │ Faults│ │ Traps │ │Maskable│ │ NMI │ │ |
| │ │ │ │ │ │ (INTR) │ │ │ │ |
| │ │Return │ │Return │ │ │ │Always │ │ |
| │ │to same│ │to next│ │Can be │ │handled│ │ |
| │ │instr │ │instr │ │disabled│ │ │ │ |
| │ └───────┘ └───────┘ └───────┘ └───────┘ │ |
| │ │ |
| │ Examples: Examples: Examples: Example: │ |
| │ - Page Fault - Breakpoint - Timer (IRQ0) - Memory │ |
| │ - Div by Zero - INT instruction - Keyboard - Parity │ |
| │ - GP Fault - Overflow - Disk - Error │ |
| │ - Network │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
+-----------------------------------------------------------------------+
The Interrupt Descriptor Table (IDT)
The IDT is a table in memory that tells the CPU where to find the handler for each interrupt vector (0-255). When an interrupt occurs, the CPU:
- Uses the interrupt number as an index into the IDT
- Reads the IDT entry to find the handler address
- Saves the current state (registers, flags, return address)
- Jumps to the handler
+-----------------------------------------------------------------------+
| INTERRUPT DESCRIPTOR TABLE |
+-----------------------------------------------------------------------+
| |
| IDT Register (IDTR) - loaded with LIDT instruction: |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ Limit (16-bit) │ Base Address (32-bit) │ |
| │ Size of IDT - 1 │ Physical address of IDT │ |
| │ (256 * 8) - 1 = 0x7FF │ e.g., 0x00000000 │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| IDT Layout (256 entries, each 8 bytes = 2048 bytes total): |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ Vector 0: Divide Error (#DE) [Fault] │ |
| │ Vector 1: Debug Exception (#DB) [Fault/Trap] │ |
| │ Vector 2: NMI Interrupt [Interrupt] │ |
| │ Vector 3: Breakpoint (#BP) [Trap] │ |
| │ Vector 4: Overflow (#OF) [Trap] │ |
| │ Vector 5: BOUND Range Exceeded (#BR) [Fault] │ |
| │ Vector 6: Invalid Opcode (#UD) [Fault] │ |
| │ Vector 7: Device Not Available (#NM) [Fault] │ |
| │ Vector 8: Double Fault (#DF) [Abort] │ |
| │ Vector 9: Coprocessor Segment Overrun [Fault] │ |
| │ Vector 10: Invalid TSS (#TS) [Fault] │ |
| │ Vector 11: Segment Not Present (#NP) [Fault] │ |
| │ Vector 12: Stack-Segment Fault (#SS) [Fault] │ |
| │ Vector 13: General Protection Fault (#GP) [Fault] │ |
| │ Vector 14: Page Fault (#PF) [Fault] │ |
| │ Vector 15: Reserved │ |
| │ Vector 16: x87 FPU Error (#MF) [Fault] │ |
| │ Vector 17: Alignment Check (#AC) [Fault] │ |
| │ Vector 18: Machine Check (#MC) [Abort] │ |
| │ Vector 19: SIMD Floating-Point (#XM) [Fault] │ |
| │ Vector 20: Virtualization Exception (#VE) [Fault] │ |
| │ Vectors 21-31: Reserved by Intel │ |
| ├─────────────────────────────────────────────────────────────────┤ |
| │ Vector 32: IRQ0 - Programmable Interval Timer [After remap] │ |
| │ Vector 33: IRQ1 - Keyboard [After remap] │ |
| │ Vector 34: IRQ2 - Cascade (PIC2) [After remap] │ |
| │ Vector 35: IRQ3 - COM2 [After remap] │ |
| │ Vector 36: IRQ4 - COM1 [After remap] │ |
| │ Vector 37: IRQ5 - LPT2 / Sound Card [After remap] │ |
| │ Vector 38: IRQ6 - Floppy Disk [After remap] │ |
| │ Vector 39: IRQ7 - LPT1 / Spurious [After remap] │ |
| │ Vector 40: IRQ8 - CMOS Real-Time Clock [After remap] │ |
| │ Vector 41: IRQ9 - ACPI / Legacy IRQ2 [After remap] │ |
| │ Vector 42: IRQ10 - Available [After remap] │ |
| │ Vector 43: IRQ11 - Available [After remap] │ |
| │ Vector 44: IRQ12 - PS/2 Mouse [After remap] │ |
| │ Vector 45: IRQ13 - FPU / Coprocessor [After remap] │ |
| │ Vector 46: IRQ14 - Primary ATA Hard Disk [After remap] │ |
| │ Vector 47: IRQ15 - Secondary ATA Hard Disk [After remap] │ |
| ├─────────────────────────────────────────────────────────────────┤ |
| │ Vectors 48-255: Available for software interrupts │ |
| │ Vector 0x80 (128): Often used for system calls (like Linux) │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
+-----------------------------------------------------------------------+
IDT Entry Format (8 bytes for 32-bit Protected Mode)
Each IDT entry (gate descriptor) is 8 bytes with a specific structure:
+-----------------------------------------------------------------------+
| IDT ENTRY FORMAT (Gate Descriptor) |
+-----------------------------------------------------------------------+
| |
| 8-byte structure (bits numbered from right to left): |
| |
| Bytes 7-6 (bits 63-48): Offset High (bits 31-16 of handler addr) |
| Byte 5 (bits 47-40): Flags/Attributes |
| Byte 4 (bits 39-32): Reserved (must be 0) |
| Bytes 3-2 (bits 31-16): Segment Selector (0x08 for kernel code) |
| Bytes 1-0 (bits 15-0): Offset Low (bits 15-0 of handler addr) |
| |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ 63 48│47 40│39 32│31 16│15 0 │ |
| │ │ │ │ │ │ |
| │ Offset High │ Flags │ Zero │ Selector │ Offset Low │ |
| │ (31:16) │ │ │ (code seg) │ (15:0) │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| Flags/Attributes Byte (Byte 5): |
| ┌───┬───┬───┬───┬───┬───┬───┬───┐ |
| │ P │ DPL │ S │ Type │ Bits: 7 6-5 4 3-0 |
| └───┴───┴───┴───┴───┴───┴───┴───┘ |
| |
| P (bit 7): Present (1 = valid entry, 0 = unused) |
| DPL (bits 6-5): Descriptor Privilege Level |
| 0 = kernel only, 3 = user can trigger |
| S (bit 4): Storage Segment (always 0 for gates) |
| Type (bits 3-0): Gate type |
| |
| Gate Types (for 32-bit protected mode): |
| ┌────────────────────────────────────────────────────────────────┐ |
| │ 0x5 (0101): 32-bit Task Gate │ |
| │ 0xE (1110): 32-bit Interrupt Gate <-- Use for interrupts │ |
| │ 0xF (1111): 32-bit Trap Gate <-- Use for exceptions │ |
| └────────────────────────────────────────────────────────────────┘ |
| |
| Interrupt Gate vs Trap Gate: |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ Interrupt Gate: Automatically clears IF flag (disables ints) │ |
| │ Use for: IRQs, critical sections │ |
| │ │ |
| │ Trap Gate: Does NOT modify IF flag │ |
| │ Use for: Exceptions, system calls │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| Common Flags values: |
| 0x8E = 10001110 = Present, Ring 0, Interrupt Gate (32-bit) |
| 0x8F = 10001111 = Present, Ring 0, Trap Gate (32-bit) |
| 0xEE = 11101110 = Present, Ring 3, Interrupt Gate (user callable) |
| |
+-----------------------------------------------------------------------+
The 8259 Programmable Interrupt Controller (PIC)
The 8259 PIC is the chip that manages hardware interrupts on legacy PC systems. There are actually two PICs: a master and a slave, cascaded together:
+-----------------------------------------------------------------------+
| 8259 PIC ARCHITECTURE |
+-----------------------------------------------------------------------+
| |
| Hardware Devices Master PIC Slave PIC |
| (I/O 0x20-0x21) (I/O 0xA0-0xA1) |
| |
| ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ |
| │ Timer │──IRQ0────►│ IR0 │ │ │ |
| │ (PIT) │ │ │ │ │ |
| └─────────────┘ │ │ │ │ |
| │ │ │ │ |
| ┌─────────────┐ │ │ │ │ |
| │ Keyboard │──IRQ1────►│ IR1 │ │ │ |
| │ Controller │ │ │ │ │ |
| └─────────────┘ │ │ │ │ |
| │ │ │ │ |
| │ │ │ │ |
| ┌────────►│ IR2 │◄────│ INT │ |
| │ Cascade │ (Cascade) │ │ (to master) │ |
| │ │ │ │ │ |
| ┌─────────────┐ │ │ │ │ │ |
| │ COM2 │──IRQ3────►│ IR3 │ │ IR0 ◄──────IRQ8 (RTC)|
| └─────────────┘ │ │ │ │ |
| │ │ │ IR1 ◄──────IRQ9 |
| ┌─────────────┐ │ │ │ │ |
| │ COM1 │──IRQ4────►│ IR4 │ │ IR2 ◄──────IRQ10 |
| └─────────────┘ │ MASTER │ │ │ |
| │ 8259A │ │ IR3 ◄──────IRQ11 |
| ┌─────────────┐ │ │ │ │ |
| │ LPT2/Sound │──IRQ5────►│ IR5 │ │ IR4 ◄──────IRQ12(PS2)|
| └─────────────┘ │ │ │ SLAVE │ |
| │ │ │ 8259A │ |
| ┌─────────────┐ │ │ │ IR5 ◄──────IRQ13(FPU)|
| │ Floppy │──IRQ6────►│ IR6 │ │ │ |
| └─────────────┘ │ │ │ IR6 ◄──────IRQ14(ATA)|
| │ │ │ │ |
| ┌─────────────┐ │ │ │ IR7 ◄──────IRQ15(ATA)|
| │ LPT1 │──IRQ7────►│ IR7 │ │ │ |
| └─────────────┘ │ │ │ │ |
| │ │ │ │ |
| │ INT────────────────────────────►│ CPU |
| │ (to CPU) │ │ │ INTR |
| └─────────────┘ └─────────────┘ |
| |
| Default vector mapping (conflicts with CPU exceptions!): |
| Master PIC: IRQ0-IRQ7 → Vectors 0x08-0x0F (conflicts!) |
| Slave PIC: IRQ8-IRQ15 → Vectors 0x70-0x77 |
| |
| After remapping (required!): |
| Master PIC: IRQ0-IRQ7 → Vectors 0x20-0x27 (32-39) |
| Slave PIC: IRQ8-IRQ15 → Vectors 0x28-0x2F (40-47) |
| |
+-----------------------------------------------------------------------+
Stack Layout During Interrupts
When an interrupt occurs, the CPU automatically pushes certain values onto the stack before jumping to your handler. Understanding this is critical:
+-----------------------------------------------------------------------+
| STACK DURING INTERRUPT HANDLING |
+-----------------------------------------------------------------------+
| |
| What the CPU pushes automatically (before your handler runs): |
| |
| WITHOUT privilege level change (interrupt from Ring 0): |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ High addresses │ |
| │ ┌───────────────────────────────────────────────────────────┐ │ |
| │ │ EFLAGS (pushed first) │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ CS (code segment selector) │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ EIP (return address) │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ Error Code (only for some exceptions!) │ │ |
| │ └───────────────────────────────────────────────────────────┘ │ |
| │ Low addresses ◄── ESP points here │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| WITH privilege level change (interrupt from Ring 3 → Ring 0): |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ High addresses (kernel stack) │ |
| │ ┌───────────────────────────────────────────────────────────┐ │ |
| │ │ SS (user stack segment) (pushed first) │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ ESP (user stack pointer) │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ EFLAGS │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ CS │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ EIP │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ Error Code (if applicable) │ │ |
| │ └───────────────────────────────────────────────────────────┘ │ |
| │ Low addresses ◄── ESP points here (on kernel stack) │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| Exceptions with error codes (CPU pushes automatically): |
| #DF (8), #TS (10), #NP (11), #SS (12), #GP (13), #PF (14), #AC (17)|
| |
| Exceptions WITHOUT error codes (you must push a dummy value): |
| #DE (0), #DB (1), #NMI (2), #BP (3), #OF (4), #BR (5), #UD (6), |
| #NM (7), #MF (16), #MC (18), #XM (19), and all IRQs |
| |
+-----------------------------------------------------------------------+
Complete Stack After ISR Stub Runs
Your ISR stub needs to save additional state to create a uniform structure:
+-----------------------------------------------------------------------+
| COMPLETE INTERRUPT FRAME |
+-----------------------------------------------------------------------+
| |
| After your ISR stub pushes everything: |
| |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ High addresses │ |
| │ ┌───────────────────────────────────────────────────────────┐ │ |
| │ │ (SS - only if privilege change) │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ (ESP - only if privilege change) │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ EFLAGS [CPU pushed] │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ CS [CPU pushed] │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ EIP [CPU pushed] │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ Error Code (or dummy 0) [You pushed] │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ Interrupt Number [You pushed] │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ EAX [pusha] │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ ECX [pusha] │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ EDX [pusha] │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ EBX [pusha] │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ ESP (value before pusha) [pusha] │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ EBP [pusha] │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ ESI [pusha] │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ EDI [pusha] │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ DS [You pushed] │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ ES [You pushed] │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ FS [You pushed] │ │ |
| │ ├───────────────────────────────────────────────────────────┤ │ |
| │ │ GS [You pushed] │ │ |
| │ └───────────────────────────────────────────────────────────┘ │ |
| │ Low addresses ◄── ESP points here │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| This structure is often defined as a C struct for easy access: |
| |
| struct interrupt_frame { |
| uint32_t gs, fs, es, ds; // Segment registers |
| uint32_t edi, esi, ebp, esp; // pusha (reversed) |
| uint32_t ebx, edx, ecx, eax; // pusha continued |
| uint32_t int_no, err_code; // Pushed by ISR stub |
| uint32_t eip, cs, eflags; // Pushed by CPU |
| uint32_t useresp, ss; // Only if ring change |
| }; |
| |
+-----------------------------------------------------------------------+
2.2 Why This Matters
Interrupts Are Fundamental to All Operating Systems
Without interrupts:
- No keyboard input: The CPU would have to constantly poll the keyboard
- No timer ticks: No way to preempt running processes
- No disk I/O: Would have to poll disk status continuously
- No exception handling: Divide by zero would crash the entire system
- No multitasking: Can’t switch between processes
Real Operating Systems Depend on This
+-----------------------------------------------------------------------+
| INTERRUPT USAGE IN REAL SYSTEMS |
+-----------------------------------------------------------------------+
| |
| Linux Kernel: |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ arch/x86/kernel/idt.c - IDT setup and management │ |
| │ arch/x86/kernel/irq.c - IRQ handling │ |
| │ arch/x86/entry/entry_32.S - ISR entry points (32-bit) │ |
| │ arch/x86/include/asm/desc.h - IDT structures │ |
| │ kernel/irq/handle.c - Generic IRQ handling │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| Windows Kernel: |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ ntoskrnl.exe - Interrupt dispatch │ |
| │ hal.dll - Hardware abstraction (PIC/APIC) │ |
| │ Interrupt Request Levels (IRQL) - Priority-based handling │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| Real-Time Operating Systems: |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ Deterministic interrupt latency is CRITICAL │ |
| │ - Medical devices: Pacemakers must respond in microseconds │ |
| │ - Automotive: ABS braking systems, airbag deployment │ |
| │ - Industrial: Robot arm control, CNC machines │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
+-----------------------------------------------------------------------+
2.3 Historical Context
The x86 interrupt architecture has evolved significantly:
+-----------------------------------------------------------------------+
| EVOLUTION OF x86 INTERRUPTS |
+-----------------------------------------------------------------------+
| |
| 1978: 8086 |
| ├── Interrupt Vector Table (IVT) at 0x0000-0x03FF |
| ├── 256 vectors, each 4 bytes (segment:offset) |
| └── No protection, any code can modify IVT |
| |
| 1981: IBM PC (8088) + 8259 PIC |
| ├── Master PIC for IRQ0-IRQ7 |
| ├── Default mapping: IRQ0-7 → INT 8-15 (conflicts with exceptions!) |
| └── Later: Slave PIC added for IRQ8-15 |
| |
| 1985: 80386 Protected Mode |
| ├── Interrupt Descriptor Table (IDT) replaces IVT |
| ├── Each entry 8 bytes with gate descriptors |
| ├── Privilege levels enforced |
| └── This is what we implement in this project |
| |
| 1993: Pentium + APIC |
| ├── Advanced Programmable Interrupt Controller |
| ├── Local APIC per CPU + I/O APIC for devices |
| ├── Supports more than 16 IRQs |
| └── Required for SMP (symmetric multiprocessing) |
| |
| 2003: AMD64 Long Mode |
| ├── IDT entries expanded to 16 bytes each |
| ├── 64-bit handler addresses |
| └── IST (Interrupt Stack Table) for dedicated stacks |
| |
| Modern systems: APIC is standard, but 8259 PIC still emulated |
| for compatibility (and used during boot before APIC is configured) |
| |
+-----------------------------------------------------------------------+
2.4 Common Misconceptions
| Misconception | Reality |
|---|---|
| “All interrupts push error codes” | Only specific exceptions push error codes; for others, you must push a dummy value for uniform stack layout |
| “IRQs start at vector 0” | By default they start at 8 (master) and 0x70 (slave), but this conflicts with exceptions. You MUST remap them! |
| “You can use BIOS for interrupt handling” | BIOS uses real-mode IVT; in protected mode you must set up your own IDT |
| “Interrupt handlers are just regular C functions” | They need special entry/exit code to save/restore all state and use IRET |
| “The CPU disables all interrupts during an ISR” | Only interrupt gates disable IF; trap gates do not. And you can manually enable interrupts if needed. |
| “One PIC handles all 16 IRQs” | There are two PICs (master and slave) cascaded together |
| “The EOI (End of Interrupt) is automatic” | You MUST send EOI to the PIC(s) or no more IRQs from that line will be delivered |
3. Project Specification
3.1 What You Will Build
A complete interrupt handling subsystem for your x86 kernel that:
- Creates an IDT with 256 entries covering all CPU exceptions and hardware IRQs
- Implements ISR stubs in assembly that properly save/restore CPU state
- Remaps the 8259 PIC to avoid conflicts with CPU exception vectors
- Handles CPU exceptions with informative error messages (divide by zero, page fault, etc.)
- Handles hardware IRQs including timer interrupts and keyboard input
- Provides a common interrupt handler in C that dispatches to specific handlers
- Demonstrates working interrupts with a test suite
3.2 Functional Requirements
| ID | Requirement | Description |
|---|---|---|
| FR-1 | IDT Creation | Create 256 IDT entries with correct format |
| FR-2 | IDT Loading | Load IDT using LIDT instruction |
| FR-3 | ISR Stubs | 256 assembly ISR stubs that save/restore state |
| FR-4 | PIC Remapping | Remap master PIC to vectors 0x20-0x27, slave to 0x28-0x2F |
| FR-5 | Exception Handling | Handle at least: #DE (0), #UD (6), #DF (8), #GP (13), #PF (14) |
| FR-6 | Timer IRQ | Handle IRQ0 (timer) and count ticks |
| FR-7 | Keyboard IRQ | Handle IRQ1 (keyboard) and read scancodes |
| FR-8 | EOI Sending | Properly acknowledge IRQs to PIC after handling |
| FR-9 | C Handler | Common C handler that receives interrupt_frame struct |
3.3 Non-Functional Requirements
| ID | Requirement | Description |
|---|---|---|
| NFR-1 | Modular Code | Separate files for IDT, ISRs, PIC, and handlers |
| NFR-2 | Documented | Each IDT entry and exception explained in comments |
| NFR-3 | Debuggable | Print meaningful information on exceptions |
| NFR-4 | Stable | System should not crash from unexpected interrupts |
| NFR-5 | Extensible | Easy to add new interrupt handlers |
3.4 Example Usage / Output
# Build and run
$ make
nasm -f elf32 -o isr.o isr.asm
i686-elf-gcc -c -ffreestanding -O2 -Wall -o kernel.o kernel.c
i686-elf-gcc -c -ffreestanding -O2 -Wall -o idt.o idt.c
i686-elf-gcc -c -ffreestanding -O2 -Wall -o pic.o pic.c
i686-elf-gcc -c -ffreestanding -O2 -Wall -o handlers.o handlers.c
i686-elf-ld -T linker.ld -o kernel.elf boot.o isr.o kernel.o idt.o pic.o handlers.o
i686-elf-objcopy -O binary kernel.elf kernel.bin
$ qemu-system-i386 -kernel kernel.elf
# Screen output:
================================================================================
x86 Kernel - Interrupt Demo
================================================================================
[INIT] Setting up IDT (256 entries)...
[INIT] Remapping 8259 PIC (master: 0x20, slave: 0x28)...
[INIT] Loading IDT at 0x00001000 (limit: 0x07FF)...
[INIT] Enabling interrupts (STI)...
=== Interrupt Test Suite ===
[Test 1] Divide by zero exception
Triggering: 1/0
[EXCEPTION 0] Divide Error (#DE)
EIP: 0x00100234
Error Code: 0x00000000
[HANDLED] Exception handled, continuing...
[Test 2] Timer interrupt (IRQ0)
Waiting for timer ticks...
[IRQ 0] Timer tick #1 (PIT frequency: ~18.2 Hz)
[IRQ 0] Timer tick #2
[IRQ 0] Timer tick #3
[Test 3] Keyboard interrupt (IRQ1)
Press a key...
[IRQ 1] Keyboard: scancode 0x1E (key 'A' pressed)
[IRQ 1] Keyboard: scancode 0x9E (key 'A' released)
[Test 4] Software interrupt (INT 0x80)
Triggering: INT 0x80
[INT 0x80] System call handler invoked
EAX: 0x00000001 (syscall number)
=== All interrupt tests passed! ===
System running with interrupts enabled...
Timer ticks: 47
Press 'Q' to halt system.
3.5 Real World Outcome
After completing this project, you will have:
- Working interrupt system that handles both hardware and software interrupts
- Foundation for keyboard input - can read key presses
- Foundation for preemptive scheduling - timer interrupts for context switching
- Exception handling - graceful handling of CPU faults
- Understanding of x86 interrupt architecture applicable to real OS development
4. Solution Architecture
4.1 High-Level Design
+-----------------------------------------------------------------------+
| INTERRUPT HANDLING ARCHITECTURE |
+-----------------------------------------------------------------------+
| |
| Interrupt Sources Your Kernel |
| |
| ┌─────────────┐ ┌─────────────────────────────┐ |
| │ Timer │─IRQ0──┐ │ IDT (256 entries) │ |
| │ (PIT) │ │ │ │ |
| └─────────────┘ │ │ ┌───────────────────────┐ │ |
| │ │ │ Vector 0: isr0 ────────────────┐
| ┌─────────────┐ │ │ │ Vector 1: isr1 │ │ │
| │ Keyboard │─IRQ1──┤ │ │ ... │ │ │
| │ │ │ │ │ Vector 13: isr13 ──────────────┤
| └─────────────┘ │ │ │ Vector 14: isr14 │ │ │
| │ │ │ ... │ │ │
| ┌─────────────┐ │ │ │ Vector 32: irq0 ───────────────┤
| │ Other │─IRQx──┤ │ │ Vector 33: irq1 │ │ │
| │ Devices │ │ │ │ ... │ │ │
| └─────────────┘ │ │ │ Vector 128: int80 ─────────────┤
| │ │ └───────────────────────┘ │ │
| ┌─────────────┐ │ │ │ │
| │ CPU (div0, │───────┘ └─────────────────────────────┘ │
| │ page fault, │ │
| │ etc.) │ │
| └─────────────┘ │
| │
| │ │
| │ Interrupt! │
| ▼ │
| ┌─────────────────────────────────────────────────────────────────┐ │
| │ ISR STUB (Assembly) │◄─┘
| │ │
| │ 1. Push error code (dummy 0 if not provided by CPU) │
| │ 2. Push interrupt number │
| │ 3. PUSHA (save all general-purpose registers) │
| │ 4. Push segment registers (DS, ES, FS, GS) │
| │ 5. Load kernel data segment (0x10) │
| │ 6. CALL isr_handler (C function) │
| │ 7. Pop segment registers │
| │ 8. POPA (restore general-purpose registers) │
| │ 9. ADD ESP, 8 (remove error code and int number) │
| │ 10. IRET (return from interrupt) │
| │ │
| └─────────────────────────────────────────────────────────────────┘
| │
| │ CALL with stack pointer as argument
| ▼
| ┌─────────────────────────────────────────────────────────────────┐
| │ isr_handler (C) │
| │ │
| │ void isr_handler(interrupt_frame* frame) { │
| │ if (frame->int_no < 32) { │
| │ exception_handler(frame); // CPU exception │
| │ } else if (frame->int_no < 48) { │
| │ irq_handler(frame); // Hardware IRQ │
| │ send_eoi(frame->int_no); // Acknowledge to PIC │
| │ } else { │
| │ software_int_handler(frame); // Software interrupt │
| │ } │
| │ } │
| │ │
| └─────────────────────────────────────────────────────────────────┘
| │
| │ Dispatch to specific handler
| ▼
| ┌─────────────────────────────────────────────────────────────────┐
| │ Specific Handlers │
| │ │
| │ exception_handlers[]: │
| │ [0] = divide_error_handler │
| │ [6] = invalid_opcode_handler │
| │ [13] = general_protection_handler │
| │ [14] = page_fault_handler │
| │ │
| │ irq_handlers[]: │
| │ [0] = timer_handler │
| │ [1] = keyboard_handler │
| │ │
| └─────────────────────────────────────────────────────────────────┘
| |
+-----------------------------------------------------------------------+
4.2 Key Components
+-----------------------------------------------------------------------+
| COMPONENT BREAKDOWN |
+-----------------------------------------------------------------------+
| |
| File Structure: |
| |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ isr.asm │ |
| │ - ISR stub macros (ISR_NOERR, ISR_ERR) │ |
| │ - 256 individual ISR entry points (isr0...isr255) │ |
| │ - Common ISR stub code (save state, call C, restore, iret) │ |
| │ - IRQ-specific stubs with PIC handling │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ idt.c / idt.h │ |
| │ - IDT entry structure (idt_entry_t) │ |
| │ - IDT pointer structure (idt_ptr_t) │ |
| │ - IDT array (idt_entries[256]) │ |
| │ - idt_set_gate() function to configure entries │ |
| │ - idt_init() function to set up all entries │ |
| │ - idt_load() assembly stub to execute LIDT │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ pic.c / pic.h │ |
| │ - PIC port definitions (0x20, 0x21, 0xA0, 0xA1) │ |
| │ - pic_remap() function │ |
| │ - pic_send_eoi() function │ |
| │ - pic_mask_irq() / pic_unmask_irq() functions │ |
| │ - IRQ_OFFSET constant (0x20) │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ handlers.c / handlers.h │ |
| │ - interrupt_frame structure definition │ |
| │ - isr_handler() - main C interrupt handler │ |
| │ - exception_handler() - handles CPU exceptions │ |
| │ - irq_handler() - handles hardware IRQs │ |
| │ - Individual handlers: timer_handler, keyboard_handler, etc. │ |
| │ - Exception names table for debug output │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ io.h (inline functions) │ |
| │ - outb(port, value) - write byte to I/O port │ |
| │ - inb(port) - read byte from I/O port │ |
| │ - io_wait() - short delay for PIC initialization │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
+-----------------------------------------------------------------------+
4.3 Data Structures
IDT Entry Structure
// IDT Entry (Gate Descriptor) - 8 bytes
typedef struct {
uint16_t offset_low; // Bits 0-15 of handler address
uint16_t selector; // Code segment selector (usually 0x08)
uint8_t zero; // Must be zero
uint8_t type_attr; // Type and attributes (see below)
uint16_t offset_high; // Bits 16-31 of handler address
} __attribute__((packed)) idt_entry_t;
// type_attr values:
// 0x8E = 10001110 = Present, Ring 0, 32-bit Interrupt Gate
// 0x8F = 10001111 = Present, Ring 0, 32-bit Trap Gate
// 0xEE = 11101110 = Present, Ring 3, 32-bit Interrupt Gate (user-callable)
IDT Pointer Structure
// IDT Pointer - 6 bytes (loaded with LIDT)
typedef struct {
uint16_t limit; // Size of IDT - 1 (256 * 8 - 1 = 2047 = 0x7FF)
uint32_t base; // Address of IDT
} __attribute__((packed)) idt_ptr_t;
Interrupt Frame Structure
// Matches the stack layout after ISR stub saves state
typedef struct {
// Pushed by ISR stub (in reverse order)
uint32_t gs, fs, es, ds; // Segment registers
uint32_t edi, esi, ebp, esp_dummy; // PUSHA (esp_dummy is useless)
uint32_t ebx, edx, ecx, eax; // PUSHA continued
uint32_t int_no, err_code; // Interrupt number and error code
// Pushed by CPU automatically
uint32_t eip, cs, eflags; // Return address and flags
uint32_t useresp, ss; // Only present on privilege change
} __attribute__((packed)) interrupt_frame_t;
4.4 Algorithm Overview
IDT Initialization Flow
+-----------------------------------------------------------------------+
| IDT INITIALIZATION SEQUENCE |
+-----------------------------------------------------------------------+
| |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ Step 1: Initialize all 256 IDT entries to default handler │ |
| │ │ |
| │ for (int i = 0; i < 256; i++) { │ |
| │ idt_set_gate(i, (uint32_t)default_handler, 0x08, 0x8E); │ |
| │ } │ |
| └────────────────────────────┬────────────────────────────────────┘ |
| │ |
| ▼ |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ Step 2: Set up exception handlers (vectors 0-31) │ |
| │ │ |
| │ idt_set_gate(0, (uint32_t)isr0, 0x08, 0x8E); // #DE │ |
| │ idt_set_gate(1, (uint32_t)isr1, 0x08, 0x8E); // #DB │ |
| │ ... │ |
| │ idt_set_gate(13, (uint32_t)isr13, 0x08, 0x8E); // #GP │ |
| │ idt_set_gate(14, (uint32_t)isr14, 0x08, 0x8E); // #PF │ |
| │ ... │ |
| └────────────────────────────┬────────────────────────────────────┘ |
| │ |
| ▼ |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ Step 3: Remap the 8259 PIC │ |
| │ │ |
| │ pic_remap(0x20, 0x28); │ |
| │ // Master PIC: IRQ0-7 → Vectors 0x20-0x27 │ |
| │ // Slave PIC: IRQ8-15 → Vectors 0x28-0x2F │ |
| └────────────────────────────┬────────────────────────────────────┘ |
| │ |
| ▼ |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ Step 4: Set up IRQ handlers (vectors 32-47) │ |
| │ │ |
| │ idt_set_gate(32, (uint32_t)irq0, 0x08, 0x8E); // Timer │ |
| │ idt_set_gate(33, (uint32_t)irq1, 0x08, 0x8E); // Keyboard │ |
| │ ... │ |
| │ idt_set_gate(47, (uint32_t)irq15, 0x08, 0x8E); // Secondary IDE│ |
| └────────────────────────────┬────────────────────────────────────┘ |
| │ |
| ▼ |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ Step 5: Set up software interrupt handlers (optional) │ |
| │ │ |
| │ idt_set_gate(0x80, (uint32_t)isr128, 0x08, 0xEE); // syscall │ |
| │ // Note: 0xEE allows Ring 3 to trigger this interrupt │ |
| └────────────────────────────┬────────────────────────────────────┘ |
| │ |
| ▼ |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ Step 6: Load the IDT │ |
| │ │ |
| │ idt_ptr.limit = sizeof(idt_entries) - 1; │ |
| │ idt_ptr.base = (uint32_t)&idt_entries; │ |
| │ idt_load((uint32_t)&idt_ptr); // LIDT instruction │ |
| └────────────────────────────┬────────────────────────────────────┘ |
| │ |
| ▼ |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ Step 7: Enable interrupts │ |
| │ │ |
| │ asm volatile ("sti"); // Set Interrupt Flag │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
+-----------------------------------------------------------------------+
PIC Remapping Sequence
+-----------------------------------------------------------------------+
| 8259 PIC REMAPPING SEQUENCE |
+-----------------------------------------------------------------------+
| |
| Why remap? |
| - Default: Master PIC IRQs 0-7 → Vectors 0x08-0x0F |
| - Problem: Vectors 0x08-0x0F are used by CPU exceptions! |
| - 0x08 = Double Fault (#DF) |
| - 0x0D = General Protection Fault (#GP) |
| - Solution: Remap IRQs to vectors 0x20-0x2F (32-47) |
| |
| Remapping sequence: |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ │ |
| │ // Save current masks │ |
| │ uint8_t mask1 = inb(PIC1_DATA); │ |
| │ uint8_t mask2 = inb(PIC2_DATA); │ |
| │ │ |
| │ // Start initialization sequence (ICW1) │ |
| │ outb(PIC1_COMMAND, 0x11); // ICW1: Initialize + ICW4 needed │ |
| │ io_wait(); │ |
| │ outb(PIC2_COMMAND, 0x11); // ICW1 for slave │ |
| │ io_wait(); │ |
| │ │ |
| │ // ICW2: Set vector offsets │ |
| │ outb(PIC1_DATA, 0x20); // Master: vectors 0x20-0x27 │ |
| │ io_wait(); │ |
| │ outb(PIC2_DATA, 0x28); // Slave: vectors 0x28-0x2F │ |
| │ io_wait(); │ |
| │ │ |
| │ // ICW3: Tell master about slave on IRQ2 │ |
| │ outb(PIC1_DATA, 0x04); // Slave on IR2 (bit 2 set) │ |
| │ io_wait(); │ |
| │ outb(PIC2_DATA, 0x02); // Slave cascade identity (2) │ |
| │ io_wait(); │ |
| │ │ |
| │ // ICW4: Set mode │ |
| │ outb(PIC1_DATA, 0x01); // 8086/88 mode │ |
| │ io_wait(); │ |
| │ outb(PIC2_DATA, 0x01); // 8086/88 mode │ |
| │ io_wait(); │ |
| │ │ |
| │ // Restore saved masks (or set new ones) │ |
| │ outb(PIC1_DATA, mask1); // Restore master mask │ |
| │ outb(PIC2_DATA, mask2); // Restore slave mask │ |
| │ │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| ICW (Initialization Command Word) breakdown: |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ ICW1 (0x11): │ |
| │ Bit 0 = 1: ICW4 needed │ |
| │ Bit 1 = 0: Cascade mode │ |
| │ Bit 4 = 1: Initialization │ |
| │ │ |
| │ ICW2: Vector offset (must be multiple of 8) │ |
| │ │ |
| │ ICW3: Cascade configuration │ |
| │ Master: Bit mask of IRQs with slaves (0x04 = IR2) │ |
| │ Slave: Slave ID (0x02 = cascade on IR2) │ |
| │ │ |
| │ ICW4 (0x01): │ |
| │ Bit 0 = 1: 8086/88 mode (vs MCS-80/85) │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
+-----------------------------------------------------------------------+
5. Implementation Guide
5.1 Development Environment Setup
Ensure you have the cross-compiler and tools from Project 6:
# Verify cross-compiler
i686-elf-gcc --version
# Verify NASM
nasm --version
# Verify QEMU
qemu-system-i386 --version
5.2 Project Structure
interrupt-kernel/
├── Makefile
├── linker.ld # Linker script (from Project 6)
├── boot.asm # Boot sector / multiboot header (from Project 6)
├── src/
│ ├── kernel.c # Main kernel entry
│ ├── idt.c # IDT management
│ ├── idt.h # IDT structures and function declarations
│ ├── isr.asm # ISR stubs in assembly
│ ├── pic.c # 8259 PIC programming
│ ├── pic.h # PIC port definitions and functions
│ ├── handlers.c # C interrupt handlers
│ ├── handlers.h # Handler declarations and interrupt_frame
│ ├── io.h # Port I/O inline functions
│ └── vga.c / vga.h # VGA text output (from Project 6)
└── README.md
5.3 The Core Question You’re Answering
“How does the CPU know where to go when an interrupt occurs, and how does your handler preserve the interrupted program’s state so it can resume correctly?”
This involves understanding:
- The IDT as a lookup table: interrupt number → handler address
- The automatic state saving by the CPU (EFLAGS, CS, EIP, possibly error code)
- The additional state saving by your ISR stub (general registers, segment registers)
- The return path via IRET (and how it differs from RET)
- The PIC’s role in hardware interrupt delivery
5.4 Concepts You Must Understand First
| Concept | Self-Assessment Question | Book/Reference |
|---|---|---|
| IDT Entry Format | Can you explain each field in an 8-byte IDT entry? | Intel SDM Vol 3, Ch 6 |
| Interrupt vs Trap Gates | What’s the difference? When would you use each? | OSDev Wiki - IDT |
| CPU Exception Types | What’s the difference between faults, traps, and aborts? | Intel SDM Vol 3, Ch 6 |
| Stack During Interrupt | What does the CPU push? What must you push? | Intel SDM Vol 3, Ch 6 |
| 8259 PIC | Why must you remap it? What’s EOI? | OSDev Wiki - PIC |
| IRET Instruction | How does it differ from RET? | Intel SDM Vol 2 |
5.5 Questions to Guide Your Design
IDT Design:
- How many entries do you need? (256 for all possible vectors)
- What selector value for all entries? (0x08 = kernel code segment)
- What type_attr for exception handlers vs IRQ handlers?
- How will you handle the fact that only some exceptions push error codes?
ISR Stub Design:
- How will you generate 256 ISR stubs without writing them manually?
- How will you pass the interrupt number to the C handler?
- How will you create a uniform stack frame despite some exceptions pushing error codes and others not?
- What must happen before calling C? After returning from C?
PIC Handling:
- To what vectors should you remap the PICs? (Usually 0x20-0x2F)
- When must you send EOI? To which PIC?
- Should you mask any IRQs initially?
Handler Design:
- How will you dispatch from the common handler to specific handlers?
- What information should exception handlers print?
- What should happen after handling an exception? (halt? continue? depends on type)
5.6 Thinking Exercise
Before coding, trace through what happens when the keyboard is pressed:
1. User presses 'A' key
2. Keyboard controller generates electrical signal
↓
3. Signal reaches 8259 master PIC on IR1 (IRQ1)
↓
4. PIC checks if IRQ1 is masked → if yes, ignore
↓
5. PIC asserts INT signal to CPU
↓
6. CPU checks IF flag → if 0, ignore until STI
↓
7. CPU finishes current instruction
↓
8. CPU acknowledges interrupt, PIC provides vector number
(After remapping: IRQ1 = vector 33)
↓
9. CPU saves state: EFLAGS, CS, EIP → stack
↓
10. CPU reads IDT entry at index 33
↓
11. CPU loads handler address from IDT entry
↓
12. CPU jumps to your ISR stub (irq1)
↓
13. Your stub:
- Push dummy error code (0)
- Push interrupt number (33)
- PUSHA
- Push segment registers
- Call isr_handler
↓
14. C handler:
- Check interrupt number (33 = IRQ1)
- Call keyboard_handler()
- Read scancode from port 0x60
- Send EOI to master PIC
- Return
↓
15. Your stub:
- Pop segment registers
- POPA
- ADD ESP, 8
- IRET
↓
16. CPU restores EIP, CS, EFLAGS from stack
↓
17. Interrupted program continues as if nothing happened
Questions to answer:
- At step 9, is the stack pointer the user’s stack or kernel’s stack? (Hint: depends on privilege level)
- At step 11, what happens if the IDT entry has Present=0?
- At step 15, why ADD ESP, 8? What values are being skipped?
- What happens if you forget the EOI at step 14?
5.7 Hints in Layers
Hint 1: Starting Point (Conceptual Direction)
You need:
- 256 ISR stubs - Assembly routines that save state and call C
- IDT with 256 entries - One for each possible interrupt vector
- PIC remapping - Move IRQs to vectors 32-47
- C handler - Dispatches to specific handlers based on interrupt number
- Specific handlers - Timer, keyboard, exceptions, etc.
The key insight: Use assembly macros to generate ISR stubs without manual repetition.
Hint 2: Next Level (More Specific Guidance)
ISR stub macro pattern (isr.asm):
; For exceptions that DON'T push error code
%macro ISR_NOERR 1
global isr%1
isr%1:
push dword 0 ; Dummy error code
push dword %1 ; Interrupt number
jmp isr_common
%endmacro
; For exceptions that DO push error code
%macro ISR_ERR 1
global isr%1
isr%1:
push dword %1 ; Interrupt number (error code already pushed)
jmp isr_common
%endmacro
; Generate stubs
ISR_NOERR 0 ; Divide Error
ISR_NOERR 1 ; Debug
; ...
ISR_ERR 8 ; Double Fault
; ...
ISR_ERR 13 ; General Protection Fault
ISR_ERR 14 ; Page Fault
; ...
IDT setup pattern (idt.c):
void idt_set_gate(uint8_t num, uint32_t handler, uint16_t sel, uint8_t flags) {
idt_entries[num].offset_low = handler & 0xFFFF;
idt_entries[num].selector = sel;
idt_entries[num].zero = 0;
idt_entries[num].type_attr = flags;
idt_entries[num].offset_high = (handler >> 16) & 0xFFFF;
}
Hint 3: Technical Details (Approach/Pseudocode)
Common ISR stub (isr.asm):
extern isr_handler
isr_common:
pusha ; Save EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI
mov ax, ds
push eax ; Save data segment
push word 0 ; Align (push es, fs, gs too if needed)
mov ax, 0x10 ; Load kernel data segment
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
push esp ; Pass pointer to stack as argument
call isr_handler
add esp, 4 ; Remove argument
pop eax ; Restore segment (reverse of above)
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
popa ; Restore general registers
add esp, 8 ; Remove error code and interrupt number
iret ; Return from interrupt
IRQ handler with EOI (handlers.c):
void irq_handler(interrupt_frame_t* frame) {
uint32_t irq_num = frame->int_no - 32; // IRQ number (0-15)
// Call specific handler if registered
if (irq_handlers[irq_num] != NULL) {
irq_handlers[irq_num](frame);
}
// Send EOI
if (irq_num >= 8) {
outb(PIC2_COMMAND, 0x20); // EOI to slave
}
outb(PIC1_COMMAND, 0x20); // EOI to master
}
Hint 4: Tools/Debugging (Verification Methods)
# Debug with QEMU monitor
qemu-system-i386 -kernel kernel.elf -monitor stdio
# In QEMU monitor:
info idt # Show IDT entries
info irq # Show IRQ state
info registers # Show CPU registers
# Debug with GDB
qemu-system-i386 -kernel kernel.elf -s -S &
gdb -ex "target remote :1234" \
-ex "set architecture i386" \
-ex "break isr_handler" \
-ex "continue"
# In GDB:
(gdb) x/10x $esp # Examine stack
(gdb) info registers
(gdb) print/x frame->int_no
# Test specific interrupts
(gdb) call *(void(*)())trigger_divide_by_zero
# Or in kernel: asm volatile("int $0");
5.8 The Interview Questions They’ll Ask
- “Explain the difference between interrupt gates and trap gates.”
- Interrupt gates: CPU automatically clears IF flag (disables interrupts)
- Trap gates: IF flag remains unchanged
- Use interrupt gates for hardware IRQs (prevent nesting)
- Use trap gates for exceptions (allow debugger intervention)
- “Why must the PIC be remapped?”
- Default vectors conflict with CPU exceptions
- Master PIC: IRQ0-7 → 0x08-0x0F overlaps with #DF, #TS, etc.
- Must move to unused range (typically 0x20+)
- Without remapping, timer interrupt looks like Double Fault!
- “What happens if you forget to send EOI?”
- PIC thinks you’re still handling the interrupt
- All IRQs at that priority or lower are blocked
- No more interrupts from that line until EOI
- System appears to freeze (no keyboard, timer stops)
- “Walk through what happens on a page fault.”
- CPU detects page not present (or permission error)
- CPU pushes error code (with specific bits for cause)
- CPU pushes EIP, CS, EFLAGS
- CPU reads IDT entry 14
- Jumps to handler
- Handler can: allocate page, map it, return (fault is “fixed”)
- Or: print error and halt (can’t recover)
- “How would you implement nested interrupts?”
- Allow higher-priority interrupts to preempt lower
- Re-enable interrupts in handler (careful!)
- Requires priority tracking
- Must save complete state on each nesting level
- EOI must be sent in correct order
- “What’s the difference between INT instruction and hardware IRQ?”
- INT is synchronous (happens at specific instruction)
- IRQ is asynchronous (can happen anytime)
- INT error code behavior depends on instruction
- Both use IDT for dispatch
- DPL in IDT controls who can trigger INT (not relevant for IRQs)
5.9 Books That Will Help
| Topic | Book | Chapter/Section |
|---|---|---|
| x86 Interrupts | Intel SDM Volume 3 | Chapter 6 |
| Exception Types | Intel SDM Volume 3 | Chapter 6.4-6.5 |
| IDT Format | Intel SDM Volume 3 | Chapter 6.10-6.11 |
| 8259 PIC | OSDev Wiki | PIC section |
| OS Interrupt Handling | “Operating Systems: Three Easy Pieces” | Chapter 33 (Mechanism: Limited Direct Execution) |
| Real-time Interrupts | “Making Embedded Systems” | Chapter 7 |
5.10 Implementation Phases
Phase 1: IDT Infrastructure (2-3 days)
- Create IDT entry structure
- Implement idt_set_gate() function
- Create idt_ptr structure
- Implement idt_load() assembly (LIDT)
- Verify IDT is loaded (QEMU monitor)
Phase 2: Exception ISR Stubs (3-4 days)
- Create ISR_NOERR and ISR_ERR macros
- Generate stubs for vectors 0-31
- Implement isr_common stub
- Create basic isr_handler in C
- Test with intentional divide-by-zero
Phase 3: PIC Remapping (1-2 days)
- Implement port I/O functions (inb, outb)
- Implement pic_remap()
- Implement pic_send_eoi()
- Verify with QEMU monitor (info irq)
Phase 4: IRQ Handling (2-3 days)
- Create IRQ stubs (irq0-irq15)
- Extend isr_handler for IRQ dispatch
- Implement timer_handler
- Verify timer interrupts working
Phase 5: Keyboard Handler (1-2 days)
- Implement keyboard_handler
- Read scancodes from port 0x60
- Display key presses
- Handle key release (bit 7)
Phase 6: Polish and Testing (2-3 days)
- Create comprehensive test suite
- Handle all common exceptions gracefully
- Add debug output (register dumps)
- Document everything
5.11 Key Implementation Decisions
| Decision | Option A | Option B | Recommendation |
|---|---|---|---|
| Gate type for exceptions | Interrupt Gate (0x8E) | Trap Gate (0x8F) | Trap Gate for most exceptions (allows debugging) |
| Gate type for IRQs | Interrupt Gate (0x8E) | Trap Gate (0x8F) | Interrupt Gate (auto-disable interrupts) |
| Stack for interrupt frame | Use current stack | Dedicated kernel stack | Current stack for now; dedicated for user mode |
| Error code handling | Check per-vector | Always push dummy | Always push dummy for uniform frame |
| EOI timing | Before handler | After handler | After handler (handler might need to read device) |
6. Testing Strategy
6.1 Unit Tests
// test_idt.c - Verify IDT setup
void test_idt_gate_format(void) {
// Set a test gate
idt_set_gate(42, 0xDEADBEEF, 0x08, 0x8E);
// Verify fields
assert(idt_entries[42].offset_low == 0xBEEF);
assert(idt_entries[42].offset_high == 0xDEAD);
assert(idt_entries[42].selector == 0x08);
assert(idt_entries[42].type_attr == 0x8E);
assert(idt_entries[42].zero == 0);
print("test_idt_gate_format: PASSED\n");
}
void test_all_gates_present(void) {
for (int i = 0; i < 256; i++) {
// Check Present bit is set
assert(idt_entries[i].type_attr & 0x80);
}
print("test_all_gates_present: PASSED\n");
}
6.2 Integration Tests
// test_interrupts.c
volatile int divide_error_count = 0;
volatile int timer_tick_count = 0;
volatile int keyboard_event_count = 0;
void test_divide_error(void) {
print("[TEST] Triggering divide by zero...\n");
divide_error_count = 0;
// Trigger divide error
volatile int a = 1;
volatile int b = 0;
volatile int c = a / b; // Should trigger #DE
(void)c;
// Handler should have been called
assert(divide_error_count == 1);
print("[TEST] Divide error: PASSED\n");
}
void test_timer_interrupts(void) {
print("[TEST] Waiting for 3 timer ticks...\n");
timer_tick_count = 0;
// Wait for 3 ticks
while (timer_tick_count < 3) {
asm volatile ("hlt"); // Halt until interrupt
}
print("[TEST] Timer interrupts: PASSED\n");
}
void test_software_interrupt(void) {
print("[TEST] Triggering INT 0x80...\n");
asm volatile ("int $0x80");
// Verify handler was called (check some flag)
print("[TEST] Software interrupt: PASSED\n");
}
6.3 Verification Commands
#!/bin/bash
# test_interrupts.sh
# Build
make clean && make
# Test 1: Kernel boots with IDT
echo "Test 1: Boot with IDT"
timeout 5 qemu-system-i386 -kernel kernel.elf -nographic &
PID=$!
sleep 2
if ps -p $PID > /dev/null; then
echo "PASS: Kernel running"
kill $PID
else
echo "FAIL: Kernel crashed"
exit 1
fi
# Test 2: Check IDT with QEMU monitor
echo "Test 2: IDT verification"
echo "info idt" | qemu-system-i386 -kernel kernel.elf -monitor stdio -nographic &
sleep 2
kill %1
# Test 3: Debug session
echo "Test 3: GDB verification"
qemu-system-i386 -kernel kernel.elf -s -S -nographic &
sleep 1
gdb -batch -ex "target remote :1234" \
-ex "break isr_handler" \
-ex "continue" \
-ex "info frame" || true
kill %1
echo "All tests completed"
6.4 GDB Debug Session for Interrupts
# Terminal 1: Start QEMU
qemu-system-i386 -kernel kernel.elf -s -S -nographic
# Terminal 2: GDB
gdb kernel.elf
(gdb) target remote :1234
(gdb) set architecture i386
# Break on interrupt handler entry
(gdb) break isr_handler
# Continue and trigger an interrupt (keyboard, timer, etc.)
(gdb) continue
# When it breaks, examine the interrupt frame
(gdb) print *((interrupt_frame_t*)$esp)
# See the interrupt number
(gdb) print frame->int_no
# Step through handler
(gdb) step
(gdb) next
# Examine IDT
(gdb) x/8bx &idt_entries[0] # First IDT entry
(gdb) x/8bx &idt_entries[14] # Page fault entry
7. Common Pitfalls & Debugging
Pitfall 1: Forgetting to Push Dummy Error Code
Symptom: Stack misaligned, C handler receives wrong values.
Cause: Some exceptions push error code, some don’t. If you don’t push a dummy for those that don’t, your stack frame is off by 4 bytes.
Fix:
; Exceptions WITHOUT error code need dummy
ISR_NOERR 0 ; Divide Error
ISR_NOERR 1 ; Debug
ISR_NOERR 2 ; NMI
; ... (most exceptions)
; Exceptions WITH error code - no dummy
ISR_ERR 8 ; Double Fault
ISR_ERR 10 ; Invalid TSS
ISR_ERR 11 ; Segment Not Present
ISR_ERR 12 ; Stack-Segment Fault
ISR_ERR 13 ; General Protection
ISR_ERR 14 ; Page Fault
ISR_ERR 17 ; Alignment Check
Exceptions that push error codes: 8, 10, 11, 12, 13, 14, 17, 21 (newer CPUs)
Pitfall 2: Not Remapping PIC
Symptom: Timer interrupt triggers “Double Fault” handler, or system immediately triple faults after STI.
Cause: Master PIC IRQ0 is vector 0x08, which is the Double Fault exception vector.
Fix: Always remap PIC before enabling interrupts:
void pic_remap(uint8_t offset1, uint8_t offset2) {
// offset1 typically 0x20, offset2 typically 0x28
// See implementation details in Section 4.4
}
// In init:
pic_remap(0x20, 0x28); // IRQs now at 0x20-0x2F
idt_init(); // Set up IDT
asm volatile ("sti"); // NOW safe to enable interrupts
Pitfall 3: Forgetting EOI
Symptom: First interrupt works, then no more interrupts ever.
Cause: PIC requires acknowledgment (EOI) before it will send more interrupts.
Fix:
void irq_handler(interrupt_frame_t* frame) {
uint32_t irq = frame->int_no - 32;
// Handle the IRQ...
// CRITICAL: Send EOI
if (irq >= 8) {
outb(0xA0, 0x20); // EOI to slave
}
outb(0x20, 0x20); // EOI to master (always)
}
Pitfall 4: Using RET Instead of IRET
Symptom: System crashes after first interrupt.
Cause: RET only pops EIP. IRET pops EIP, CS, and EFLAGS.
Fix:
isr_common:
; ... save state ...
call isr_handler
; ... restore state ...
add esp, 8 ; Remove error code and int number
iret ; NOT ret!
Pitfall 5: Wrong Segment Selector in IDT Entries
Symptom: Triple fault when interrupt occurs.
Cause: IDT entry points to wrong code segment or non-present segment.
Fix:
// Selector should be kernel code segment from GDT
#define KERNEL_CS 0x08
void idt_init(void) {
idt_set_gate(0, (uint32_t)isr0, KERNEL_CS, 0x8E);
// ...
}
Pitfall 6: Not Saving All Registers
Symptom: Interrupted code behaves strangely after interrupt.
Cause: Interrupt handler modified a register that the interrupted code was using.
Fix: Save ALL general purpose registers and segment registers:
isr_common:
pusha ; Save EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI
; Also save segment registers
push ds
push es
push fs
push gs
mov ax, 0x10 ; Load kernel data segment
mov ds, ax
mov es, ax
; ... call handler ...
pop gs ; Restore in reverse order
pop fs
pop es
pop ds
popa ; Restore general registers
Pitfall 7: Spurious IRQ7 / IRQ15
Symptom: Unexpected IRQ7 or IRQ15 with no apparent cause.
Cause: Electrical noise or race condition in PIC causes spurious interrupt.
Fix:
void irq7_handler(interrupt_frame_t* frame) {
// Check if this is a real IRQ7 or spurious
outb(0x20, 0x0B); // Read In-Service Register
if ((inb(0x20) & 0x80) == 0) {
// Spurious! Don't send EOI
return;
}
// Real IRQ7 - handle and send EOI
outb(0x20, 0x20);
}
Debugging with QEMU
# See interrupt activity
qemu-system-i386 -kernel kernel.elf -d int 2>&1 | head -100
# Get detailed CPU exceptions
qemu-system-i386 -kernel kernel.elf -d int,cpu_reset
# Interactive monitor
qemu-system-i386 -kernel kernel.elf -monitor stdio
(qemu) info idt # Show IDT
(qemu) info irq # Show IRQ activity
(qemu) info registers
8. Extensions & Challenges
Extension 1: Keyboard Driver with Scancode to ASCII (Medium)
// Extend keyboard handler to convert scancodes to characters
static char scancode_to_ascii[128] = {
0, 27, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\b',
'\t', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\n',
0, 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '`',
0, '\\', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', 0,
'*', 0, ' ', /* ... */
};
void keyboard_handler(interrupt_frame_t* frame) {
uint8_t scancode = inb(0x60);
if (!(scancode & 0x80)) { // Key press (not release)
char c = scancode_to_ascii[scancode];
if (c) {
print_char(c);
}
}
}
Extension 2: Stack Trace on Exception (Advanced)
void exception_handler(interrupt_frame_t* frame) {
print_exception_info(frame);
// Walk the stack for return addresses
print("Stack trace:\n");
uint32_t* ebp = (uint32_t*)frame->ebp;
for (int i = 0; i < 10 && ebp; i++) {
uint32_t eip = ebp[1]; // Return address
print(" [%d] 0x%x\n", i, eip);
ebp = (uint32_t*)ebp[0]; // Previous EBP
if ((uint32_t)ebp < 0x1000) break; // Sanity check
}
}
Extension 3: Programmable Interval Timer (PIT) Configuration (Medium)
// Configure PIT for specific frequency (default ~18.2 Hz)
#define PIT_FREQ 1193182 // Base frequency in Hz
void pit_set_frequency(uint32_t freq) {
uint16_t divisor = PIT_FREQ / freq;
outb(0x43, 0x36); // Channel 0, lobyte/hibyte, rate generator
outb(0x40, divisor & 0xFF); // Low byte
outb(0x40, (divisor >> 8) & 0xFF); // High byte
}
// In init:
pit_set_frequency(100); // 100 Hz = 10ms per tick
Extension 4: Nested Interrupt Support (Expert)
// Allow higher-priority interrupts during handling
void irq_handler_with_nesting(interrupt_frame_t* frame) {
uint32_t irq = frame->int_no - 32;
// Send EOI early (allows same-priority or lower to queue)
if (irq >= 8) outb(0xA0, 0x20);
outb(0x20, 0x20);
// Re-enable interrupts (DANGEROUS - must be careful!)
asm volatile ("sti");
// Now higher-priority IRQs can interrupt us
if (irq_handlers[irq]) {
irq_handlers[irq](frame);
}
// Disable interrupts before returning
asm volatile ("cli");
}
Extension 5: System Call Interface (Advanced)
// Implement INT 0x80 as system call entry
// IDT entry with DPL=3 so user code can call it
void syscall_handler(interrupt_frame_t* frame) {
uint32_t syscall_num = frame->eax;
uint32_t arg1 = frame->ebx;
uint32_t arg2 = frame->ecx;
uint32_t arg3 = frame->edx;
switch (syscall_num) {
case 1: // sys_write
frame->eax = sys_write(arg1, arg2, arg3);
break;
case 2: // sys_read
frame->eax = sys_read(arg1, arg2, arg3);
break;
// ... more syscalls ...
default:
frame->eax = -1; // Unknown syscall
}
}
9. Real-World Connections
9.1 How This Relates to Production Systems
| This Project | Real-World Equivalent |
|---|---|
| IDT setup | Linux arch/x86/kernel/idt.c |
| ISR stubs | Linux arch/x86/entry/entry_32.S |
| PIC handling | Linux arch/x86/kernel/i8259.c (legacy), APIC for modern |
| Exception handlers | Linux arch/x86/mm/fault.c (page fault), etc. |
| Timer interrupt | Linux kernel/time/clockevents.c |
| Keyboard driver | Linux drivers/input/keyboard/atkbd.c |
9.2 Industry Applications
+-----------------------------------------------------------------------+
| INTERRUPT HANDLING IN INDUSTRY |
+-----------------------------------------------------------------------+
| |
| Operating Systems: |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ Linux: IDT, APIC, softirqs, tasklets, workqueues │ |
| │ Windows: IDT, HAL, Interrupt Request Levels (IRQLs) │ |
| │ macOS: Mach interrupt handling, I/O Kit │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| Real-Time Systems: |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ - Deterministic interrupt latency (microseconds) │ |
| │ - Priority-based interrupt handling │ |
| │ - Hard deadlines (automotive, aerospace, medical) │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| Embedded Systems: |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ - Direct interrupt vector tables (ARM NVIC) │ |
| │ - Minimal latency for sensor processing │ |
| │ - Power management via interrupt-driven sleep │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| Security Research: |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ - SMM (System Management Mode) interrupts │ |
| │ - Rootkits hooking IDT entries │ |
| │ - Hypervisor interrupt virtualization │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
| Virtualization: |
| ┌─────────────────────────────────────────────────────────────────┐ |
| │ - Virtual APIC emulation │ |
| │ - Posted interrupts (Intel VT-x) │ |
| │ - Interrupt injection into guests │ |
| └─────────────────────────────────────────────────────────────────┘ |
| |
+-----------------------------------------------------------------------+
9.3 Career Relevance
Understanding interrupt handling is essential for:
- Kernel developers: All OS kernels handle interrupts
- Embedded engineers: Direct hardware interrupt handling
- Systems programmers: Understanding system call entry
- Security researchers: Rootkit analysis, vulnerability research
- Performance engineers: Interrupt latency optimization
- Hypervisor developers: Interrupt virtualization
10. Resources
10.1 Official Documentation
- Intel SDM Volume 3, Chapter 6 - Interrupt and Exception Handling
- Intel SDM Volume 2 - LIDT, IRET instruction reference
- 8259A Datasheet - PIC programming
10.2 OSDev Wiki Articles
- Interrupt Descriptor Table - IDT format and setup
- Interrupts - General interrupt concepts
- 8259 PIC - PIC programming guide
- Exceptions - CPU exception list
- I Can’t Get Interrupts Working - Troubleshooting
10.3 Tutorials
- James Molloy’s Kernel Tutorial - IRQs and the PIT
- BrokenThorn OS Dev - IDT
- OSDev Wiki - Getting Started
10.4 Source Code Examples
10.5 Books
| Book | Relevant Chapter |
|---|---|
| “Operating Systems: Three Easy Pieces” | Ch. 33 - Limited Direct Execution |
| “Intel SDM Volume 3” | Chapter 6 - Interrupt and Exception Handling |
| “Linux Kernel Development” by Love | Ch. 7 - Interrupts and Interrupt Handlers |
| “Making Embedded Systems” by White | Ch. 7 - Interrupts |
| “Write Great Code, Vol 2” by Hyde | Ch. 17 - Interrupt Processing |
11. Self-Assessment Checklist
Understanding
- I can explain each field in an 8-byte IDT entry
- I understand the difference between interrupt gates and trap gates
- I know which exceptions push error codes and which don’t
- I can trace through the complete interrupt handling flow
- I understand why PIC must be remapped
- I can explain what EOI is and why it’s necessary
- I know the difference between IRET and RET
Implementation
- My IDT has 256 valid entries
- ISR stubs correctly save and restore all registers
- PIC is remapped to non-conflicting vectors (0x20+)
- Exception handlers print useful debugging information
- Timer interrupt increments a counter
- Keyboard interrupt reads and displays scancodes
- All IRQs properly send EOI to PIC
Skills
- I can debug interrupt issues with QEMU monitor
- I can set breakpoints in interrupt handlers with GDB
- I understand the assembly syntax for ISR stubs
- I can add new interrupt handlers to my kernel
- I can explain my implementation to others
12. Submission / Completion Criteria
Your project is complete when:
12.1 Build Requirements
- Builds with
makeusing i686-elf-gcc and NASM - Produces a bootable kernel.elf or kernel.bin
- All source files are organized and documented
12.2 Functional Requirements
- IDT loaded: 256 entries, verified with
info idtin QEMU - Exceptions work: Divide by zero is caught and handled gracefully
- Timer works: IRQ0 fires regularly, tick count increments
- Keyboard works: IRQ1 fires on keypress, scancode displayed
- EOI works: Multiple interrupts can occur (not stuck after first)
12.3 Code Quality
- Clear separation: idt.c, isr.asm, pic.c, handlers.c
- Comments explain IDT entry format, PIC initialization sequence
- Exception names table for readable debug output
- Consistent coding style
12.4 Documentation
- README explains how to build and run
- Vector assignments documented (which vector = which handler)
- Known limitations noted
12.5 Verification Commands
# Build
make clean && make
# Run and observe output
qemu-system-i386 -kernel kernel.elf
# Verify IDT in QEMU monitor
(qemu) info idt
# Trigger divide by zero (in kernel code)
# Should see exception message, not crash
# Press keyboard keys
# Should see scancode output
# Watch timer ticks accumulate
12.6 Expected Output Example
================================================================================
x86 Kernel - Interrupt System v1.0
================================================================================
[INIT] IDT: 256 entries at 0x00001000
[INIT] PIC: Remapped to 0x20-0x2F
[INIT] Enabling interrupts... done.
[IRQ 0] Timer tick: 1
[IRQ 0] Timer tick: 2
[IRQ 0] Timer tick: 3
[KEYBOARD] Press any key...
[IRQ 1] Scancode: 0x1E (A pressed)
[IRQ 1] Scancode: 0x9E (A released)
[TEST] Triggering divide by zero...
[EXCEPTION 0] Divide Error (#DE)
EIP=0x00100234 CS=0x0008 EFLAGS=0x00000202
EAX=0x00000001 EBX=0x00000000 ECX=0x00000000 EDX=0x00000000
Error Code: 0x00000000
[Halted - cannot continue after #DE]
System halted after 47 timer ticks.
Congratulations! Completing this project means you understand one of the most fundamental aspects of operating system design. Your kernel can now:
- Respond to hardware events (keyboard, timer)
- Handle CPU exceptions gracefully
- Serve as a foundation for preemptive multitasking (Project 10)
- Serve as a foundation for system calls (user programs calling kernel)
You’ve built the nervous system of your operating system. Next up: Project 8 (Memory Management - Paging) to give your kernel virtual memory!