Introduction: Event-Driven Computing
Phase 5 Goals: By the end of this phase, your kernel will respond to hardware events via interrupts instead of polling. You'll have a working timer, proper exception handling, and interrupt-driven keyboard input. Your OS will tick 100 times per second.
Phase 0: Orientation & Big Picture
OS fundamentals, kernel architectures, learning path
Phase 1: How a Computer Starts
BIOS/UEFI, boot sequence, dev environment
Phase 2: Real Mode - First Steps
Real mode, bootloader, BIOS interrupts
Phase 3: Entering Protected Mode
GDT, 32-bit mode, C code execution
Phase 4: Display, Input & Output
VGA text mode, keyboard handling
6
Phase 5: Interrupts & CPU Control
IDT, ISRs, PIC programming
You Are Here
7
Phase 6: Memory Management
Paging, virtual memory, heap allocator
8
Phase 7: Disk Access & Filesystems
Block devices, FAT, VFS layer
9
Phase 8: Processes & User Mode
Task switching, system calls, user space
10
Phase 9: ELF Loading & Executables
ELF format, program loading
11
Phase 10: Standard Library & Shell
C library, command-line shell
12
Phase 11: 64-Bit Long Mode
x86-64, 64-bit paging, modern architecture
13
Phase 12: Modern Booting with UEFI
UEFI boot services, memory maps
14
Phase 13: Graphics & GUI Systems
Framebuffer, windowing, drawing
15
Phase 14: Advanced Input & Timing
Mouse, high-precision timers
16
Phase 15: Hardware Discovery & Drivers
PCI, device drivers, NVMe
17
Phase 16: Performance & Optimization
Caching, scheduler tuning
18
Phase 17: Stability, Security & Finishing
Debugging, hardening, completion
In Phase 4, we implemented keyboard input using polling—constantly checking if a key was pressed. This works but is incredibly wasteful: the CPU spins in a loop doing nothing useful while waiting. Interrupts solve this problem elegantly.
Imagine you're waiting for a pizza delivery. You could stand at the door checking every second (polling), or you could do something useful and let the doorbell notify you when it arrives (interrupts). Computers work the same way!
Key Insight: Interrupts are the foundation of responsive computing. Instead of constantly checking if something happened (polling), the CPU can do useful work and get notified instantly when an event occurs. This is how modern operating systems achieve multitasking.
Why Interrupts?
Without interrupts, your CPU would need to:
- Constantly check the keyboard for input (wasting cycles)
- Constantly check if disk operations completed
- Constantly check if network packets arrived
- Constantly check the timer for elapsed time
With interrupts, the CPU can execute your program and be interrupted when something happens. It's like the difference between constantly asking "are we there yet?" versus getting a notification when you arrive.
THE INTERRUPT WORKFLOW
═══════════════════════════════════════════════════════════════
1. CPU executes normal code
│
▼
2. Hardware event occurs (key pressed, timer tick, etc.)
│
▼
3. Device sends interrupt signal to CPU
│
▼
4. CPU immediately:
├── Saves current state (registers, flags, return address)
├── Looks up handler address in IDT
└── Jumps to interrupt handler
│
▼
5. Interrupt handler runs
├── Handles the event (read key, increment timer, etc.)
└── Signals "End of Interrupt" to hardware
│
▼
6. CPU restores state and resumes normal code
Types of Interrupts
x86 systems have three categories of interrupts:
INTERRUPT CLASSIFICATION
═══════════════════════════════════════════════════════════════
1. EXCEPTIONS (Vectors 0-31) - Synchronous, generated by CPU
├── FAULTS: Can be corrected (Page Fault #14)
│ → Handler runs, then RETRY the instruction
├── TRAPS: Intentional (Breakpoint #3, System Call)
│ → Handler runs, then CONTINUE to next instruction
└── ABORTS: Unrecoverable (Double Fault #8, Machine Check)
→ System must be halted/reset
2. HARDWARE INTERRUPTS (IRQs) - Asynchronous, from devices
├── IRQ 0: Timer (PIT) → Vector 32 (after remapping)
├── IRQ 1: Keyboard → Vector 33
├── IRQ 2: Cascade (PIC2) → Vector 34
├── IRQ 3: COM2 → Vector 35
├── IRQ 4: COM1 → Vector 36
├── IRQ 5: LPT2 / Sound → Vector 37
├── IRQ 6: Floppy → Vector 38
├── IRQ 7: LPT1 → Vector 39
├── IRQ 8: RTC → Vector 40
├── IRQ 9: ACPI → Vector 41
├── IRQ 10: Available → Vector 42
├── IRQ 11: Available → Vector 43
├── IRQ 12: Mouse → Vector 44
├── IRQ 13: FPU → Vector 45
├── IRQ 14: Primary ATA → Vector 46
└── IRQ 15: Secondary ATA → Vector 47
3. SOFTWARE INTERRUPTS - Triggered by INT instruction
└── INT 0x80: Linux system call convention
INT 0x21: DOS function calls (historically)
Conflict Alert! By default, the PIC maps IRQs 0-7 to vectors 8-15, which CONFLICT with CPU exception vectors! We must remap IRQs to vectors 32-47 before enabling interrupts.
Interrupt Descriptor Table (IDT)
The IDT is similar to the GDT we set up in Phase 3, but instead of describing memory segments, it describes where to find interrupt handlers. Each entry tells the CPU: "When interrupt N occurs, jump to address X."
IDT Structure
The IDT can have up to 256 entries (vectors 0-255). Each entry is 8 bytes in 32-bit protected mode:
IDT ENTRY FORMAT (32-bit Protected Mode)
═══════════════════════════════════════════════════════════════
Byte 7-6 Byte 5 Byte 4 Byte 3-2 Byte 1-0
┌────────────┬────────────┬────────────┬────────────┬────────────┐
│ Offset │ Attributes │ Zero │ Segment │ Offset │
│ 31:16 │ (P,DPL,T) │ (0x00) │ Selector │ 15:0 │
└────────────┴────────────┴────────────┴────────────┴────────────┘
Offset: 32-bit address of the interrupt handler (split in two)
Selector: Code segment selector (usually 0x08 for kernel code)
Attributes: Present bit, privilege level, gate type
ATTRIBUTE BYTE FORMAT
┌────┬─────────┬────┬─────────────┐
│ P │ DPL │ 0 │ Gate Type │
└────┴─────────┴────┴─────────────┘
Bit 7 6-5 4 3-0
P = Present (1 = valid entry)
DPL = Descriptor Privilege Level (0-3)
Gate Type = 0xE (32-bit interrupt gate) or 0xF (32-bit trap gate)
/* idt.h - IDT structures and declarations */
#ifndef IDT_H
#define IDT_H
#include <stdint.h>
/* IDT entry structure (32-bit protected mode) */
typedef struct {
uint16_t offset_low; /* Offset bits 0-15 of handler address */
uint16_t selector; /* Code segment selector in GDT */
uint8_t zero; /* Always 0 */
uint8_t type_attr; /* Type and attributes (P, DPL, type) */
uint16_t offset_high; /* Offset bits 16-31 of handler address */
} __attribute__((packed)) idt_entry_t;
/* IDTR register structure (loaded via LIDT instruction) */
typedef struct {
uint16_t limit; /* Table size - 1 */
uint32_t base; /* Table address */
} __attribute__((packed)) idt_ptr_t;
/* Number of IDT entries (256 possible interrupts) */
#define IDT_ENTRIES 256
/* Gate type/attribute definitions */
#define IDT_PRESENT 0x80 /* Present bit */
#define IDT_DPL_RING0 0x00 /* Kernel privilege */
#define IDT_DPL_RING3 0x60 /* User privilege */
#define IDT_INTERRUPT 0x0E /* 32-bit interrupt gate */
#define IDT_TRAP 0x0F /* 32-bit trap gate */
/* Common attribute combinations */
#define IDT_ATTR_KERNEL (IDT_PRESENT | IDT_DPL_RING0 | IDT_INTERRUPT) /* 0x8E */
#define IDT_ATTR_USER (IDT_PRESENT | IDT_DPL_RING3 | IDT_INTERRUPT) /* 0xEE */
#define IDT_ATTR_TRAP (IDT_PRESENT | IDT_DPL_RING0 | IDT_TRAP) /* 0x8F */
/* Function declarations */
void idt_init(void);
void idt_set_gate(uint8_t num, uint32_t handler, uint16_t selector, uint8_t flags);
#endif
Gate Types
The IDT supports different gate types that control interrupt behavior:
IDT GATE TYPES
═══════════════════════════════════════════════════════════════
INTERRUPT GATE (0x0E / 0x8E with Present+Ring0):
├── Automatically clears IF (Interrupt Flag) → disables interrupts
├── Used for hardware IRQs and most CPU exceptions
├── Prevents nested interrupts by default
└── You must re-enable interrupts explicitly with STI
TRAP GATE (0x0F / 0x8F with Present+Ring0):
├── Does NOT clear IF → interrupts remain enabled
├── Used for software interrupts (INT instruction)
├── Allows nested interrupts
└── Good for system calls (INT 0x80)
TASK GATE (0x05):
├── Causes a full task switch via TSS
├── Rarely used in modern OS designs
└── Can be used for Double Fault handling
What happens when an interrupt fires?
═══════════════════════════════════════════════════════════════
1. CPU pushes SS:ESP (if privilege change)
2. CPU pushes EFLAGS
3. CPU pushes CS:EIP (return address)
4. CPU pushes error code (for certain exceptions)
5. For interrupt gates: CPU clears IF (disables interrupts)
6. CPU loads new CS:EIP from IDT entry
7. Handler executes...
Interrupt vs Trap Gate: The key difference is whether interrupts are automatically disabled. For hardware IRQs, you usually want interrupt gates (auto-disable) to prevent the same interrupt from firing repeatedly while you're handling it.
IDT Implementation
Let's implement the IDT setup:
/* idt.c - IDT implementation */
#include "idt.h"
#include "string.h" /* memset */
/* The actual IDT array */
static idt_entry_t idt[IDT_ENTRIES];
/* IDTR pointer structure */
static idt_ptr_t idtp;
/* Set a single IDT entry */
void idt_set_gate(uint8_t num, uint32_t handler, uint16_t selector, uint8_t flags) {
idt[num].offset_low = handler & 0xFFFF;
idt[num].offset_high = (handler >> 16) & 0xFFFF;
idt[num].selector = selector;
idt[num].zero = 0;
idt[num].type_attr = flags;
}
/* External assembly ISR stubs (defined in isr.asm) */
extern void isr0(void); /* Division by Zero */
extern void isr1(void); /* Debug */
extern void isr2(void); /* NMI */
extern void isr3(void); /* Breakpoint */
extern void isr4(void); /* Overflow */
extern void isr5(void); /* Bounds Check */
extern void isr6(void); /* Invalid Opcode */
extern void isr7(void); /* No Coprocessor */
extern void isr8(void); /* Double Fault */
extern void isr9(void); /* Coprocessor Segment Overrun */
extern void isr10(void); /* Bad TSS */
extern void isr11(void); /* Segment Not Present */
extern void isr12(void); /* Stack Fault */
extern void isr13(void); /* General Protection Fault */
extern void isr14(void); /* Page Fault */
extern void isr15(void); /* Reserved */
extern void isr16(void); /* x87 FPU Error */
extern void isr17(void); /* Alignment Check */
extern void isr18(void); /* Machine Check */
extern void isr19(void); /* SIMD Exception */
/* ... isr20-31 are reserved by Intel */
/* External IRQ handlers (after PIC remapping) */
extern void irq0(void); /* Timer (PIT) */
extern void irq1(void); /* Keyboard */
extern void irq2(void); /* Cascade for slave PIC */
extern void irq3(void); /* COM2 */
extern void irq4(void); /* COM1 */
extern void irq5(void); /* LPT2 / Sound card */
extern void irq6(void); /* Floppy disk */
extern void irq7(void); /* LPT1 / Spurious */
extern void irq8(void); /* RTC */
extern void irq9(void); /* ACPI */
extern void irq10(void); /* Available */
extern void irq11(void); /* Available */
extern void irq12(void); /* Mouse */
extern void irq13(void); /* FPU */
extern void irq14(void); /* Primary ATA */
extern void irq15(void); /* Secondary ATA */
/* Initialize the IDT */
void idt_init(void) {
/* Set up IDTR */
idtp.limit = (sizeof(idt_entry_t) * IDT_ENTRIES) - 1;
idtp.base = (uint32_t)&idt;
/* Clear IDT first */
memset(&idt, 0, sizeof(idt));
/* Install CPU exception handlers (vectors 0-31) */
/* Selector 0x08 = kernel code segment from our GDT */
idt_set_gate(0, (uint32_t)isr0, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(1, (uint32_t)isr1, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(2, (uint32_t)isr2, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(3, (uint32_t)isr3, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(4, (uint32_t)isr4, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(5, (uint32_t)isr5, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(6, (uint32_t)isr6, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(7, (uint32_t)isr7, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(8, (uint32_t)isr8, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(9, (uint32_t)isr9, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(10, (uint32_t)isr10, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(11, (uint32_t)isr11, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(12, (uint32_t)isr12, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(13, (uint32_t)isr13, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(14, (uint32_t)isr14, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(15, (uint32_t)isr15, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(16, (uint32_t)isr16, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(17, (uint32_t)isr17, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(18, (uint32_t)isr18, 0x08, IDT_ATTR_KERNEL);
idt_set_gate(19, (uint32_t)isr19, 0x08, IDT_ATTR_KERNEL);
/* Vectors 20-31 reserved, set to isr_reserved if needed */
/* Install IRQ handlers (vectors 32-47 after PIC remapping) */
idt_set_gate(32, (uint32_t)irq0, 0x08, IDT_ATTR_KERNEL); /* Timer */
idt_set_gate(33, (uint32_t)irq1, 0x08, IDT_ATTR_KERNEL); /* Keyboard */
idt_set_gate(34, (uint32_t)irq2, 0x08, IDT_ATTR_KERNEL); /* Cascade */
idt_set_gate(35, (uint32_t)irq3, 0x08, IDT_ATTR_KERNEL); /* COM2 */
idt_set_gate(36, (uint32_t)irq4, 0x08, IDT_ATTR_KERNEL); /* COM1 */
idt_set_gate(37, (uint32_t)irq5, 0x08, IDT_ATTR_KERNEL); /* LPT2 */
idt_set_gate(38, (uint32_t)irq6, 0x08, IDT_ATTR_KERNEL); /* Floppy */
idt_set_gate(39, (uint32_t)irq7, 0x08, IDT_ATTR_KERNEL); /* LPT1 */
idt_set_gate(40, (uint32_t)irq8, 0x08, IDT_ATTR_KERNEL); /* RTC */
idt_set_gate(41, (uint32_t)irq9, 0x08, IDT_ATTR_KERNEL); /* ACPI */
idt_set_gate(42, (uint32_t)irq10, 0x08, IDT_ATTR_KERNEL); /* Available */
idt_set_gate(43, (uint32_t)irq11, 0x08, IDT_ATTR_KERNEL); /* Available */
idt_set_gate(44, (uint32_t)irq12, 0x08, IDT_ATTR_KERNEL); /* Mouse */
idt_set_gate(45, (uint32_t)irq13, 0x08, IDT_ATTR_KERNEL); /* FPU */
idt_set_gate(46, (uint32_t)irq14, 0x08, IDT_ATTR_KERNEL); /* ATA1 */
idt_set_gate(47, (uint32_t)irq15, 0x08, IDT_ATTR_KERNEL); /* ATA2 */
/* Load IDT - this makes it active */
__asm__ volatile("lidt %0" : : "m"(idtp));
}
Interrupt Service Routines
ISRs (Interrupt Service Routines) are the functions that handle interrupts. They must follow strict rules because they run in a special context—normal code was interrupted mid-execution!
ISR Structure
Each ISR must:
- Save the current CPU state (all registers)
- Handle the interrupt (your actual handler code)
- Restore the CPU state exactly as it was
- Return using IRET instead of normal RET
STACK STATE WHEN ISR BEGINS
═══════════════════════════════════════════════════════════════
If interrupt occurred in SAME privilege level (kernel → kernel):
┌─────────────────────────┐ ← Old ESP
│ Old EFLAGS │
├─────────────────────────┤
│ Old CS │
├─────────────────────────┤
│ Old EIP │ ← Return address
├─────────────────────────┤
│ Error Code (maybe) │ ← Only for some exceptions
└─────────────────────────┘ ← ESP when ISR starts
If interrupt occurred with privilege CHANGE (user → kernel):
┌─────────────────────────┐
│ Old SS │ ← User's stack segment
├─────────────────────────┤
│ Old ESP │ ← User's stack pointer
├─────────────────────────┤
│ Old EFLAGS │
├─────────────────────────┤
│ Old CS │ ← User's code segment
├─────────────────────────┤
│ Old EIP │
├─────────────────────────┤
│ Error Code (maybe) │
└─────────────────────────┘ ← ESP (now pointing to kernel stack)
Context Saving
We need to save ALL registers so we can restore the interrupted code perfectly. We'll create a structure to hold this context:
/* registers.h - CPU register state structure */
#ifndef REGISTERS_H
#define REGISTERS_H
#include <stdint.h>
/* Structure pushed by our ISR stub + what CPU pushes automatically */
typedef struct {
/* Pushed by our common stub (pusha-like) */
uint32_t ds; /* Data segment */
uint32_t edi, esi, ebp, esp_dummy; /* General registers */
uint32_t ebx, edx, ecx, eax; /* (esp_dummy ignored) */
/* Pushed by our ISR stub */
uint32_t int_no; /* Interrupt number */
uint32_t err_code; /* Error code (or 0) */
/* Pushed automatically by CPU */
uint32_t eip; /* Return address */
uint32_t cs; /* Code segment */
uint32_t eflags; /* CPU flags */
/* Pushed by CPU only on privilege change */
uint32_t useresp; /* User's ESP */
uint32_t ss; /* User's SS */
} __attribute__((packed)) registers_t;
#endif
Assembly ISR Stubs
We need assembly code for the low-level ISR entry points. Each ISR pushes its number and jumps to a common stub:
; isr.asm - Interrupt Service Routine stubs
; These are the actual entry points installed in the IDT
[BITS 32]
; External C handler
extern isr_handler
extern irq_handler
; Macro for ISRs that DON'T push an error code
%macro ISR_NOERRCODE 1
global isr%1
isr%1:
cli ; Disable interrupts
push dword 0 ; Push dummy error code
push dword %1 ; Push interrupt number
jmp isr_common_stub
%endmacro
; Macro for ISRs that DO push an error code
%macro ISR_ERRCODE 1
global isr%1
isr%1:
cli ; Disable interrupts
; Error code already pushed by CPU
push dword %1 ; Push interrupt number
jmp isr_common_stub
%endmacro
; Macro for IRQs (hardware interrupts)
%macro IRQ 2
global irq%1
irq%1:
cli
push dword 0 ; No error code for IRQs
push dword %2 ; Push interrupt number (32+)
jmp irq_common_stub
%endmacro
; CPU Exceptions (vectors 0-31)
ISR_NOERRCODE 0 ; #DE - Division by Zero
ISR_NOERRCODE 1 ; #DB - Debug
ISR_NOERRCODE 2 ; NMI - Non-Maskable Interrupt
ISR_NOERRCODE 3 ; #BP - Breakpoint
ISR_NOERRCODE 4 ; #OF - Overflow
ISR_NOERRCODE 5 ; #BR - Bound Range Exceeded
ISR_NOERRCODE 6 ; #UD - Invalid Opcode
ISR_NOERRCODE 7 ; #NM - No Math Coprocessor
ISR_ERRCODE 8 ; #DF - Double Fault (error code = 0 always)
ISR_NOERRCODE 9 ; Coprocessor Segment Overrun (obsolete)
ISR_ERRCODE 10 ; #TS - Invalid TSS
ISR_ERRCODE 11 ; #NP - Segment Not Present
ISR_ERRCODE 12 ; #SS - Stack Segment Fault
ISR_ERRCODE 13 ; #GP - General Protection Fault
ISR_ERRCODE 14 ; #PF - Page Fault
ISR_NOERRCODE 15 ; Reserved
ISR_NOERRCODE 16 ; #MF - x87 FPU Error
ISR_ERRCODE 17 ; #AC - Alignment Check
ISR_NOERRCODE 18 ; #MC - Machine Check
ISR_NOERRCODE 19 ; #XM - SIMD Exception
; 20-31 reserved...
; Hardware IRQs (mapped to vectors 32-47)
IRQ 0, 32 ; Timer (PIT)
IRQ 1, 33 ; Keyboard
IRQ 2, 34 ; Cascade for slave PIC
IRQ 3, 35 ; COM2
IRQ 4, 36 ; COM1
IRQ 5, 37 ; LPT2
IRQ 6, 38 ; Floppy
IRQ 7, 39 ; LPT1 / Spurious
IRQ 8, 40 ; RTC
IRQ 9, 41 ; ACPI
IRQ 10, 42 ; Available
IRQ 11, 43 ; Available
IRQ 12, 44 ; Mouse
IRQ 13, 45 ; FPU
IRQ 14, 46 ; Primary ATA
IRQ 15, 47 ; Secondary ATA
; Common ISR stub - saves CPU state, calls C handler
isr_common_stub:
; Save all general-purpose registers
pusha
; Save data segment
mov ax, ds
push eax
; Load kernel data segment
mov ax, 0x10 ; Kernel data segment selector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
; Push pointer to register structure (stack pointer)
push esp
; Call C handler: isr_handler(registers_t* regs)
call isr_handler
; Remove pushed pointer
add esp, 4
; Restore data segment
pop eax
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
; Restore general-purpose registers
popa
; Clean up pushed error code and interrupt number
add esp, 8
; Return from interrupt (pops EIP, CS, EFLAGS, and possibly ESP, SS)
iret
; Common IRQ stub - similar but calls irq_handler
irq_common_stub:
pusha
mov ax, ds
push eax
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
push esp
call irq_handler
add esp, 4
pop eax
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
popa
add esp, 8
iret
Error Code Complexity: Some exceptions push an error code automatically, others don't. Our macros handle this by pushing a dummy 0 when needed. The Page Fault (#14) error code contains valuable information about what went wrong!
CPU Exceptions
CPU exceptions are interrupts generated by the processor itself when something goes wrong (or intentionally, like breakpoints). Handling them properly is crucial for a stable OS.
Exception Types
x86 CPU EXCEPTIONS (Vectors 0-31)
═══════════════════════════════════════════════════════════════
Vec │ Name │ Type │ Err? │ Description
────┼────────────────────────┼────────┼──────┼─────────────────────
0 │ #DE Division Error │ Fault │ No │ DIV/IDIV by zero
1 │ #DB Debug │ F/Trap │ No │ Debug exception
2 │ NMI │ Int │ No │ Non-Maskable Interrupt
3 │ #BP Breakpoint │ Trap │ No │ INT 3 instruction
4 │ #OF Overflow │ Trap │ No │ INTO instruction
5 │ #BR Bound Range │ Fault │ No │ BOUND instruction
6 │ #UD Invalid Opcode │ Fault │ No │ Unknown instruction
7 │ #NM No FPU │ Fault │ No │ FPU not available
8 │ #DF Double Fault │ Abort │ Yes* │ Exception during exc.
9 │ (obsolete) │ - │ No │ Coprocessor overrun
10 │ #TS Invalid TSS │ Fault │ Yes │ Task switch failed
11 │ #NP Segment Not Pres. │ Fault │ Yes │ Segment not in memory
12 │ #SS Stack Fault │ Fault │ Yes │ Stack segment issue
13 │ #GP General Protection │ Fault │ Yes │ Memory/privilege error
14 │ #PF Page Fault │ Fault │ Yes │ Page not present/etc.
15 │ (reserved) │ - │ - │ Intel reserved
16 │ #MF x87 FPU Error │ Fault │ No │ Math error
17 │ #AC Alignment Check │ Fault │ Yes │ Unaligned access
18 │ #MC Machine Check │ Abort │ No │ Hardware error
19 │ #XM SIMD Exception │ Fault │ No │ SSE/SSE2/etc. error
20-31│ (reserved) │ - │ - │ Intel reserved
*Double Fault error code is always 0
Implementing Exception Handlers
Here's a complete C handler for CPU exceptions:
/* isr.c - Interrupt Service Routine handlers */
#include "idt.h"
#include "registers.h"
#include "terminal.h"
#include "io.h"
/* Human-readable exception messages */
static const char* exception_messages[32] = {
"Division By Zero",
"Debug",
"Non Maskable Interrupt",
"Breakpoint",
"Into Detected Overflow",
"Out of Bounds",
"Invalid Opcode",
"No Coprocessor (Device Not Available)",
"Double Fault",
"Coprocessor Segment Overrun",
"Bad TSS",
"Segment Not Present",
"Stack Fault",
"General Protection Fault",
"Page Fault",
"Unknown Interrupt (Reserved)",
"x87 FPU Error",
"Alignment Check",
"Machine Check",
"SIMD Floating-Point Exception",
"Virtualization Exception",
"Control Protection Exception",
"Reserved", "Reserved", "Reserved", "Reserved",
"Reserved", "Reserved",
"Hypervisor Injection Exception",
"VMM Communication Exception",
"Security Exception",
"Reserved"
};
/* Display register state on exception */
static void dump_registers(registers_t* regs) {
kprintf("\n=== REGISTER DUMP ===\n");
kprintf("EAX=%08X EBX=%08X ECX=%08X EDX=%08X\n",
regs->eax, regs->ebx, regs->ecx, regs->edx);
kprintf("ESI=%08X EDI=%08X EBP=%08X ESP=%08X\n",
regs->esi, regs->edi, regs->ebp, regs->useresp);
kprintf("EIP=%08X CS=%04X DS=%04X EFLAGS=%08X\n",
regs->eip, regs->cs & 0xFFFF, regs->ds & 0xFFFF, regs->eflags);
}
/* Main ISR handler - called from assembly stub */
void isr_handler(registers_t* regs) {
/* Only handle CPU exceptions (0-31) here */
if (regs->int_no < 32) {
/* Change to red for error display */
terminal_set_color(vga_make_color(VGA_LIGHT_RED, VGA_BLACK));
kprintf("\n╔══════════════════════════════════════════╗\n");
kprintf("║ KERNEL PANIC! ║\n");
kprintf("╚══════════════════════════════════════════╝\n");
kprintf("Exception: %s (INT %d)\n",
exception_messages[regs->int_no], regs->int_no);
if (regs->int_no == 14) {
/* Page fault - read CR2 for faulting address */
uint32_t faulting_address;
__asm__ volatile("mov %%cr2, %0" : "=r"(faulting_address));
kprintf("Faulting Address: 0x%08X\n", faulting_address);
kprintf("Error Code: 0x%X\n", regs->err_code);
kprintf(" %s | %s | %s\n",
(regs->err_code & 1) ? "Protection" : "Not Present",
(regs->err_code & 2) ? "Write" : "Read",
(regs->err_code & 4) ? "User" : "Kernel");
} else if (regs->err_code != 0) {
kprintf("Error Code: 0x%X\n", regs->err_code);
}
dump_registers(regs);
kprintf("\nSystem Halted.\n");
/* Disable interrupts and halt forever */
__asm__ volatile("cli");
for (;;) {
__asm__ volatile("hlt");
}
}
}
Understanding Error Codes
The Page Fault (#14) error code is particularly useful for debugging:
PAGE FAULT ERROR CODE FORMAT
═══════════════════════════════════════════════════════════════
Bit │ Name │ Meaning when SET
────┼──────┼─────────────────────────────────────────
0 │ P │ Page-level protection violation (vs. not present)
1 │ W/R │ Write access caused fault (vs. read)
2 │ U/S │ User-mode access (vs. supervisor/kernel)
3 │ RSVD │ Reserved bit set in page table
4 │ I/D │ Instruction fetch (vs. data access)
5 │ PK │ Protection key violation
6 │ SS │ Shadow stack access
7 │ HLAT │ HLAT paging fault
15 │ SGX │ SGX violation
EXAMPLE INTERPRETATIONS:
Error Code 0x00: Page not present, read, kernel
Error Code 0x02: Page not present, write, kernel
Error Code 0x04: Page not present, read, user
Error Code 0x05: Page present but protected, read, user
Error Code 0x07: Page present but protected, write, user
The faulting linear address is stored in CR2!
General Protection Fault (GPF) Causes
GPF (#13) is the "catch-all" for privilege violations. Common causes:
- Writing to read-only memory
- Accessing kernel memory from user mode
- Loading invalid segment selectors
- Executing privileged instructions from Ring 3
- Exceeding segment limits
Debugging
Programming the PIC
The Programmable Interrupt Controller (PIC) is a chip that manages hardware interrupts. The original IBM PC used the Intel 8259A, and modern systems emulate it for compatibility.
Why We Need the PIC
HARDWARE INTERRUPT FLOW
═══════════════════════════════════════════════════════════════
┌─────────────┐
IRQ0 ──────>│ │
(Timer)IRQ1 ──────>│ Master │ ┌─────────────┐
(Keyboard)IRQ2 ◄─────│ PIC │──────────>│ CPU │
IRQ3 ──────>│ (0x20-21) │ INTR │ │
IRQ4 ──────>│ │ │ Processes │
IRQ5 ──────>│ │ │ IDT[n] │
IRQ6 ──────>└─────────────┘ └─────────────┘
IRQ7 ──────> │
│ CASCADE (IRQ2)
IRQ8 ──────>┌─────┴───────┐
(RTC) IRQ9 ──────>│ │
IRQ10 ──────>│ Slave │
IRQ11 ──────>│ PIC │
(PS/2) IRQ12 ──────>│ (0xA0-A1) │
(FPU) IRQ13 ──────>│ │
(ATA1) IRQ14 ──────>│ │
(ATA2) IRQ15 ──────>└─────────────┘
Master PIC: IRQ 0-7 → Vectors 32-39 (after remapping)
Slave PIC: IRQ 8-15 → Vectors 40-47 (after remapping)
Why Remap? By default, the PIC maps IRQ 0-7 to vectors 8-15, which conflicts with CPU exceptions (8 = Double Fault, 13 = GPF, 14 = Page Fault). We must remap them to vectors 32+ where there's no conflict.
IRQ Remapping
The PIC is initialized by sending a sequence of Initialization Command Words (ICWs):
PIC INITIALIZATION SEQUENCE
═══════════════════════════════════════════════════════════════
ICW1 ICW2 ICW3 ICW4
┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
│ Trigger │ │ Vector │ │ Cascade │ │ Mode │
│ Mode │ -> │ Offset │ -> │ Config │ -> │ Settings │
│ Init Bit │ │ (32/40) │ │ (IRQ2) │ │ (8086) │
└───────────┘ └───────────┘ └───────────┘ └───────────┘
ICW1 (0x11): Start init, expect ICW4
ICW2: Base vector (Master=0x20/32, Slave=0x28/40)
ICW3: Cascade configuration
ICW4 (0x01): 8086/88 mode
/* pic.c - 8259 Programmable Interrupt Controller */
#include "io.h"
/* PIC I/O Ports */
#define PIC1_COMMAND 0x20 /* Master PIC command port */
#define PIC1_DATA 0x21 /* Master PIC data port */
#define PIC2_COMMAND 0xA0 /* Slave PIC command port */
#define PIC2_DATA 0xA1 /* Slave PIC data port */
/* PIC Commands */
#define PIC_EOI 0x20 /* End of Interrupt command */
/* ICW1 - Initialization Command Word 1 */
#define ICW1_ICW4 0x01 /* ICW4 needed */
#define ICW1_INIT 0x10 /* Initialization command */
/* ICW4 - Initialization Command Word 4 */
#define ICW4_8086 0x01 /* 8086/88 mode */
/**
* Remap the PIC to avoid conflicts with CPU exceptions
*
* By default:
* Master PIC: IRQ 0-7 → vectors 8-15 (CONFLICT!)
* Slave PIC: IRQ 8-15 → vectors 112-127
*
* After remapping:
* Master PIC: IRQ 0-7 → vectors 32-39
* Slave PIC: IRQ 8-15 → vectors 40-47
*/
void pic_remap(void) {
/* Save existing masks */
uint8_t mask1 = inb(PIC1_DATA);
uint8_t mask2 = inb(PIC2_DATA);
/* Start initialization sequence (cascade mode) */
outb(PIC1_COMMAND, ICW1_INIT | ICW1_ICW4);
io_wait();
outb(PIC2_COMMAND, ICW1_INIT | ICW1_ICW4);
io_wait();
/* ICW2: Set vector offsets */
outb(PIC1_DATA, 0x20); /* Master: IRQ 0-7 → INT 32-39 */
io_wait();
outb(PIC2_DATA, 0x28); /* Slave: IRQ 8-15 → INT 40-47 */
io_wait();
/* ICW3: Configure cascade wiring */
outb(PIC1_DATA, 0x04); /* Master: Slave on IRQ2 (bit 2) */
io_wait();
outb(PIC2_DATA, 0x02); /* Slave: Cascade identity = 2 */
io_wait();
/* ICW4: Set 8086 mode */
outb(PIC1_DATA, ICW4_8086);
io_wait();
outb(PIC2_DATA, ICW4_8086);
io_wait();
/* Restore saved masks */
outb(PIC1_DATA, mask1);
outb(PIC2_DATA, mask2);
}
End of Interrupt (EOI)
After handling any IRQ, you must send an EOI command to the PIC, or it won't deliver any more interrupts of that priority:
/**
* Send End-of-Interrupt signal to PIC
* MUST be called at end of every IRQ handler!
*/
void pic_send_eoi(uint8_t irq) {
/* If this came from the slave PIC, send EOI to both */
if (irq >= 8) {
outb(PIC2_COMMAND, PIC_EOI);
}
/* Always send EOI to master */
outb(PIC1_COMMAND, PIC_EOI);
}
/**
* Mask (disable) a specific IRQ
*/
void pic_set_mask(uint8_t irq) {
uint16_t port;
uint8_t value;
if (irq < 8) {
port = PIC1_DATA;
} else {
port = PIC2_DATA;
irq -= 8;
}
value = inb(port) | (1 << irq);
outb(port, value);
}
/**
* Unmask (enable) a specific IRQ
*/
void pic_clear_mask(uint8_t irq) {
uint16_t port;
uint8_t value;
if (irq < 8) {
port = PIC1_DATA;
} else {
port = PIC2_DATA;
irq -= 8;
}
value = inb(port) & ~(1 << irq);
outb(port, value);
}
Critical: Forgetting EOI
If you forget to send EOI:
- The PIC thinks you're still handling that interrupt
- No more interrupts of that priority or lower will fire
- Your timer/keyboard will appear "frozen"
- For slave PIC IRQs (8-15), you must send EOI to BOTH PICs
Common Bug
Programmable Interval Timer (PIT)
The PIT (Intel 8253/8254) is a hardware timer that can generate periodic interrupts. It's essential for time-keeping, scheduling, and implementing sleep functions.
Understanding the PIT
PIT ARCHITECTURE
═══════════════════════════════════════════════════════════════
┌───────────────────────────────────────────────────────────────┐
│ 8253/8254 PIT Chip │
├───────────┬───────────┬───────────┬───────────────────────────┤
│ │ │ │ │
│ Channel 0 │ Channel 1 │ Channel 2 │ Common Control │
│ │ │ │ │
│ IRQ 0 │ DRAM │ PC │ Base Frequency: │
│ (Timer) │ Refresh │ Speaker │ 1,193,182 Hz │
│ │ (Legacy) │ │ │
├───────────┴───────────┴───────────┤ Output = Base / Div │
│ Port 0x40 Port 0x41 Port 0x42 │ Command Port: 0x43 │
└───────────────────────────────────┴───────────────────────────┘
WHY 1,193,182 Hz?
─────────────────────────
The original IBM PC used a 14.31818 MHz crystal (for NTSC video).
This was divided by 12 for the PIT: 14,318,180 / 12 = 1,193,182
COMMON DIVISORS:
─────────────────────────
Divisor │ Frequency │ Period │ Use Case
───────────┼────────────┼────────────┼──────────────────
1193 │ ~1000 Hz │ ~1 ms │ High precision
11932 │ 100 Hz │ 10 ms │ Typical OS tick
47727 │ 25 Hz │ 40 ms │ Retro gaming
119318 │ 10 Hz │ 100 ms │ Battery saving
Choosing a Tick Rate: 100 Hz (10ms periods) is a good balance. Higher rates give finer scheduling granularity but increase interrupt overhead. Linux defaults to 250 Hz or 1000 Hz depending on configuration.
Configuring the Timer
/* timer.c - Programmable Interval Timer driver */
#include "io.h"
#include "pic.h"
#include "registers.h"
/* PIT I/O Ports */
#define PIT_CHANNEL0 0x40 /* Channel 0 data port */
#define PIT_CHANNEL1 0x41 /* Channel 1 data port */
#define PIT_CHANNEL2 0x42 /* Channel 2 data port */
#define PIT_COMMAND 0x43 /* Mode/Command register */
/* PIT Command Register Bits */
#define PIT_SEL_CH0 0x00 /* Select channel 0 */
#define PIT_SEL_CH1 0x40 /* Select channel 1 */
#define PIT_SEL_CH2 0x80 /* Select channel 2 */
#define PIT_ACCESS_LH 0x30 /* Access: lobyte then hibyte */
#define PIT_MODE2 0x04 /* Mode 2: rate generator */
#define PIT_MODE3 0x06 /* Mode 3: square wave */
#define PIT_BINARY 0x00 /* Binary counting (vs BCD) */
/* Base frequency of the PIT */
#define PIT_BASE_FREQ 1193182
/* Our desired timer frequency */
#define TIMER_FREQ_HZ 100 /* 100 Hz = 10ms periods */
/* Global tick counter (volatile because modified in ISR) */
static volatile uint32_t timer_ticks = 0;
/**
* Initialize the PIT for periodic interrupts
*
* Command byte breakdown for 0x36:
* Bits 7-6: 00 = Channel 0
* Bits 5-4: 11 = Access lobyte/hibyte
* Bits 3-1: 011 = Mode 3 (square wave)
* Bit 0: 0 = Binary counting
*/
void timer_init(uint32_t frequency) {
/* Calculate divisor for desired frequency */
uint32_t divisor = PIT_BASE_FREQ / frequency;
/* Clamp divisor to 16-bit range */
if (divisor > 0xFFFF) {
divisor = 0xFFFF;
}
if (divisor < 1) {
divisor = 1;
}
/* Send command byte: Channel 0, lobyte/hibyte, square wave, binary */
outb(PIT_COMMAND, PIT_SEL_CH0 | PIT_ACCESS_LH | PIT_MODE3 | PIT_BINARY);
/* Send divisor (low byte first, then high byte) */
outb(PIT_CHANNEL0, (uint8_t)(divisor & 0xFF));
outb(PIT_CHANNEL0, (uint8_t)((divisor >> 8) & 0xFF));
/* Enable IRQ 0 */
pic_clear_mask(0);
}
/**
* Get current tick count
*/
uint32_t timer_get_ticks(void) {
return timer_ticks;
}
/**
* Sleep for a given number of ticks
*/
void timer_sleep(uint32_t ticks) {
uint32_t end_ticks = timer_ticks + ticks;
while (timer_ticks < end_ticks) {
/* Wait - could use HLT for power saving */
__asm__ volatile("hlt");
}
}
/**
* Get uptime in seconds
*/
uint32_t timer_uptime_seconds(void) {
return timer_ticks / TIMER_FREQ_HZ;
}
Timer IRQ Handler
The timer handler is called every tick (e.g., 100 times per second at 100 Hz):
/**
* Timer interrupt handler (IRQ 0 → INT 32)
* Called from irq_handler dispatch
*/
void timer_handler(registers_t* regs) {
timer_ticks++;
/* Periodic tasks could go here:
* - Update system time
* - Check for sleeping processes to wake
* - Trigger scheduler (preemptive multitasking)
*/
/* Note: EOI is sent by irq_handler after this returns */
}
/**
* Main IRQ dispatcher (called from irq_common_stub)
*/
void irq_handler(registers_t* regs) {
/* Calculate IRQ number from interrupt number */
uint8_t irq = regs->int_no - 32;
/* Dispatch to specific handler */
switch (irq) {
case 0: /* Timer */
timer_handler(regs);
break;
case 1: /* Keyboard */
keyboard_handler(regs);
break;
/* Add more IRQ handlers here */
}
/* Send End-of-Interrupt to PIC */
pic_send_eoi(irq);
}
Implementing a Simple Clock
Once you have timer interrupts working, you can display a running clock:
void display_uptime(void) {
uint32_t total = timer_get_ticks() / 100; /* Assuming 100 Hz */
uint32_t hours = total / 3600;
uint32_t minutes = (total % 3600) / 60;
uint32_t seconds = total % 60;
/* Display at fixed screen position */
terminal_set_cursor(70, 0);
kprintf("%02d:%02d:%02d", hours, minutes, seconds);
}
Exercise
Keyboard IRQ Handler
In Phase 4, we used polling to read keyboard input. Now we'll upgrade to interrupt-driven input, which is more efficient and responsive.
POLLING vs INTERRUPT-DRIVEN I/O
═══════════════════════════════════════════════════════════════
POLLING (What we did in Phase 4):
┌──────────────────────────────────────────────────────────────┐
│ while (true) { │
│ if (keyboard_data_available()) { <- Wastes CPU cycles │
│ process_key(); checking constantly│
│ } │
│ } │
└──────────────────────────────────────────────────────────────┘
INTERRUPT-DRIVEN (What we do now):
┌──────────────────────────────────────────────────────────────┐
│ CPU does useful work... │
│ CPU does useful work... │
│ [USER PRESSES KEY] │
│ CPU jumps to keyboard_handler() ← Hardware notifies us! │
│ keyboard_handler() stores key ─>│ Buffer │ │
│ CPU resumes useful work... │
└──────────────────────────────────────────────────────────────┘
Circular Buffer
We need a buffer to store keypresses until the program is ready to read them. A circular (ring) buffer is perfect:
/* keyboard.c - Interrupt-driven keyboard driver */
#include "io.h"
#include "pic.h"
#include "registers.h"
/* PS/2 Keyboard Ports */
#define KEYBOARD_DATA_PORT 0x60
#define KEYBOARD_STATUS_PORT 0x64
/* Circular buffer for keyboard input */
#define KEYBOARD_BUFFER_SIZE 256
static volatile char keyboard_buffer[KEYBOARD_BUFFER_SIZE];
static volatile uint8_t buffer_head = 0; /* Write position */
static volatile uint8_t buffer_tail = 0; /* Read position */
/* Modifier key states */
static volatile uint8_t shift_pressed = 0;
static volatile uint8_t ctrl_pressed = 0;
static volatile uint8_t alt_pressed = 0;
/* Scan code to ASCII lookup tables (US QWERTY) */
static const char scancode_normal[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, ' ', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
/* ... function keys, etc ... */
};
static const char scancode_shifted[128] = {
0, 27, '!','@','#','$','%','^','&','*','(',')','_','+','\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, ' ', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
/**
* Add a character to the keyboard buffer
*/
static void buffer_add(char c) {
uint8_t next = (buffer_head + 1) % KEYBOARD_BUFFER_SIZE;
/* Only add if buffer isn't full */
if (next != buffer_tail) {
keyboard_buffer[buffer_head] = c;
buffer_head = next;
}
/* If full, keypress is dropped (you could beep here) */
}
/**
* Check if keyboard buffer has data
*/
int keyboard_has_data(void) {
return buffer_head != buffer_tail;
}
/**
* Read one character from keyboard buffer (blocking)
*/
char keyboard_getchar(void) {
/* Wait for data */
while (!keyboard_has_data()) {
__asm__ volatile("hlt"); /* Sleep until next interrupt */
}
char c = keyboard_buffer[buffer_tail];
buffer_tail = (buffer_tail + 1) % KEYBOARD_BUFFER_SIZE;
return c;
}
/**
* Keyboard interrupt handler (IRQ 1 → INT 33)
*/
void keyboard_handler(registers_t* regs) {
/* Read the scancode */
uint8_t scancode = inb(KEYBOARD_DATA_PORT);
/* Check if this is a key release (bit 7 set) */
int key_released = scancode & 0x80;
uint8_t code = scancode & 0x7F;
/* Handle modifier keys */
if (code == 0x2A || code == 0x36) { /* Left/Right Shift */
shift_pressed = !key_released;
return;
}
if (code == 0x1D) { /* Ctrl */
ctrl_pressed = !key_released;
return;
}
if (code == 0x38) { /* Alt */
alt_pressed = !key_released;
return;
}
/* Only process key presses, not releases */
if (key_released) {
return;
}
/* Look up the ASCII character */
char c;
if (shift_pressed) {
c = scancode_shifted[code];
} else {
c = scancode_normal[code];
}
/* Handle Ctrl+C (break) */
if (ctrl_pressed && (c == 'c' || c == 'C')) {
/* Could set a flag for process termination */
buffer_add(3); /* ASCII ETX (Ctrl+C) */
return;
}
/* Add to buffer if it's a printable character */
if (c != 0) {
buffer_add(c);
}
/* Note: EOI is sent by irq_handler after this returns */
}
Putting It All Together
Here's the complete interrupt system initialization:
/* Initialize all interrupt-related subsystems */
void interrupts_init(void) {
/* Set up the Interrupt Descriptor Table */
idt_init();
/* Remap the PIC (move IRQs to vectors 32-47) */
pic_remap();
/* Initialize the timer at 100 Hz */
timer_init(100);
/* Enable the keyboard IRQ */
pic_clear_mask(1);
/* Enable interrupts! */
__asm__ volatile("sti");
kprintf("Interrupts enabled.\n");
}
/* Main kernel entry point */
void kernel_main(void) {
/* Initialize display */
terminal_init();
kprintf("Kernel starting...\n");
/* Set up interrupt handling */
interrupts_init();
/* Now we can use interrupt-driven I/O! */
kprintf("Type something: ");
while (1) {
char c = keyboard_getchar();
terminal_putc(c); /* Echo the character */
}
}
Benefits of Interrupt-Driven I/O
- CPU Efficiency: No wasted cycles polling; CPU can do useful work or sleep
- Responsiveness: Input is captured immediately when it happens
- Buffering: Keypresses aren't lost if the program is busy
- Foundation for Multitasking: Timer interrupt enables preemptive scheduling
Best Practice
What You Can Build
Phase 5 Achievement: You now have a fully interrupt-driven kernel! It ticks 100 times per second, responds instantly to keypresses, and handles CPU exceptions gracefully. This is the foundation for everything else in an operating system.
Project: Timer-Driven Status Bar
Let's put everything together with a status bar that updates in real-time:
/* status.c - Real-time status bar */
#include "terminal.h"
#include "timer.h"
#include "keyboard.h"
/* Status bar lives at row 0 */
#define STATUS_ROW 0
/* Called periodically (e.g., every 10 ticks = 100ms) */
void update_status_bar(void) {
static uint32_t last_update = 0;
uint32_t now = timer_get_ticks();
/* Only update every 10 ticks (100ms at 100Hz) */
if (now - last_update < 10) {
return;
}
last_update = now;
/* Save cursor position */
int saved_x = terminal_getx();
int saved_y = terminal_gety();
/* Draw status bar with different colors */
uint8_t saved_color = terminal_getcolor();
terminal_set_color(vga_make_color(VGA_BLACK, VGA_LIGHT_GREY));
/* Clear status line */
terminal_set_cursor(0, STATUS_ROW);
for (int i = 0; i < 80; i++) {
terminal_putc(' ');
}
/* Left: OS name */
terminal_set_cursor(1, STATUS_ROW);
terminal_set_color(vga_make_color(VGA_LIGHT_CYAN, VGA_LIGHT_GREY));
kprintf(" MyOS v0.5 ");
/* Center: Current time */
terminal_set_cursor(35, STATUS_ROW);
terminal_set_color(vga_make_color(VGA_BLACK, VGA_LIGHT_GREY));
uint32_t uptime = timer_uptime_seconds();
kprintf(" %02d:%02d:%02d ",
uptime / 3600,
(uptime % 3600) / 60,
uptime % 60);
/* Right: Keyboard buffer status */
terminal_set_cursor(65, STATUS_ROW);
if (keyboard_has_data()) {
terminal_set_color(vga_make_color(VGA_LIGHT_GREEN, VGA_LIGHT_GREY));
kprintf("Data Ready");
} else {
terminal_set_color(vga_make_color(VGA_DARK_GREY, VGA_LIGHT_GREY));
kprintf(" Idle ");
}
/* Restore cursor and color */
terminal_set_color(saved_color);
terminal_set_cursor(saved_x, saved_y);
}
/* Hook into timer interrupt */
void timer_handler(registers_t* regs) {
timer_ticks++;
update_status_bar();
}
Exercises
Exercise 1: Implement sleep() Function
Create a user-friendly sleep function that waits for a specified number of milliseconds:
void sleep_ms(uint32_t milliseconds) {
/* Convert ms to ticks based on TIMER_FREQ_HZ */
/* Handle rounding: 1ms at 100Hz = 0.1 ticks, round up! */
}
Code Challenge
Exercise 2: Add IRQ Handler Registration
Instead of a big switch statement in irq_handler, implement a registration system:
typedef void (*irq_handler_fn)(registers_t*);
void irq_install_handler(uint8_t irq, irq_handler_fn handler);
void irq_uninstall_handler(uint8_t irq);
/* Usage: */
irq_install_handler(0, timer_handler);
irq_install_handler(1, keyboard_handler);
Design Pattern
Exercise 3: Implement a Beep Function
Use the PC Speaker (port 0x61 + PIT Channel 2) to generate a beep:
void beep(uint32_t frequency, uint32_t duration_ms);
/* The PC Speaker is controlled by:
* - PIT Channel 2 (0x42) for frequency
* - Port 0x61 bits 0-1 for enabling
*/
Hardware Fun
Next Steps
Your kernel now has a heartbeat! With interrupts working, you have the foundation for:
- Preemptive Multitasking: Timer interrupts let you switch between processes
- Responsive I/O: Handle keyboard, mouse, disk without polling
- Time Keeping: Track uptime, implement delays, schedule events
- Power Efficiency: Let CPU sleep between interrupts (HLT instruction)
WHAT'S NEXT: PHASE 6 - MEMORY MANAGEMENT
═══════════════════════════════════════════════════════════════
Your current kernel: After Phase 6:
┌───────────────────────────┐ ┌───────────────────────────┐
│ Physical Memory │ │ Virtual Address Space │
│ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │
│ │ Kernel (fixed addr) │ │ │ │ Kernel Space (high) │ │
│ ├─────────────────────┤ │ --> │ ├─────────────────────┤ │
│ │ ??? (unused) │ │ │ │ User Space (low) │ │
│ │ ??? (unknown) │ │ │ │ [protected, paged] │ │
│ └─────────────────────┘ │ │ └─────────────────────┘ │
│ No protection! │ │ Page tables + allocator! │
└───────────────────────────┘ └───────────────────────────┘
Topics in Phase 6:
• Paging: Virtual to physical address translation
• Page Tables: 4KB pages, CR3, TLB
• Physical Memory Manager: Track free/used frames
• Virtual Memory Manager: Map pages for processes
• Heap Allocator: kmalloc() and kfree()
Key Takeaways from Phase 5:
- The IDT maps interrupt numbers to handler functions (256 entries)
- CPU exceptions (0-31) are faults, traps, and aborts from the processor
- The PIC must be remapped to avoid conflicts with CPU exceptions
- Always send EOI to the PIC after handling hardware interrupts
- Interrupt-driven I/O is more efficient than polling
- Timer interrupts enable scheduling and time-keeping
Continue the Series
Phase 4: Display, Input & Output
Review VGA text mode output and basic keyboard input handling.
Read Article
Phase 6: Memory Management
Implement paging, virtual memory, and a dynamic heap allocator.
Read Article
Phase 7: Disk Access & Filesystems
Access block devices, implement FAT filesystem, and build a VFS layer.
Read Article