Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

APIC and IO APIC Initialization

Background

The x86/x86_64 interrupt subsystem has two generations:

  • 8259 PIC (Programmable Interrupt Controller) — the legacy two-chip design. Master (IRQs 0–7) and slave (IRQs 8–15) are chained. Vectors are remapped to 0x20–0x2F to avoid conflicts with CPU exceptions.
  • APIC (Advanced Programmable Interrupt Controller) — the modern design, required for SMP. Consists of a Local APIC (LAPIC) per CPU core and one or more IO APICs for external devices.

ACPI describes which model the firmware uses via the MADT (Multiple APIC Description Table). On QEMU with default settings, the MADT reports InterruptModel::Apic, meaning APIC mode is required.

Architecture

   Device
     │
     ▼
 IO APIC  ──── Redirection Table ───► Local APIC ──► CPU
(external                              (per core)
 IRQs)                                  LAPIC ID

Local APIC (LAPIC)

  • One per CPU core, memory-mapped at physical address 0xFEE00000 by default.
  • Handles inter-processor interrupts (IPIs) and LAPIC-local sources (timer, thermal, etc.).
  • Must be enabled by writing to the Spurious Interrupt Vector Register (SIVR) at offset 0xF0. Setting bit 8 (APIC_ENABLE) activates the LAPIC. Bits 0–7 set the spurious interrupt vector (conventionally 0xFF).
  • EOI (End of Interrupt) is signalled by writing 0 to the EOI register at offset 0xB0. Unlike the PIC, no interrupt number is needed — the write itself is the acknowledgement.

IO APIC

  • Handles external hardware interrupts (ISA IRQs, PCI interrupts).

  • Accessed via two MMIO registers: IOREGSEL (write selector) and IOWIN (read/write data window), both at the IO APIC base address.

  • Contains a Redirection Table with one 64-bit entry per input pin:

    BitsFieldNotes
    0–7VectorIDT vector to deliver
    8–10Delivery mode0 = fixed
    11Destination mode0 = physical (LAPIC ID), 1 = logical
    13Pin polarity0 = active high, 1 = active low
    15Trigger mode0 = edge, 1 = level
    16Mask1 = masked (disabled)
    56–63DestinationPhysical: target LAPIC ID

ACPI and Interrupt Source Overrides

ISA IRQs are conventionally edge-triggered, active-high. However, some IRQs are remapped: QEMU reports that ISA IRQ 0 (the PIT timer) is redirected to GSI 2 with edge/active-high signalling. The ACPI InterruptSourceOverride table entries describe these remappings:

ISA IRQDefault GSIOverride GSIOverride PolarityOverride Trigger
002 (QEMU)Same as busSame as bus
11

The init_io function reads these overrides from apic_info.interrupt_source_overrides and uses the correct GSI, polarity, and trigger mode when programming each redirection entry.

Initialization Sequence

1. Map Local APIC (libkernel::apic::init_local)

The LAPIC physical address is read from the IA32_APIC_BASE MSR. A virtual page at APIC_BASE is mapped to this physical frame (with NO_CACHE flag, as MMIO must not be cached):

Physical 0xFEE00000  →  Virtual 0xFFFF_8001_0000_0000

After mapping:

  • init() logs the LAPIC ID, version, and LVT register state.
  • enable() writes the SIVR: APIC_ENABLE | 0xFF (enable + spurious vector).
  • The EOI virtual address (APIC_BASE + 0xB0) is stored in libkernel::interrupts::LAPIC_EOI_ADDR so interrupt handlers can issue EOI without needing a reference to the apic module.

2. Map IO APIC(s) (libkernel::apic::init_io)

Each IO APIC listed in the ACPI MADT is mapped to consecutive virtual pages starting at APIC_BASE + 4KiB. The global_system_interrupt_base field records which GSIs this IO APIC handles (typically 0 for the first IO APIC).

After mapping all IO APICs:

  • Mask all entries — every redirection table slot is masked before programming, preventing spurious interrupts during setup.
  • Route ISA IRQs — IRQ 0 (timer) and IRQ 1 (keyboard) are routed to IDT vectors 0x20 and 0x21 respectively, targeting the BSP’s LAPIC ID. Source overrides are applied (e.g. timer GSI 2 on QEMU).

3. Update IDT and EOI (libkernel::interrupts)

The IDT is extended with a spurious interrupt handler at vector 0xFF. Spurious LAPIC interrupts must not receive an EOI.

The timer and keyboard handlers are updated to call send_eoi() instead of PICS.notify_end_of_interrupt(). send_eoi() checks LAPIC_EOI_ADDR: if non-zero (APIC mode), it writes 0 to the LAPIC EOI register; otherwise it falls back to the PIC path. This allows the same IDT to work in both PIC and APIC modes.

4. Disable the 8259 PIC (libkernel::interrupts::disable_pic)

After the IO APIC is programmed, the PIC is disabled by masking all IRQs:

#![allow(unused)]
fn main() {
Port::<u8>::new(0x21).write(0xFF);  // mask master PIC
Port::<u8>::new(0xA1).write(0xFF);  // mask slave PIC
}

This prevents the PIC from delivering interrupts that would arrive at the wrong vectors or cause double-delivery with the IO APIC.

Key Constants

SymbolValueDescription
APIC_BASE0xFFFF_8001_0000_0000Virtual base for LAPIC mapping
LAPIC_EOI_OFFSET0xB0Offset of EOI register in LAPIC
LAPIC_SIVR_OFFSET0xF0Offset of SIVR in LAPIC
SPURIOUS_VECTOR0xFFIDT vector for LAPIC spurious IRQs
TIMER_VECTOR0x20IDT vector for timer (ISA IRQ 0)
KEYBOARD_VECTOR0x21IDT vector for keyboard (ISA IRQ 1)

Crate Location

The APIC code lives in libkernel::apic (module libkernel/src/apic/). It was originally a separate apic crate but was merged into libkernel so that libkernel::irq_handle can call IO APIC mask/unmask/write functions directly without duplicating raw MMIO code.

The LAPIC EOI address is communicated via a single AtomicU64 in libkernel::interrupts: the apic module writes the address after mapping the LAPIC, and interrupt handlers read it to perform EOI.

References

  • Intel SDM Vol. 3A, Chapter 10: Advanced Programmable Interrupt Controller (APIC)
  • OSDev Wiki: APIC, IO APIC, MADT
  • ACPI Specification, Section 5.2.12: Multiple APIC Description Table