Skip to main content
Aeolos implements a robust interrupt handling system using the x86_64 Interrupt Descriptor Table (IDT) to manage CPU exceptions, hardware interrupts, and system calls.

Interrupt Descriptor Table (IDT)

The IDT is a table of 256 entries that maps interrupt vectors to handler functions. It’s initialized early in the boot process.

IDT Entry Structure

From kernel/sys/idt.h:11:
kernel/sys/idt.h
struct [[gnu::packed]] idt_entry {
    uint16_t offset_15_0;   // Lower 16 bits of handler address
    uint16_t selector;       // Code segment selector
    uint16_t flags;          // Type and attributes
    uint16_t offset_31_16;   // Middle 16 bits of handler address
    uint32_t offset_63_32;   // Upper 32 bits of handler address
    uint32_t reserved;
};

struct [[gnu::packed]] idtr {
    uint16_t limit;  // Size of IDT - 1
    uint64_t base;   // Address of IDT
};

IDT Initialization

The IDT is set up in kernel/sys/idt.c:40:
kernel/sys/idt.c
void idt_init()
{
    // exception isr's
    IDT[0] = idt_make_entry((uint64_t)&isr0, false);
    IDT[1] = idt_make_entry((uint64_t)&isr1, false);
    IDT[2] = idt_make_entry((uint64_t)&isr2, false);
    IDT[3] = idt_make_entry((uint64_t)&isr3, false);
    IDT[4] = idt_make_entry((uint64_t)&isr4, false);
    IDT[5] = idt_make_entry((uint64_t)&isr5, false);
    IDT[6] = idt_make_entry((uint64_t)&isr6, false);
    IDT[7] = idt_make_entry((uint64_t)&isr7, false);
    IDT[8] = idt_make_entry((uint64_t)&isr8, false);
    IDT[10] = idt_make_entry((uint64_t)&isr10, false);
    IDT[11] = idt_make_entry((uint64_t)&isr11, false);
    IDT[12] = idt_make_entry((uint64_t)&isr12, false);
    IDT[13] = idt_make_entry((uint64_t)&isr13, false);
    IDT[14] = idt_make_entry((uint64_t)&isr14, false);
    IDT[16] = idt_make_entry((uint64_t)&isr16, false);
    IDT[17] = idt_make_entry((uint64_t)&isr17, false);
    IDT[18] = idt_make_entry((uint64_t)&isr18, false);
    IDT[19] = idt_make_entry((uint64_t)&isr19, false);
    IDT[20] = idt_make_entry((uint64_t)&isr20, false);
    IDT[30] = idt_make_entry((uint64_t)&isr30, false);

    // syscall isr
    IDT[SYSCALL_IRQ_VECTOR] = idt_make_entry((uint64_t)&syscall_entry, true);

    struct idtr i = { .limit = sizeof(IDT) - 1, .base = (uint64_t)&IDT };

    idt_load(&i);
    klog_ok("done\n");
}
Note that vectors 9, 15, and 21-29 are not initialized as they are reserved or not used in standard x86_64 systems.

Creating IDT Entries

The helper function at kernel/sys/idt.c:15 creates IDT entries:
kernel/sys/idt.c
static struct idt_entry idt_make_entry(uint64_t offset, bool usermode)
{
    return (struct idt_entry) {
        .selector = CODE_SEGMENT_SELECTOR,
        .offset_15_0 = offset & 0xFFFF,
        .offset_31_16 = (offset >> 16) & 0xFFFF,
        .offset_63_32 = (offset >> 32) & 0xFFFFFFFF,
        .flags = usermode ? IDT_FLAGS_USER : IDT_FLAGS_DEFAULT
    };
}
The flags determine privilege level:
  • IDT_FLAGS_DEFAULT (0b1000111000000000) - Kernel-only, present, interrupt gate
  • IDT_FLAGS_USER (0b1110111000000000) - User-accessible (DPL=3), present, interrupt gate

CPU Exception Handlers

Aeolos installs handlers for standard x86_64 exceptions:

Vector 0 - Division Error

Divide by zero or divide overflow

Vector 1 - Debug

Debug exception (single-step, breakpoint)

Vector 2 - NMI

Non-maskable interrupt

Vector 3 - Breakpoint

INT3 instruction

Vector 4 - Overflow

INTO instruction with overflow flag set

Vector 5 - Bound Range

BOUND instruction range exceeded

Vector 6 - Invalid Opcode

Undefined or invalid instruction

Vector 7 - Device Not Available

FPU/SSE instruction without FPU present

Vector 8 - Double Fault

Exception while handling another exception

Vector 10 - Invalid TSS

Invalid Task State Segment

Vector 11 - Segment Not Present

Segment marked not present

Vector 12 - Stack Fault

Stack segment or limit violation

Vector 13 - General Protection

General protection violation

Vector 14 - Page Fault

Invalid page access

Vector 16 - x87 FPU Error

Floating-point exception

Vector 17 - Alignment Check

Unaligned memory access with AC flag set

Vector 18 - Machine Check

Hardware error detected

Vector 19 - SIMD Exception

SSE/AVX floating-point exception

Vector 20 - Virtualization

Virtualization exception

Vector 30 - Security Exception

Security-sensitive condition

Dynamic Interrupt Vector Allocation

For hardware interrupts (like the APIC timer), vectors are dynamically allocated at kernel/sys/idt.c:31:
kernel/sys/idt.c
uint8_t idt_get_vector()
{
    lastvector++;
    if (lastvector == 0)
        kernel_panic("Out of IRQ vectors\n");

    return lastvector;
}
Allocation starts at SYSCALL_IRQ_VECTOR + 1, ensuring system call and exception vectors remain fixed.

Setting Interrupt Handlers

Handlers can be installed at runtime using idt_set_handler() at kernel/sys/idt.c:26:
kernel/sys/idt.c
void idt_set_handler(uint8_t vector, void* handler, bool usermode)
{
    IDT[vector] = idt_make_entry((uint64_t)handler, usermode);
}

Example: APIC Timer Handler

From kernel/sys/apic/timer.c:70:
kernel/sys/apic/timer.c
void apic_timer_init()
{
    // allocate a vector for the timer
    vector = idt_get_vector();
    idt_set_handler(vector, &apic_timer_handler, false);

    // unmask the apic timer interrupt and set divisor to 4
    apic_write_reg(APIC_REG_TIMER_LVT, APIC_TIMER_FLAG_MASKED | vector);
    apic_write_reg(APIC_REG_TIMER_DCR, 0b0001);
    divisor = 4;
    
    // ... calibration code ...
}

Interrupt Handler Attributes

Aeolos uses GCC’s interrupt attribute for handler functions:
kernel/sys/apic/timer.c
[[gnu::interrupt]] static void apic_timer_handler(void* v)
{
    (void)v;
    klog_warn("APIC Timer: No handler registered\n");
    apic_send_eoi();
}
The interrupt attribute tells the compiler to:
  1. Save all registers on entry
  2. Use iretq instead of ret to return
  3. Handle the interrupt stack frame correctly
All interrupt handlers must send an End-of-Interrupt (EOI) to the APIC using apic_send_eoi() before returning, or interrupts will be blocked.

APIC End-of-Interrupt

The EOI signal tells the APIC that interrupt processing is complete at kernel/sys/apic/apic.c:27:
kernel/sys/apic/apic.c
void apic_send_eoi()
{
    apic_write_reg(APIC_REG_EOI, 1);
}

System Calls

System calls use a dedicated interrupt vector that is accessible from user mode:
kernel/sys/idt.c
// syscall isr
IDT[SYSCALL_IRQ_VECTOR] = idt_make_entry((uint64_t)&syscall_entry, true);
Note the true parameter indicating user-mode accessibility (DPL=3).

Spurious Interrupts

The APIC can generate spurious interrupts in rare cases. A handler is registered at kernel/sys/apic/apic.c:12:
kernel/sys/apic/apic.c
[[gnu::interrupt]] static void spurious_int_handler(void* v [[maybe_unused]])
{
    klog_info("APIC spurious interrupt recieved");
}
Spurious interrupt handlers should NOT send EOI, as the interrupt never actually occurred.

Inter-Processor Interrupts (IPI)

IPIs allow one CPU to interrupt another, used for SMP operations. From kernel/sys/apic/apic.c:33:
kernel/sys/apic/apic.c
void apic_send_ipi(uint8_t dest, uint8_t vector, uint32_t mtype)
{
    apic_write_reg(APIC_REG_ICR_HIGH, (uint32_t)dest << 24);
    apic_write_reg(APIC_REG_ICR_LOW, (mtype << 8) | vector);
}
Parameters:
  • dest - Destination CPU’s local APIC ID
  • vector - Interrupt vector to trigger
  • mtype - Message type (fixed, init, startup, etc.)

Interrupt Stack Frame

When an interrupt occurs, the CPU pushes this stack frame:
+------------------+
| SS               | +40
| RSP              | +32
| RFLAGS           | +24
| CS               | +16
| RIP              | +8
| Error Code       | 0 (not present for all interrupts)
+------------------+
For interrupts without error codes, the handler frame pointer points directly to RIP.

Context Switching via Timer

The scheduler uses the APIC timer interrupt to preempt tasks. The timer handler at kernel/proc/sched/sched.c:75 performs context switching:
kernel/proc/sched/sched.c
void _do_context_switch(task_state_t* state)
{
    lock_wait(&sched_lock);
    uint16_t cpu = smp_get_current_info()->cpu_id;

    // save state of current task
    task_t* curr = tasks_running[cpu];
    curr->kstack_top = state;
    curr->status &= ~TASK_RUNNING;

    // wake up tasks which need to be woken up
    while (tasks_asleep.back && tasks_asleep.back->wakeuptime < hpet_get_nanos()) {
        task_t* t = tq_pop_back(&tasks_asleep);
        t->status &= ~TASK_ASLEEP;
    }

    // choose next task
    task_t* next = NULL;
    for (tid_t i = curr->tid + 1; i < tasks_all.len; i++) {
        task_t* t = tasks_all.data[i];
        if (t != NULL && t->status == TASK_READY) {
            next = t;
            goto task_chosen;
        }
    }
    // ... search from beginning ...

task_chosen:
    if (!next)
        next = tasks_idle[cpu];

    next->status |= TASK_RUNNING;
    tasks_running[cpu] = next;

    smp_get_current_info()->tss.rsp0 = (uint64_t)(next->kstack_limit + KSTACK_SIZE);
    apic_timer_set_period(TIMESLICE_DEFAULT);
    apic_send_eoi();

    lock_release(&sched_lock);
    finish_context_switch(next);
}
This function:
  1. Saves current task’s register state
  2. Wakes sleeping tasks whose time has expired
  3. Selects the next task to run
  4. Updates the TSS with the new task’s kernel stack
  5. Resets the timer for the next timeslice
  6. Sends EOI to the APIC
  7. Switches to the next task

Build docs developers (and LLMs) love