To download the Operating System tOSh, you can do it from the Resources section of this same site.

Parts

This series of articles will have the following parts:

tOSh Kernel Panic!tOSh Kernel Panic!

Interrupts

The fact that both old-school and modern PC architectures are based on [Interrupts] is precisely because of this chip, Intel’s Chip 8259a PIC.

An interrupt is a signal received by the CPU that temporarily stops whatever the CPU is computing so it can handle (or manage) an event with a higher priority.

For a PC to interact with any sufficiently complex electronic device, there has to be some kind of interaction with a CPU, to a greater or lesser extent. One option was to simply go around asking each device "Hey!, what’s up?".

However, constantly asking connected devices whether they need anything wastes time and resources on every single question. This strategy is called (Polling).

One of the solutions proposed to avoid Polling is simply to let the CPU do its thing and run its programs normally, and wait for a signal when it’s time to attend to some action from someone. The two most common types of CPU interrupts are, for example:

  • Hardware Interrupts: They are sent asynchronously by physical devices such as mice, keyboards, and hard drives, which need attention from time to time. For example, pressing a key on a PC keyboard triggers a hardware interrupt and INTERRUPTS EVERYTHING, and you have to let whatever it is executing, whether it’s a Kernel or a program in "Userspace", stop and handle the interrupt immediately.

  • Software Interrupts: Exceptions or Traps, generated internally by the processor or by a running program due to an error (such as dividing by zero) or by a request to the operating system via a system call.

How Do You Handle an Interrupt?

Diego Silicio reminds us in his "interrupted sextillas":

Original: acá le venimo' a canta'
en compás he interrumpido
usted, amigo tupido
pero nunca 'ES' como humano
PICante es el paisano
en su línea se ha unido

Translation:

here we come to sing to you
in interrupted meter
you, my dense friend
but never 'IS' like a human
PICante is the countryman
on his line he has joined

Explanation: The poem is packed with technical and linguistic wordplay. “PICante” combines PIC (Programmable Interrupt Controller) with picante (“spicy”), while “paisano” keeps the gaucho/countryman voice of the poem.

There is also a layered joke in “usted, amigo tupido / pero nunca ‘ES’ como humano”. Tupido can mean dense/thick, but tupido + ES visually produces “EStupido”, evoking estúpido (“stupid”). At the same time, ES is literally the Spanish verb es (“is”), so “nunca ES como humano” means “it is never like a human.” The capitalization makes the hidden EStupido construction more apparent.

So the poem simultaneously talks about interrupts and PICs while disguising technical terminology inside deliberately gaucho-style verse and wordplay.

The cryptic gaucho has interrupted us, but once we finish this exasperating article, his song will be attended to by everyone.

When an interrupt occurs, the CPU should save the execution context somewhere. The stack is a good place.

So: Save context -> Execute an Interrupt Service Routine ISR (we’re getting to this very soon) -> Restore the context and keep working.

Before I can even start putting together the interrupt routines, which we’re only going to handle a couple of for now, I still have to explain even more things, because in the world of the PC, nothing is, unfortunately, simple.

The PIC - Programmable Interrupt Controller

In a traditional PC, the Programmable Interrupt Controller or Chip 8259a PIC is the intermediary between devices and the processor.

Devices don’t talk directly to the processor.

They are connected to the PIC through interrupt lines called Interrupt Request or IRQs.

The PIC receives those signals and takes care of telling the CPU that someone is asking for attention.

If we read the Chip 8259a PIC datasheet for a bit, we’ll find that it has 8 IRQ lines, from IRQ0 to IRQ7, and everything is just fine until we have 9 or more devices to connect simultaneously, because we run out of them.

At some point in the ’80s they effectively ran out of IRQs for devices, and like good engineers, they slapped another one in there, creating a master/slave setup with two Chip 8259a PICs connected in cascade.

Hardware Line 8259a PIC Chip Default Vector Protected Mode Remapped Assigned Device I/O Port Used
IRQ 0 Master 0x08 0x20 Programmable Interrupt Timer (PIT) 0x20 (Command), 0x21 (Data)
IRQ 1 Master 0x09 0x21 PS/2 Keyboard 0x20 (Command), 0x21 (Data)
IRQ 2 Master 0x0A 0x22 Cascade Line (Connects Slave PIC) 0x20 (Command), 0x21 (Data)
IRQ 3 Master 0x0B 0x23 COM2 / COM4 Serial Ports 0x20 (Command), 0x21 (Data)
IRQ 4 Master 0x0C 0x24 COM1 / COM3 Serial Ports 0x20 (Command), 0x21 (Data)
IRQ 5 Master 0x0D 0x25 LPT2 Parallel Port / Sound Card 0x20 (Command), 0x21 (Data)
IRQ 6 Master 0x0E 0x26 Floppy Disk Controller 0x20 (Command), 0x21 (Data)
IRQ 7 Master 0x0F 0x27 LPT1 Parallel Port (Spurious IRQ Line) 0x20 (Command), 0x21 (Data)
IRQ 8 Slave 0x70 0x28 CMOS Real-Time Clock (RTC) 0xA0 (Command), 0xA1 (Data)
IRQ 9 Slave 0x71 0x29 ACPI / PCI Peripheral Extended 0xA0 (Command), 0xA1 (Data)
IRQ 10 Slave 0x72 0x2A Open PCI Peripheral Slot 0xA0 (Command), 0xA1 (Data)
IRQ 11 Slave 0x73 0x2B Open PCI Peripheral Slot 0xA0 (Command), 0xA1 (Data)
IRQ 12 Slave 0x74 0x2C PS/2 Mouse Port 0xA0 (Command), 0xA1 (Data)
IRQ 13 Slave 0x75 0x2D Floating Point Unit / Coprocessor 0xA0 (Command), 0xA1 (Data)
IRQ 14 Slave 0x76 0x2E Primary ATA / IDE Hard Drive 0xA0 (Command), 0xA1 (Data)
IRQ 15 Slave 0x77 0x2F Secondary ATA / IDE Hard Drive (Spurious) 0xA0 (Command), 0xA1 (Data)

By using IRQ2 as the Cascade Line, we lose one IRQ, but devices attached to the slave can access the CPU through it.

Device -> Slave -> Master -> CPU
Device -> Master -> CPU

Additionally, the Chip 8259a PIC allows interrupts to be masked. If we’re handling the keyboard and don’t want the floppy drive sneaking in at that moment, we tell the PIC "this particular IRQ" don’t bother with it.

This is what I do when configuring the IRQs: tOSh only implements 3 of them:

  • IRQ0 (timer / pit)
  • IRQ1 (keyboard)
  • IRQ6 (floppy)

All interrupts are masked except these three, which I specifically unmask so they can be handled:

// idt.c

[...]

void interrupt_handler(uint32_t number)
{
    uint32_t irq = number - 32;
    switch (irq) {

        case 0:
            // timer (IRQ 0)
            pit_irq_handler();
            break;

        case 1:
            // Keyboard (IRQ 1)
            kbd_handler();
            break;

        case 6:
            // Floppy (IRQ 6)
            floppy_irq_handler();
            break;

        default:
            break;
    }

    pic_eoi(irq);
}

The IDT - Interrupt Descriptor Table

The IDT is a binary data structure specific to the IA-32 and x86-64 architectures and is the counterpart to the [Real Mode] and Long Mode IVT.

Configuring the IDT allows us to tell the CPU where our ISRs, or Interrupt Service Routines, are located. These are the functions we have to write ourselves in order to handle interrupts triggered by the processor or by devices. It is quite similar to the structure of the GDT.

Below is Intel’s Protected Mode Interrupt Table:

Int. Nº (hex) Int. Nº (dec) Mnem. Type Err. code Name Source
0x00 0 #DE Fault No Divide Error DIV and IDIV instructions.
0x01 1 #DB Trap No Debug Exception Instruction, data, and I/O breakpoints; single-step; and others.
0x02 2 NMI Interrupt No NMI Interrupt Nonmaskable external interrupt.
0x03 3 #BP Trap No Breakpoint INT3 instruction.
0x04 4 #OF Trap No Overflow INTO instruction.
0x05 5 #BR Fault No BOUND Range Exceeded BOUND instruction.
0x06 6 #UD Fault No Invalid Opcode (Undefined Opcode) UD instruction or reserved opcode.
0x07 7 #NM Fault No Device Not Available (No Math Coprocessor) Floating-point or WAIT/FWAIT instruction.
0x08 8 #DF Abort Yes (zero) Double Fault Any instruction that can generate an exception, an NMI (Non-maskable interrupt), or an INTR (external interrupt).
0x09 9 — Fault No Coprocessor Segment Overrun (reserved) Floating-point instruction.
0x0A 10 #TS Fault Yes Invalid TSS Task switch or TSS access.
0x0B 11 #NP Fault Yes Segment Not Present Loading segment registers or accessing system segments.
0x0C 12 #SS Fault Yes Stack-Segment Fault Stack operations and SS (stack segment) register loads.
0x0D 13 #GP Fault Yes General Protection Any memory reference and other protection checks.
0x0E 14 #PF Fault Yes Page Fault Any memory reference.
0x0F 15 — — No Intel reserved. Do not use. —
0x10 16 #MF Fault No x87 FPU Floating-Point Error (Math Fault) x87 FPU floating-point or WAIT/FWAIT instruction.
0x11 17 #AC Fault Yes (zero) Alignment Check Any data reference in memory.
0x12 18 #MC Abort No Machine Check Error codes (if any) and source are model dependent.
0x13 19 #XM Fault No SIMD Floating-Point Exception SSE/SSE2/SSE3 floating-point instructions.
0x14 20 #VE Fault No Virtualization Exception EPT violations.
0x15 21 #CP Fault Yes Control Protection Exception RET, IRET, RSTORSSP, and SETSSBSY instructions can generate this exception. When CET indirect branch tracking is enabled, this exception can be generated due to a missing ENDBRANCH instruction at target of an indirect call or jump.
0x16–0x1F 22–31 — — — Reserved for future use as CPU exception vectors. —
0x20–0xFF 32–255 — Interrupt No — External interrupts.

A common mistake is confusing the IRQs of the Chip 8259a PIC with the INTerrupts. THEY ARE NOT THE SAME!!!!!1

An IRQ is the physical hardware line. The interrupt number is the entry the CPU uses in the IDT.

They are two related things, but they are not the same, and the thing that connects them is precisely the PIC’s remapping.

The interrupt number works as an index into the table: the interrupt arrives, the CPU looks up the corresponding entry, and jumps straight to the code we configured to handle it.

Remapping

You can already smell the problem, right? The IRQ values and the CPU Interrupts (WHICH ARE NOT THE SAME THING!!!!!!!!!!1) overlap.

This overlap means we have to move or remap the IRQ values to after the CPU’s INTerrupt table.

The first 32 entries of the IDT (vectors 0 through 31) are reserved for exceptions from the x86 itself (_divide by zero error, general protection fault, etc.._) and the PIC’s IRQs also start at 0, overlapping with them.

That’s why when booting our Kernel, we have to reconfigure the PIC so that it runs above the CPU’s INTerrupt table area, like this:

PIC Original IRQ Destination IRQ
Master IRQ0 - IRQ7 INT 32 - INT 39
Slave IRQ8 - IRQ15 INT 40 - INT 47

Press Any Key to Continue…

Let’s use pressing the any key as an example.

The logic of any PC keyboard, for every key pressed, generates a scancode for the keyboard controller.

This controller generates an IRQ, which in our case is IRQ1 (see the table above). The one receiving this IRQ is the Chip 8259a PIC, which checks that the interrupt is enabled (or unmasked), marks it as pending, and sends a signal to the CPU through the INTR line.

The CPU accepts the interrupt and the PIC delivers the corresponding vector. If IRQ1 is remapped to vector 0x21, the CPU uses that vector to look up the IDT and see at which memory address it will find the corresponding interrupt handler.

This is tOSh’s; we only support a Timer, the Keyboard, and the Floppy:

/* de idt.asm, declaramos esta y otras funciones como "extern" */

void interrupt_handler(uint32_t number)
{
    uint32_t irq = number - 32;
    switch (irq) {

        case 0:
            // timer (IRQ 0)
            pit_irq_handler();
            break;

        case 1:
            // Keyboard (IRQ 1)
            kbd_handler();
            break;

        case 6:
            // Floppy (IRQ 6)
            floppy_irq_handler();
            break;

        default:
            break;
    }

    pic_eoi(irq);
}

And from the routines in idt.asm, where we declare how we’re going to load the handler, we create this function:


[...]

extern interrupt_handler

[...]

irq_common:
    pusha

    ; ESP apunta ahora al comienzo de pusha:
    ;
    ; [ESP + 0]  EDI
    ; [ESP + 4]  ESI
    ; [ESP + 8]  EBP
    ; [ESP + 12] original ESP
    ; [ESP + 16] EBX
    ; [ESP + 20] EDX
    ; [ESP + 24] ECX
    ; [ESP + 28] EAX
    ; [ESP + 32] interrupt number
    ; [ESP + 36] error code
    ; [ESP + 40] EIP
    ; [ESP + 44] CS
    ; [ESP + 48] EFLAGS

    mov eax, [esp + 32]         ; +32 = interrupt number 
    push eax
    call interrupt_handler
    add esp, 4

    popa
    ; remove interrupt number + fake error code
    add esp, 8
    iret

[...]

The CPU enters the corresponding handler through our extern interrupt_handler, which is called from irq_common and makes a call to our C code, which ends up executing the kbd_handler() function.

As you can see, we save the execution context with pusha because whatever the CPU had been doing, we have to save it in order to do whatever we need to do and, when returning from our call interrupt_handler, restore the context and return from our interrupt with iret.

We’ve got everything covered. The kbd_handler() and everything related to the keyboard is pretty damn boring because it’s just mapping scancodes to keyboard functions (CTRL+C does this, SHIFT + lowercase turns it into uppercase, etc…) and as phun phakt, this piece of Kernel took me years to develop because when I looked at what I had to code, I couldn’t be arsed, and for a couple of years I felt weird about copying some cool implementations I found from other people on GitHub, but recently I got over it and got heavily inspired. Unfortunately, I don’t remember exactly who they were today, but hey, thanks.

As you can see in the C function interrupt_handler(), there’s a call to the pic_eoi() function. An iret alone isn’t enough to indicate that we’re returning from an interrupt; we also have to tell the PIC that we’re done handling the IRQ.

The CPU finally returns to the program that was interrupted, continuing exactly at the EIP where it had stopped.

And that’s it. We’re done.

wh0a, you’re missing the APIC! (Advanced Programmable Interrupt Controller)

Oh no! Wait! I still need to make one clarification. APIC is the replacement for the Chip 8259a PIC, and to this day, tOSh doesn’t support it.

In some way, supporting APIC would allow us to handle, among all the things it supports, more CPUs (dual-core and up), but at this point in development I’m busy fighting with interrupt masking and programming a keyboard.

So we’ll leave that for later. :)

References