Para descargar el Sistema Operativo tOSh, podés hacerlo desde la sección Resources de este mismo sitio.

Partes

Esta serie de artículos tiene las siguientes partes:

tOSh Kernel Panic!tOSh Kernel Panic!

Interrupciones

Que la arquitectura de las PCs de antaño y modernas estén basadas en Interrupciones, es precisamente por este chip, el Chip 8259a PIC de Intel. Una interrupción es una señal que recibe el CPU que frena temporariamente cualquier cosa que esté computando el CPU y de esta manera poder handlear (o manejar) un evento de mayor prioridad.

Para que la PC pueda interactuar con cualquier dispositivo electrónico suficientemente complejo, alguna interacción en mayor o menor medida con un CPU debe haber. Una de las opciones, era directamente ir preguntándole a cada dispositivo "¡Eu!, ¿Qué onda?".

Sin embargo al estar constantemente preguntando a los dispositivos conectados si necesitan algo, se pierde tiempo y recursos en cada una de las preguntas. A esta estrategia se la llama (Polling). Una de las soluciones propuestas para evitar hacer Polling es simplemente permitir que el CPU trabaje en sus cosas y programas normalmente y esperar por una señal en el momento en que se necesita atender una acción de alguien. Los dos dos tipos de interrupciones de CPU más comunes son por ejemplo:

  • Hardware Interrupts: Son enviadas asincrónicamente desde dispositivos físicos como mouse, teclado, discos rígidos, que cada tanto necesitan atención. Por ejemplo, apretar una tecla del teclado de una PC dispara una interrupción de hardware e INTERRUPE TODO y uno tiene que dejarlo que sea que esté ejecutando sea un Kernel o un programa en "Userspace" y debe atender inmediatamente la interrupción.
  • Software Interrupts: Excepciones o Traps, generadas internamente por el procesador o por un programa corriendo debido a un error ( como dividir por cero ) o por un request del sistema operativo via un system call.

¿Cómo se Handlea (maneja) una Interrupción?

Diego Silicio en su "sextillas interrumpidas" nos recuerda:

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

Críptico el gaucho nos ha interrumpido, pero después de terminar este artículo desasnante, su canto será atendido por todos.

Cuando ocurre una interrupción, el CPU debería salvar el contexto de ejecución en algún lado. En el stack es un buen lugar. Entonces: Salvar contexto -> Ejecutar un Interrupt Service Routine ISR (vamos muy pronto con esto) -> Recuperar el contexto y seguir laburando. Antes de siquiera empezar a armar las rutinas de interrupción, las cuales vamos a handlear solamente un par y no todas por ahora, tengo que explicar aún más cosas, porque en el mundo del PC nada es, desafortunadamente, sencillo.

El PIC - Programmable Interrupt Controller

En una PC tradicional el Programmable Interrupt Controller o Chip 8259a PIC es el intermediario entro los dispositivos y el procesador. Los dispositivos no le hablan directamente al procesador. Estos están conectados al PIC mediante líneas de interrupción llamadas Interrupt Request o IRQs. El PIC recibe esas señales y se encarga de avisarle al CPU que hay alguien pidiendo atención.

Si leemos un toque el datasheet del Chip 8259a PIC vamos a encontrarnos que tiene 8 líneas de IRQ, de IRQ0 a IRQ7, y está todo bárbaro hasta que tenemos 9 o más dispositivos que conectar simultáneamente porque nos quedamos cortos. En algún momento de los 80s efectivamente se quedaron cortos con los dispositivos y como buenos ingenieros, le chantaron otro más, creando un esquema master/slave dos Chip 8259a PIC conectados en cascada.

Línea de Hardware 8259a PIC Chip Vector Default Remapeado Modo Protegido Dispositivo Asignado Puerto I/O Usado
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)

Al usar el IRQ2 como Cascade Line entonces nos quedamos sin un IRQ pero los dispositivos attachados al slave pueden acceder a través de él.

Dispositivo → Slave → Master → CPU
Dispositivo → Master → CPU

Adicionalmente, el Chip 8259a PIC permite enmascarar interrupciones. Si estamos atendiendo el teclado y no queremos que se nos cuele la disquetera en ese momento, le decimos al PIC "esta IRQ en particular" no le des bola.

Esto es lo que hago cuando configuro las IRQs: tOSh solo implementa 3 nomás:

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

Todas las Interrupciones están enmascaradas menos estas tres que las desenmascaro especialmente para poder ser atendidas:

// 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);
}

La IDT - Interrupt Descriptor Table

La IDT es una estructura de datos binaria específica para las arquitecturas IA-32 y x86-64 y es la contrapartida del Modo Real y el Long Mode IVT. Configurar la IDT nos permite decirle a la CPU donde están nuestras ISR o Interrupt Services Routines, que son las funciones que tenemos que escribir nosotros para poder manejar las interrupciones que el procesador o dispositivos disparen. Es bastante parecida a la estructura de la GDT.

A continuación va la tabla de Interrupciones del Modo Protegido de Intel:

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.

Un error común es confundir los IRQ del Chip 8259a PIC con las INTerrupciones. NO SON LO MISMO!!!!!1 Una IRQ es la línea física del hardware. El número de interrupción es la entrada que usa el CPU en la IDT. Son dos cosas relacionadas, pero no son lo mismo, y el que las conecta es justamente el remapping del PIC.

El número de interrupción funciona como índice dentro de la tabla: llega la interrupción, el CPU busca la entrada correspondiente y salta directo al código que configuramos para atenderla.

Remapping

Ya se está oliendo el problema, ¿verdad? Los valores de los IRQ y las Interrupciones del CPU (QUE NO SON LO MISMO!!!!!!!!!!1) se solapan. Este solapamiento hace que tengamos que mover o remapear los valores de los IRQ después de la tabla de INTerrupciones del CPU. Las primeras 32 entradas de la IDT (vectores del 0 al 31) están reservadas para excepciones del propio x86 (divide by zero error, general protection fault, etc..) y los IRQ del PIC también arrancan desde del 0, solapándose.

Por eso al arrancar nuestro Kernel tenemos que reconfigurar el PIC para que corra arriba de la zona de la tabla de INTerrupciones del CPU de la siguiente manera:

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

Presione cualquier tecla para continuar…

Pongamos como ejemplo, presionar la tecla cualquiera. La lógica de cualquier teclado de PC, por cada tecla presionada, genera un scancode al controlador del teclado. Este controlador genera un IRQ, que en nuestro caso, es el IRQ1 (ver tabla más arriba). El que recibe este IRQ es el Chip 8259a PIC que verifica que la interrupción esté habilitada (o desenmascarada), la marca como pendiente y le manda una señal al CPU mediante la línea INTR. La CPU acepta la interrupción y el PIC deliverea el vector correspondiente. Si la IRQ1 esta remapeada al vector 0x21, la CPU usa ese vector para consultar la IDT y ver en que posición de memoria va a encontrar el manejador de interrupción correspondiente (interrupt handler).

Este es el de tOSh, solamente soportamos un Timer, el Teclado y el 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);
}

Y desde las rutinas en idt.asm donde declaramos como vamos a cargar el handler, creamos esta función:


[...]

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

[...]

La CPU entra al handler correspondiente a través de nuestro extern interrupt_handler que es llamado en irq_common y hace un call a nuestro código en C que termina ejecutando la función kbd_handler(). Como se puede ver, guardamos con pusha el contexto de ejecución porque cualquier cosa que haya estado haciendo la CPU, la tenemos que salvar para hacer lo que tengamos que hacer y al volver de nuestro call interrupt_handler recuperar el contexto y volver con iret de nuestra interrupción.

Tenemos todo cubierto. El kbd_handler() y todo lo que tiene que ver con el teclado es bastante un embole porque es mapear scancodes con funciones de teclado (CTRL+C hacé esto, SHIFT + minúscula transforma a mayúscula, etc…) como phun phakt, este pedazo de Kernel me llevo años desarrollarlo porque al ver lo que tenía que codear, me daba paja y por un par de años me dio cosa copiar algunas implementaciones piolas que encontré de otras personas en GitHub, pero hace poco se me pasó y me inspiré muy fuertemente. Lamentablemente hoy, no recuerdo bien de quienes, pero hey, gracias.

Como pueden ver en la funcion C interrupt_handler() hay una llamada a la función pic_eoi(). No basta con un iret para indicar que se está volviendo de una interrupción, también hay que decírselo al PIC para decirle que terminamos de atender la IRQ también.

La CPU finalmente, vuelve al programa que fue interrumpido, continuando exactamente en el EIP en la que se había detenido.

Y listo. Terminamos.

¡wh0a, te falta el APIC! (Advanced Programmable Interrupt Controller)

¡Ah no! ¡Pará! Me queda hacer una aclaración. APIC es el reemplazo del Chip 8259a PIC y hasta hoy, tOSh no lo soporta. De alguna manera, soportar APIC permitiría poder manejar entre todas las cosas que soporta, más CPUs (dualcore en adelante), pero en este momento del desarrollo me estoy peleando con enmascaramiento de interrupciones y programar un teclado.

Así que lo dejamos para más adelante. :)

Referencias