One of the first things that an OS needs to handle is is interrupts. Interrupts are signals or events to the processor to temporarily halt the code it's currently executing and handle the event within a reasonable time. Interrupts are typically handled by an interrupt service routine (ISR) with the processor resuming normal execution after the interrupt is finished.

Interrupt Descriptor Table

To enable interrupts on the CPU, an Interrupt Descriptor Table first needs to be defined. When an interrupt is fired, the CPU will use the IDT to determine which ISR to use. It was initially a bit challenging for me to get the interrupts to work, so my code is a bit of a mess. My IDT is written in assembly, but I use C for the actual interrupt service routines.

Each entry is defined using a FASM macro like so:

macro irq_interrupt_entry entry_num* {
    public irq#entry_num
    irq#entry_num:
        dw 0xDEAD    ; Lower bytes of ISR address
        dw 0x0008    ; Segment selector from our GDT
        db 0x00      ; Always set to zero
        db 10101110b ; Type and attributes 
        dw 0xDEAD    ; Higher bytes of ISR address
}

The IDT layout along with a more descriptive detail of each can be found on OSDev.org. From here, it's a matter of calling the above macro for each interrupt:

idt_start:
   irq_interrupt_entry 0
   irq_interrupt_entry 1
   irq_interrupt_entry 2
   ...

Writing a Common Interrupt Handler

The very first thing to do while handling an interrupt is clearing the interrupt flag. This makes sure that the processor isn't interrupted while it's already handling an interrupt. Since we need to resume where we left off after returning from the interrupt, we save the register state by pushing all the registers to the stack. Next, we grab the interrupt number and error from the stack and use it to figure out the proper interrupt code to call . For things like Breakpoint Exception, we can resume normally, but for other things like a general protection fault, we would normally just want to panic the kernel and halt further execution. My implementation looks a bit like the following:

 ; Stack layout is as following:
        ; 44 eflags
        ; 40 cs
        ; 36 eip
        ; 32 error code
        ; 28 interrupt num
        pushd eax ;24
        pushd ebx ;20
        pushd ecx ;16
        pushd edx ;12
        pushd esi ;8
        pushd edi ;4
        pushd ebp ;0

        ; EDI = interrupt #
        ; ECX = error
        ; EBX = 1 to print interrupt, 0 = do not print
        mov edi, esp
        add edi, 28
        mov ecx, esp
        add ecx, 32
        mov ebx, 1 ; Set to 0 to disable printing interrupts

        ; Interrupt 1 is recoverable
        mov edi, esp
        add edi, 28
        mov ecx, 1
        cmp [edi], ecx
        je .resume

        ; Otherwise panic! (We don't know how to handle this interrupt)
        ; This function should NEVER return!
        ccall printf, msg, [edi], [ecx]
        ccall panic, panicmsg
        HLT 

Connecting up the ISRs

This code is a bit messy, but it works for the most part. I wrote two macros: one that handles ISRs with an error code and one that handles ISRs without an error code. Luckily, which ISRs have error codes and which do not are fairly standard.

; This macro pushes 0 as the error code for interrupts
; that do not have an error code
; It also pushes the interrupt number
macro no_error_code_interrupt_handler int_num* {
    public isr_#int_num
    isr_#int_num#:
        pushd 0
        pushd int_num
        jmp common_interrupt_handler
}

; This macro already assumes that the error code is on the stack
; It only pushes the interrupt number
macro error_code_interrupt_handler int_num* {
    public isr_#int_num
    isr_#int_num#:
        pushd int_num
        jmp common_interrupt_handler
}

no_error_code_interrupt_handler 0   ; Divide by 0 error
no_error_code_interrupt_handler 1   ; Debug exception
no_error_code_interrupt_handler 2   ; Non-maskable interrup
no_error_code_interrupt_handler 3   ; Breakpoint exception 
no_error_code_interrupt_handler 4   ; Overflow detected exception
no_error_code_interrupt_handler 5   ; Out of bounds exception
no_error_code_interrupt_handler 6   ; Invalid opcode
no_error_code_interrupt_handler 7   ; No coprocessor
error_code_interrupt_handler 8   ; Double fault
...

Keyboard Interrupt Handler

To get the keyboard working, the PIC first needs to be set up. Although the APIC replaced the 8259 system on modern systems, we're setting up the 8259 because it's easiest. Thankfully, our good friend OSDev has a pretty lengthy section about the PIC here https://wiki.osdev.org/PIC.

The two important things we need to do are to:

  • Set the interrupt maps
  • Remap the PIC
void pic_remap()
{
    /* We save the makes so we can restore them at the end of the function */
    //uint8_t pic1_mask = io_byte_in(PIC1_DATA);
    //uint8_t pic2_mask = io_byte_in(PIC2_DATA);

    /* 
     * Starts initialization in cascade mode
     * Initialize both PICs the same way 
     */
    io_byte_out(PIC1_COMMAND, ICW1_INIT | ICW1_ICW4);
    io_wait();
    io_byte_out(PIC2_COMMAND, ICW1_INIT | ICW1_ICW4);
    io_wait();
    
    /* Set vector offsets for both the leader and follower PICs */
    io_byte_out(PIC1_DATA, PIC1_START_INTERRUPT);
    io_wait();
    io_byte_out(PIC2_DATA, PIC2_START_INTERRUPT);
    io_wait();

    /* Tell the leader PIC that the follower is at IRQ2 (0000 0100b) */
    io_byte_out(PIC1_DATA, 4);
    io_wait();

    /* Tell follower PIC that it's identity is 2 (0000 0010b) */
    io_byte_out(PIC2_DATA, 2);
    io_wait();

    /* Set to 8086 mode */
    io_byte_out(PIC1_DATA, ICW4_8086);
    io_wait();
    io_byte_out(PIC2_DATA, ICW4_8086);
    io_wait();

    /* Restore masks */
    //io_byte_out(PIC1_DATA, pic1_mask);
    //io_byte_out(PIC2_DATA, pic2_mask);
    io_byte_out( PIC1_DATA, 0xf8 ); /* master PIC */
    io_byte_out( PIC2_DATA, 0xff ); /* slave PIC */
}

void pic_set_interrupt_masks()
{
    /* Only listen to irqs 1 */
    io_byte_out( PIC1_DATA, 0xfd ); /* master PIC */
    io_byte_out( PIC2_DATA, 0xff ); /* slave PIC */
    asm("sti"); /* enable interrupts */
}

Putting it all together

Now that everything is set up, loading the interrupt table is as simple as passing the address to the lidt command in assembly. Note that it's very important to perform a long jump after initiailizing or interrupts will not work correctly. Forgetting this part actually cost me a couple days of development. After that's done, interrupts should work.

; at this point the stack should contain [esp + 4] -> first entry in EDT
; [esp] -> the return address    
load_idt:
    PUSHAD

    call setup_idt
    lidt [idt_info]

    POPAD
    ret

We can write some tests for some of the simple interrupts like the breakpoint exception just to make sure that they work.

/* 
 * Self-test a few of the recoverable interrupts to make
 * sure everything is working
 * */
void self_test_idt(void)
{
  printf("Self-testing Interrupts\n");
  asm("INT $1");
  asm("INT $3");
  printf("Self-test Complete\n");
}

Testing out Interrupts

Running the kernel shows where all the interrupts are now mapped and the self-test completes without an issue.

Pressing some keys shows that the keyboard interrupt is working and filling the screen with useless characters.

Conclusion

Interrupts honestly took me a bit of troubleshooting to get working correctly, but now at least we have a way to capture keyboard input along with other interrupts. The next thing on our list now is physical memory management.