Khaled Hammouda
Home
Fusion OS
NimJet
Home
Fusion OS
NimJet
  • Fusion OS
    • Writing an OS in Nim
    • Environment Setup
    • Targeting UEFI (Part 1)
    • Targeting UEFI (Part 2)
    • UEFI Bootloader (Part 1)
    • UEFI Bootloader (Part 2)
    • Kernel Image
    • Loading the Kernel (Part 1)
    • Loading the Kernel (Part 2)
    • Loading the Kernel (Part 3)
    • Physical Memory
    • Virtual Memory
    • Higher Half Kernel
    • Memory Segments
    • Interrupts
    • User Mode
    • Task State Segment
    • System Calls
    • Tasks
    • Position Independent Code
    • ELF Loader (Part 1)
    • ELF Loader (Part 2)
    • Cooperative Multitasking
    • System Library
    • Interrupt Controller

Interrupt Controller

The legacy x86 architecture featured a single interrupt controller, the 8259A Programmable Interrupt Controller (PIC), responsible for managing hardware interrupts. The PIC is a simple device that is limited to 8 interrupts per controller (15 interrupts total when cascaded with another PIC, as one interrupt line is used for the cascade), and is limited in terms of interrupt priority, routing flexibility, and multiprocessor support.

The PIC is now considered obsolete, and modern x86 systems use the Advanced Programmable Interrupt Controller (APIC) architecture. The APIC architecture consists of two main components: the I/O APIC and the Local APIC. The I/O APIC is responsible for managing interrupts from external devices (usually there is only one in the system), while the Local APIC is integrated into each CPU core and is responsible for managing interrupts delivered to the CPU core (whether from the I/O APIC or from the Local APIC of other CPU cores), as well as interrupts from internal sources such as the Local APIC timer. For this reason, we will focus on the Local APIC in this section.

Local APIC

The Local APIC is responsible for managing interrupts delivered to its associated core. The interrupts it delivers can originate from internal sources, such as its timer, thermal sensors, and performance monitoring counters, or from external sources, such as the I/O APIC and Inter-Processor Interrupts (IPIs).

The following is a simplified diagram of the relevant components of the Local APIC (there are more components, but we won't need them for now):

┌─────────────────────────────┐
│      Version Register       │
└─────────────────────────────┘

┌──────────────────────────────┐ ◄──┐
│    Current Count Register    │    │
├──────────────────────────────┤    │
│    Initial Count Register    │    ├── Timer Registers
├──────────────────────────────┤    │
│     Divide Configuration     │    │
└──────────────────────────────┘ ◄──┘

┌──────────────────────────────┐ ◄──┐
│            Timer             │    │
├──────────────────────────────┤    │
│          Local INT0          │    │
├──────────────────────────────┤    │
│          Local INT1          │    │
├──────────────────────────────┤    ├── Local Vector Table (LVT)
│  Perf. Monitoring Counters   │    │
├──────────────────────────────┤    │
│        Thermal Sensors       │    │
├──────────────────────────────┤    │
│        Error Register        │    │
└──────────────────────────────┘ ◄──┘

┌──────────────────────────────┐
┆       Other registers...     ┆

The Local APIC is memory-mapped, typically at physical address 0xFEE00000; the actual address should be read from the IA32_APIC_BASE MSR. Although the address is the same for all cores, they operate independently and can be programmed separately. Here's a diagram of this MSR:

                                IA32_APIC_BASE MSR
 
 63             MAX_PHYS_ADDR                          12 11 10  9  8 7         0
┌────────────────────────────┬───────────────────────────┬──┬─────┬──┬───────────┐
│░░░░░░░░░░░░░░░░░░░░░░░░░░░░│         APIC Base         │  │░░░░░│  │░░░░░░░░░░░│
└────────────────────────────┴───────────────────────────┴──┴─────┴──┴───────────┘
                                           ▲               ▲        ▲
              Physical base address ───────┘               │        │
         APIC global enable/disable ───────────────────────┘        │
             BSP - Processor is BSP ────────────────────────────────┘

The APIC is programmed by writing to its registers, which are 32 bits wide and located at fixed offsets from the base address. The registers occupy a 4KB page of physical memory address space, from 0xFEE00000 to 0xFEE00FFF, with each register aligned on a 16-byte boundary.

Since the base address we get from the IA32_APIC_BASE MSR is a physical address, we can't use it directly; we need to map a page of virtual memory to it first, and then use that virtual memory region to access the APIC registers. Also note this description from the Intel manual:

APIC Base field, bits 12 through 35: Specifies the base address of the APIC registers. This 24-bit value is extended by 12 bits at the low end to form the base address. This automatically aligns the address on a 4-KByte boundary.

So we'll need to shift the base address left by 12 bits to get the physical address of the APIC. Since the address is already aligned to a page boundary, we can use it directly when mapping it to virtual memory.

Let's start by creating a new module lapic.nim and defining a type for the IA32_APIC_BASE MSR so that we can read it and get the physical address of the Local APIC.

# src/kernel/lapic.nim

type
  IA32ApicBaseMsr {.packed.} = object
    reserved1   {.bitsize:  8.}: uint64
    isBsp       {.bitsize:  1.}: uint64  # Is Bootstrap Processor?
    reserved2   {.bitsize:  2.}: uint64
    enabled     {.bitsize:  1.}: uint64  # APIC Enabled?
    baseAddress {.bitsize: 24.}: uint64  # Physical Base Address (bits 12-35)
    reserved3   {.bitsize: 28.}: uint64

Let's import our vmm module so that we can map the APIC region, and let's define a proc to initialize the base virtual address of the APIC.

import vmm

var
  apicBaseAddress: uint64

proc initBaseAddress() =
  let apicBaseMsr = cast[IA32ApicBaseMsr](readMSR(IA32_APIC_BASE))
  let apicPhysAddr = (apicBaseMsr.baseAddress shl 12).PhysAddr
  # by definition, apicPhysAddr is aligned to a page boundary, so we map it directly
  let apicVMRegion = vmalloc(kspace, 1)
  mapRegion(
    pml4 = kpml4,
    virtAddr = apicVMRegion.start,
    physAddr = apicPhysAddr,
    pageCount = 1,
    pageAccess = paReadWrite,
    pageMode = pmSupervisor,
    noExec = true
  )
  apicBaseAddress = apicVMRegion.start.uint64

The APIC has many registers at different offsets from the base address, each register is 32 bits wide. Let's define the offsets of those registers. The registers we are interested in are pointed out with comments, as we'll use them later to initialize the APIC and program the timer. And while we're at it, let's also add a couple of procs to read from and write to the APIC registers.

type
  LapicOffset = enum
    LapicId            = 0x020
    LapicVersion       = 0x030
    TaskPriority       = 0x080
    ProcessorPriority  = 0x0a0
    Eoi                = 0x0b0 # ◄────── End Of Interrupt Register
    LogicalDestination = 0x0d0
    DestinationFormat  = 0x0e0
    SpuriousInterrupt  = 0x0f0 # ◄────── Spurious Interrupt Vector Register
    InService          = 0x100
    TriggerMode        = 0x180
    InterruptRequest   = 0x200
    ErrorStatus        = 0x280
    LvtCmci            = 0x2f0
    InterruptCommandLo = 0x300
    InterruptCommandHi = 0x310
    LvtTimer           = 0x320 # ◄────── LVT Timer Register
    LvtThermalSensor   = 0x330
    LvtPerfMonCounters = 0x340
    LvtLint0           = 0x350
    LvtLint1           = 0x360
    LvtError           = 0x370
    TimerInitialCount  = 0x380  # ◄──┐
    TimerCurrentCount  = 0x390  #    ├── Timer Config Registers
    TimerDivideConfig  = 0x3e0  # ◄──┘

proc readRegister(offset: LapicOffset): uint32 {.inline.} =
  result = cast[ptr uint32](apicBaseAddress + offset.uint16)[]

proc writeRegister(offset: LapicOffset, value: uint32) {.inline.} =
  cast[ptr uint32](apicBaseAddress + offset.uint16)[] = value

Initializing the APIC

There are two places that control whether the APIC is enabled or not: the IA32_APIC_BASE MSR APIC global enable/disable flag (bit 11), and the APIC software enable/disable flag in the spurious-interrupt vector register.

Note

Spurious interrupts can occur in rare situations when the processor receives an interrupt at a lower priority than the current interrupt being processed, causing it to become pending. While the ISR for the current interrupt is executing, it may mask the pending interrupt. The APIC will then deliver a spurious interrupt to the processor, which will cause the processor to execute the ISR configured for spurious interrupts. In this case the spurious interrupt handler should just ignore the interrupt and return without an EOI.

The APIC global enable/disable flag is enabled by default, so we don't need to worry about it. This is not the case for the enable/disable bit in the spurious-interrupt vector register, so we need to set it to enable the APIC. Let's first look at a diagram of the spurious-interrupt vector register.

                       Spurious Interrupt Vector Register
 
 31                                               12 11 10  9  8 7             0
┌────────────────────────────────────────────────┬──┬─────┬──┬──┬───────────────┐
│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│  │░░░░░│  │  │               │
└────────────────────────────────────────────────┴──┴─────┴──┴──┴───────────────┘
                                                  ▲         ▲  ▲       ▲
                 EOI Broadcast Suppression ───────┘         │  │       │
                  Focus Processor Checking ─────────────────┘  │       │
              APIC Software Enable/Disable ────────────────────┘       │
                 Spurious Interrupt Vector ────────────────────────────┘

The bits we are interested in are the APIC software enable/disable bit (bit 8) and the spurious interrupt vector (bits 7-0). The vector is set to 0xFF by the CPU by default, which we will keep as is, and will add a new interrupt handler for. Let's create a type for the spurious interrupt vector register so we can easily access its fields.

type
  SpuriousInterruptVectorRegister {.packed.} = object
    vector                  {.bitsize:  8.}: uint32
    apicEnabled             {.bitsize:  1.}: uint32
    focusProcessorChecking  {.bitsize:  1.}: uint32
    reserved0               {.bitsize:  2.}: uint32
    eoiBroadcastSuppression {.bitsize:  1.}: uint32
    reserved1               {.bitsize: 19.}: uint32

Let's create the spurious interrupt handler, which ignores those interrupts.

import idt
...

proc spuriousInterruptHandler*(frame: ptr InterruptFrame)
  {.cdecl, codegenDecl: "__attribute__ ((interrupt)) $# $#$#".} =
  # Ignore spurious interrupts; do not send an EOI
  return

Now let's create a proc to initialize the APIC. This proc will first call initBaseAddress to initialize the base address of the APIC, write the spurious interrupt vector register to enable the APIC, and finally install the spurious interrupt handler in the IDT.

proc lapicInit*() =
  initBaseAddress()
  # enable APIC
  let sivr = SpuriousInterruptVectorRegister(vector: 0xff, apicEnabled: 1)
  writeRegister(LapicOffset.SpuriousInterrupt, cast[uint32](sivr))
  # install spurious interrupt handler
  installHandler(0xff, spuriousInterruptHandler)

Finally, we need to call lapicInit from the kernel's main proc to initialize the APIC.

# src/kernel/main.nim

import lapic
...

proc KernelMainInner(bootInfo: ptr BootInfo) =
  ...

  logger.info "init idt"
  idtInit()

  logger.info "init lapic"
  lapicInit()
  ...

The Local APIC should now be initialized and ready to receive interrupts. In the next section, we'll look at how to program the Local APIC timer to generate periodic interrupts.

Last Updated:
Prev
System Library