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:
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:
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:
static struct idt_entry idt_make_entry ( uint64_t offset , bool usermode )
{
return ( struct idt_entry) {
.selector = CODE_SEGMENT_SELECTOR,
.offset_15_0 = offset & 0x FFFF ,
.offset_31_16 = (offset >> 16 ) & 0x FFFF ,
.offset_63_32 = (offset >> 32 ) & 0x FFFFFFFF ,
.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:
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:
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:
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, 0b 0001 );
divisor = 4 ;
// ... calibration code ...
}
Interrupt Handler Attributes
Aeolos uses GCC’s interrupt attribute for handler functions:
[[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:
Save all registers on entry
Use iretq instead of ret to return
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:
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:
// 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:
[[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:
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:
Saves current task’s register state
Wakes sleeping tasks whose time has expired
Selects the next task to run
Updates the TSS with the new task’s kernel stack
Resets the timer for the next timeslice
Sends EOI to the APIC
Switches to the next task