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:
- Parte I - Bootloader
- Parte II - Stage 1
- Parte III - Saltando a C
- Parte IV - Interrupciones <— Estás acá
- Parte V - Sistema de Archivos y Driver de Disquetera
- Parte VI - Booteando en diferentes tipos de máquinas
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
atenderinmediatamente 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
requestdel sistema operativo via unsystem 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. :)