Writing an OS Kernel from Scratch | Interrupt and Exception Handling — How the CPU Gets “Interrupted” by Peripherals

Your program is running, and suddenly you press a key on the keyboard — how does the operating system know about it? How does the CPU instantly switch from executing code to handling keyboard input? The answer is interrupts.

Interrupts are the core mechanism of hardware-software interaction: peripherals notify the CPU via interrupts “I have something to handle here,” the CPU pauses its current work, jumps to execute the corresponding Interrupt Service Routine (ISR), and returns after processing.

But interrupts aren’t just for hardware — the CPU itself can trigger Exceptions, such as division by zero, accessing invalid memory, illegal instructions, etc. These are also “interruptions,” but the source is internal to the CPU rather than external hardware.

Today, let’s figure out the complete chain of interrupt and exception handling.


1. Essential Differences Between Interrupts and Exceptions

Hardware Interrupts:

  • Source: External hardware (keyboard, NIC, hard drive, timer, etc.)
  • Trigger condition: Peripheral sends signal to CPU via interrupt pin (IRQ)
  • Handling: Pause current task, execute ISR, resume after completion

CPU Exceptions:

  • Source: CPU itself detects errors during instruction execution
  • Trigger condition: Division by zero, out-of-bounds access, invalid opcode, page fault, etc.
  • Handling: Recoverability depends on exception type (fault/trap/abort)

Bash
Classification of Interrupts and Exceptions:

Hardware Interrupts (Asynchronous)
  ├── Maskable Interrupt (IRQ) — responds when IF=1, ignores when IF=0
  └── Non-Maskable Interrupt (NMI) — hardware failure, e.g. memory parity error

CPU Exceptions (Synchronous)
  ├── Fault — recoverable, can return to triggering instruction and re-execute
  │   ├── #DE (Divide-by-Zero)
  │   ├── #PF (Page Fault)
  │   └── #GP (General Protection)
  ├── Trap — intentionally triggered, continues to next instruction after execution
  │   ├── #DB (Debug Breakpoint)
  │   └── #OF (Overflow)
  └── Abort — unrecoverable, log error, terminate process
      └── #DF (Double Fault)

2. IDT: Interrupt Descriptor Table

x86 uses the IDT (Interrupt Descriptor Table) to describe the handling function for each interrupt/exception. Similar to GDT, the IDT is also a table, but each entry has a different format.

2.1 IDT Entry Format (Gate Descriptor)

Bash
IDT Gate Descriptor (8 bytes = 64 bits) format:

┌──────────────────────────────────────────────────────────────┐
│  Bit 63-48:  Offset [31:16]  (Handler address high 16 bits) │
│  Bit 47-40:  Reserved / Selector (Code segment selector)    │
│  Bit 39-36:  Zero                                           │
│  Bit 35-32:  Gate Type                                      │
│  Bit 31-16:  Offset [15:0]   (Handler address low 16 bits)  │
│  Bit 15-8:   Selector (Code segment selector)               │
│  Bit 7-0:    IST / Reserved                                 │
└──────────────────────────────────────────────────────────────┘

Gate Type Values:
  0x5E = 64-bit Interrupt Gate
  0x6E = 64-bit Trap Gate
  0x7E = 64-bit Task Gate (rarely used)

Difference between Interrupt Gate and Trap Gate:
  Interrupt Gate: Automatically disables interrupts (IF=0) on jump, prevents nesting
  Trap Gate: Keeps IF unchanged on jump, allows nested interrupts (but be careful)

2.2 IDT Vector Assignment (Common Exceptions and Interrupts)

Bash
IDT Vector Allocation (x86 common mappings):

Vector   Description                        Source          Type
0        #DE — Divide-by-Zero Error         CPU             Fault
1        #DB — Debug Exception              CPU             Fault/Trap
2        NMI — Non-Maskable Interrupt       External        NMI
3        #BP — Breakpoint (INT3)            CPU             Trap
4        #OF — Overflow (INTO)              CPU             Trap
5        #BR — Bound Range Exceeded         CPU             Fault
6        #UD — Invalid Opcode               CPU             Fault
7        #NM — Device Not Available         CPU             Fault
8        #DF — Double Fault                 CPU             Abort
10       #TS — Invalid TSS                  CPU             Fault
11       #NP — Segment Not Present          CPU             Fault
12       #SS — Stack Segment Fault          CPU             Fault
13       #GP — General Protection Fault     CPU             Fault
14       #PF — Page Fault                   CPU             Fault
16       #MF — x87 FPU Error                CPU             Fault
17       #AC — Alignment Check              CPU             Fault
18       #MC — Machine Check                CPU             Abort
19       #XM — SIMD Floating-Point Fault    CPU             Fault
20       #VE — Virtualization Exception     CPU             Fault

32-255   IRQ 0-223 (hardware interrupts)    External        Interrupt
  IRQ 0    — PIT Timer
  IRQ 1    — Keyboard
  IRQ 3    — COM2
  IRQ 4    — COM1
  IRQ 8    — RTC
  IRQ 12   — PS/2 Mouse
  IRQ 14   — Primary IDE
  IRQ 15   — Secondary IDE

3. How Interrupts Propagate: From Hardware to IDT

When a peripheral triggers an interrupt, the CPU goes through the following steps:

Bash
Full interrupt response process:

1. Device triggers interrupt signal
   └── Keyboard sends signal on IRQ 1
       └── PIC (8259A or APIC) receives the signal
           └── PIC converts IRQ number to vector number (e.g. IRQ 1 → Vector 33)
               └── PIC sends signal to CPU's INTR pin

2. CPU checks IF flag (on x86: EFLAGS bit 9)
   ├── IF=1 → handle interrupt
   └── IF=0 → ignore (except NMI)

3. CPU saves current execution state (hardware saves)
   ├── Push SS / RSP (current stack pointer)
   ├── Push RFLAGS
   ├── Push CS / RIP (return address)
   └── Push error code (some exceptions push an error code)

4. CPU looks up IDT by vector number
   └── IDT entry points to the handler

5. Jump to handler (Interrupt Service Routine)
   ├── Save remaining registers
   ├── Call specific handler logic
   ├── Send EOI (End of Interrupt) to PIC
   └── Restore registers

6. IRETQ returns to original execution point

4. Real-World Code: Setting Up IDT

Let’s look at a minimal IDT initialization code:

C
// Minimal IDT initialization (x86_64)
#define IDT_ENTRIES 256

struct idt_entry {
    uint16_t offset_low;   // Handler address low 16 bits
    uint16_t selector;     // Code segment selector (Kernel CS)
    uint8_t  ist;          // Interrupt Stack Table offset
    uint8_t  type_attr;    // Gate type + attributes
    uint16_t offset_mid;   // Handler address mid 16 bits
    uint32_t offset_high;  // Handler address high 32 bits
    uint32_t zero;         // Reserved
} __attribute__((packed));

struct idt_ptr {
    uint16_t limit;        // IDT size - 1
    uint64_t base;         // IDT base address
} __attribute__((packed));

struct idt_entry idt[IDT_ENTRIES];
struct idt_ptr idtp;

// Set IDT entry
void idt_set_entry(int vector, uint64_t handler, uint8_t type) {
    idt[vector].offset_low  = handler & 0xFFFF;
    idt[vector].offset_mid  = (handler >> 16) & 0xFFFF;
    idt[vector].offset_high = (handler >> 32) & 0xFFFFFFFF;
    idt[vector].selector    = KERNEL_CS;          // Kernel code segment
    idt[vector].ist         = 0;                  // No IST switching
    idt[vector].type_attr   = type | 0x80;        // Present + Gate type
    idt[vector].zero        = 0;
}

// Load IDT
void idt_init() {
    idtp.limit = sizeof(struct idt_entry) * IDT_ENTRIES - 1;
    idtp.base  = (uint64_t)&idt;
    asm volatile("lidt %0" : : "m"(idtp));
}

5. Interrupt Nesting and Reentrancy

5.1 Nested Interrupts

What happens if another interrupt arrives while handling one interrupt?

Bash
Scenario:
  CPU executing user code
    → IRQ 1 (keyboard) arrives
      → Enter keyboard ISR
        → IRQ 3 (COM2) arrives
          → Should we handle it now, or wait?

There are two strategies:

Interrupt Gate (IF=0) strategy:

  • Automatically close interrupts, disable nested interrupts
  • Ensure the current ISR runs uninterrupted
  • But: The next interrupt is delayed, which may affect real-time performance

Trap Gate (IF unchanged) strategy:

  • Keep IF unchanged, allow nested interrupts
  • High-priority interrupts can be handled immediately
  • But: Must ensure reentrancy — ISR is like a function called by itself

5.2 Reentrancy Requirements

Interrupt handlers that allow nesting must:

  1. Save and restore all used registers
  2. Use separate stacks to avoid stack corruption
  3. Lock shared data (use spinlock)
  4. Ensure all operations are atomic or access critical sections safely

6. Double Fault and Triple Fault

The most painful situation: an exception occurs while handling an exception.

Bash
#DF (Double Fault) Trigger Path:

Scenario: IDT entry for vector 0 (Divide by Zero) is not set up
  Program executes DIV instruction
    → CPU tries to read IDT[0], finds it invalid
      → CPU triggers #GP (General Protection, vector 13)
        → CPU tries to read IDT[13], also invalid
          → CPU triggers #DF (Double Fault, vector 8)
            → If IDT[8] is also invalid...
              → TRIPLE FAULT: CPU reset!

Triple Fault = CPU detects a double fault while trying to process a double fault. The CPU has no way out and directly triggers a restart.

Prevention: At minimum, set up valid IDT entries for:

  • DE (0), #GP (13), #DF (8)

  • At least one hardware interrupt entry (for PIC timer)

7. Summary

Understanding the interrupt/exception mechanism is the foundation for mastering operating systems. Key takeaways:

  1. Interrupts are from external hardware, exceptions are from the CPU itself
  2. IDT is the mapping table from vector numbers to handlers
  3. Interrupt Gates disable nests, Trap Gates allow nests
  4. Double Fault is an exception in an exception; must set up at least the three key entries
  5. The first thing in an ISR is to save the scene, the last thing is to restore it

Next up: How system calls cross the Ring 3 ↔ Ring 0 boundary — stay tuned!

OS #KernelDevelopment #Interrupts #Exceptions #IDT #x86 #WritingFromScratch

Last modified: 2024年9月24日

Author

Comments

Write a Reply or Comment

Your email address will not be published.