StellatorOS: Interrupciones modo protegido IDT/ISR/IRQ


Una petición de interrupción es una señal para indicar al procesador que algo requiere su atención inmediata.Estas peticiones pueden ser generadas tanto por dispositivos hardware(IRQ) como por programas e incluso en circunstancias especiales (errores generalmente) por el propio procesador.

 

Antes de comenzar es recomendable que leas estos artículos anteriores:

Sin interrupciones la CPU debería chequear constantemente los dispositivos para comprobar su actividad, estas permiten que los dispositivos puedan permanecer en silencio hasta el momento que requieren atención del procesador.

Los dispositivos hardware comunican sus interrupciones a través de unas líneas en el bus de control, cada una de estas líneas se llama IRQ. Estas líneas llegan al controlador de interrupciones PIC y este encola y prioriza las interrupciones antes de comunicárselas a la CPU.

Estas IRQs son limitadas, en total se disponen de 16(0-15) esto es debido a que disponemos de dos PICs cada uno de ellos con 8 IRQs pero hay que tener en cuenta que están conectados en cascada, el mas cercano a la CPU es el MASTER y el mas lejano el SLAVE, la conexión del MASTER(IRQ2) con el SLAVE(IRQ9) consume una IRQ en cada PIC.

Cuando se produce una interrupción hardware se siguen los siguientes pasos:

  • El PIC MASTER avisa a la CPU de que hay interrupciones pendientes a través de una pista física INTR(Interrupt Request)
  • Cuando la CPU recibe la interrupción responde al PIC a través de una pista física INTA(Interrupt Acknowledge), la CPU envía dos veces esta señal, una de aviso y otra para que el PIC coloque el id de interrupción en el bus de datos
  • El PIC correspondiente escribe el id de interrupción en el bus de datos
  • La CPU lo lee y con este id localiza la rutina de servicio(ISR: Interrupt Service Routine) asociada a tal interrupción
  • La CPU termina la instrucción que está ejecutando
  • La CPU guarda el contenido de los registros
  • La CPU deshabilita el sistema de interrupciones
  • La CPU ejecuta la rutina de servicio
  • Cuando se ha ejecutado la rutina de servicio la CPU notifica al PIC correspondiente EOI(End Of Interrupt)
  • Vuelve a habilitar el sistema de interrupciones
  • Termina con una instrucción IRET(Interrupt Return) que restituye el contenido de los registros: CS, EIP, EFLAGS, SS, ESP

La rutina de servicio comentada anteriormente se localiza consultando la IDT(nterrupt Descriptor Table), esta no es mas que una tabla que relaciona el id de interrupción con la posición de memoria de la ISR, los códigos van de 0-255, si ocurre una interrupción y no hay entrada en la IDT para dicha interrupción el procesador entrará en panic y se reiniciará.

NOTA: En modo real 16 bits la IDT es llamada IVT(interrupt vector table)

No todas las interrupciones tienen porque ser hardware.

  • Interrupción hardware: Ocurren cuando un dispositivo necesita atención del procesador y genera una señal eléctrica en la línea IRQ que tiene asignada. Esta señal es recogida y procesada por el controlador de excepciones PIC antes de ser enviada al procesador.
    • Interrupción enmascarable: Bajo control del software, el procesador puede aceptar o ignorar la señal de interrupción. Cuando el procesador recibe una de estas insterrupciones responde con un INTA al PIC y busca la rutina de servicio asociada a la interrupción.
    • Interrupción no enmascarable: Significa que la interrupción no puede ser deshabilitada por software.  Este tipo de interrupciones ocurren cuando se recibe una señal en la pata NMI(Nonmaskable Interrupt) del procesador. Se reservan para casos críticos, por ejemplo cuando se detecta un error de paridad en la memoria. Además son de prioridad más alta que las enmascarables.
      Cuando el procesador recibe una de estas instrucciones no se genera ningún INTA y el procesador le asigna un 2 como id de excepción.
  • Interrupciones software: Permiten generar por software cualquiera de los 256 tipos de interrupción enmascarable. El proceso seguido es exactamente el mismo que si se recibe una interrupción hardware en la pata INTR salvo que en este caso se conoce el id de interrupción y no se requiere ningún INTA.
  • Excepciones del procesador: Durante el funcionamiento del procesador pueden ocurrir circunstancias excepcionales, por ejemplo una división por cero. En estos casos, el procesador genera una excepción que es tratada como si fuese una interrupción software, con la diferencia de que el id de interrupción depende del tipo de excepción.

Las excepciones siguen un orden de prioridad:

  • Excepciones del procesador
  • Interrupciones software
  • Interrupciones hardware no enmascarables
  • Interrupciones hardware enmascarables

En la siguiente tabla podemos ver la relación de las interrupciones con sus IRQs tal y como las carga la BIOS por defecto:

Master 8259:
IVT | INT # | IRQ # | Description
-----------+-------+-------+------------------------------
0x0020     | 0x08  | 0     | PIT
0x0024     | 0x09  | 1     | Keyboard
0x0028     | 0x0A  | 2     | 8259A slave controller
0x002C     | 0x0B  | 3     | COM2 / COM4
0x0030     | 0x0C  | 4     | COM1 / COM3
0x0034     | 0x0D  | 5     | LPT2
0x0038     | 0x0E  | 6     | Floppy controller
0x003C     | 0x0F  | 7     | LPT1
Slave 8259:
IVT | INT # | IRQ # | Description
-----------+-------+-------+------------------------------
0x01C0     | 0x70  | 8     | RTC
0x01C4     | 0x71  | 9     | Unassigned
0x01C8     | 0x72  | 10    | Unassigned
0x01CC     | 0x73  | 11    | Unassigned
0x01D0     | 0x74  | 12    | Mouse controller
0x01D4     | 0x75  | 13    | Math coprocessor
0x01D8     | 0x76  | 14    | Hard disk controller 1
0x01DC     | 0x77  | 15    | Hard disk controller 2

Cuando se entra en modo protegido la propia CPU utilizará los códigos 0x0 - 0x1F para interrupciones internas creando un solapamiento(bug de intel) por lo tanto tendremos que remapear las interrupciones originales a otros códigos.

Lista de interrupciones en modo protegido:

IVT        | INT #     | Description
-----------+-----------+-----------------------------------
0x0000     | 0x00      | Divide by 0
0x0004     | 0x01      | Reserved
0x0008     | 0x02      | NMI Interrupt
0x000C     | 0x03      | Breakpoint (INT3)
0x0010     | 0x04      | Overflow (INTO)
0x0014     | 0x05      | Bounds range exceeded (BOUND)
0x0018     | 0x06      | Invalid opcode (UD2)
0x001C     | 0x07      | Device not available (WAIT/FWAIT)
0x0020     | 0x08      | Double fault
0x0024     | 0x09      | Coprocessor segment overrun
0x0028     | 0x0A      | Invalid TSS
0x002C     | 0x0B      | Segment not present
0x0030     | 0x0C      | Stack-segment fault
0x0034     | 0x0D      | General protection fault
0x0038     | 0x0E      | Page fault
0x003C     | 0x0F      | Reserved
0x0040     | 0x10      | x87 FPU error
0x0044     | 0x11      | Alignment check
0x0048     | 0x12      | Machine check
0x004C     | 0x13      | SIMD Floating-Point Exception
0x00xx     | 0x14-0x1F | Reserved
0x0xxx     | 0x20-0xFF | User definable(32-255)

Los código conflictivos son:

0x0020     | 0x08  | 0     | PIT
0x0024     | 0x09  | 1     | Keyboard
0x0028     | 0x0A  | 2     | 8259A slave controller
0x002C     | 0x0B  | 3     | COM2 / COM4
0x0030     | 0x0C  | 4     | COM1 / COM3
0x0034     | 0x0D  | 5     | LPT2
0x0038     | 0x0E  | 6     | Floppy controller
0x003C     | 0x0F  | 7     | LPT1
0x0020     | 0x08      | Double fault
0x0024     | 0x09      | Coprocessor segment overrun
0x0028     | 0x0A      | Invalid TSS
0x002C     | 0x0B      | Segment not present
0x0030     | 0x0C      | Stack-segment fault
0x0034     | 0x0D      | General protection fault
0x0038     | 0x0E      | Page fault
0x003C     | 0x0F      | Reserved

Podemos ver la lista completa de interrupciones en esta web:

https://dos4gw.org/Interrupts_-_List

Las interrupciones del modo protegido abarcan los ids de interrupción 0x00-0x1F(0-31), por lo tanto quedan libres 0x20-0x1f(32-255), es en este último rango donde vamos a remapear las interrupciones originales de los PICs.

Tras el remapeo quedará del siguiente modo:

La reprogramación de los PICs será a través de los registros I/O-ports:

0x20: Port de control del MASTER
0x21: Port de datos del MASTER
0xA0: Port de control del SLAVE
0xA1: Port de datos del SLAVE

Los PICs aceptan dos tipos de comandos: los ICW(Inicialization Command Word) que lo inicializan, y los OCW(Operation Command Word) que permiten programar la modalidad de funcionamiento, nosotros solo vamos a utilizar los ICW.

Antes de que los PICs de un sistema comiencen a trabajar deben recibir una secuencia de ICWs que los inicialice, el PIC espera recibir secuencialmente estos comandos unos tras otro, los tres primeros son obligatorios, el ICW4 es opcional.

El ICW1 se compone de la suma de dos valores:

  • 10: Indica que se trata del primer comando ICW1
  • 0/1: Indica si se va a enviar un ICW4

En nuestro caso sí que vamos a enviar un ICW4 por lo tanto inicamos la reprogramación con 0x11, este comando hace que el PIC se ponga a la espera de 3 comandos mas en el puerto de datos:

  • Vector offset ICW2: Indica el id de interrupción inicial en el que comenzar el mapeo de las IRQs, por defecto es PIC-M: 0x08 de modo que el id 0x08 queda asignado a la IRQ0, el 0x09 a la IRQ1 y así sucesivamente en el PIC-S el razonamiento es exactamente el mismo, 0x70-IRQ8, 0x71-IRQ9 y así sucesivamente.
  • Modo en que están conectados los dos PICs(IRQ2-IRQ9) ICW3
  • CPU sobre la que estamos trabajando ICW4(0:8085/1:8086)

NOTA: Recordemos que el PIC-M abarca las IRQs 0-7 y PIC-S 8-15, este detalle es importante cuando se indica como están conectados los PICs entre ellos

A parte de la reconfiguración también haremos un enmascaramiento de las interrupciones para mas adelante desenmascarar las que nos interesen.

El código de remapeo y enmascaramiento en C sería el siguiente.

vi kernel/irqs.h
void remap_irqs();
void mask_irq(unsigned char IRQline);
void unmask_irq(unsigned char IRQline);
vi kernel/irqs.c
#include "irqs.h"
#include "../drivers/ports.h"

void remap_irqs() {
    port_byte_out(0x20, 0x11); // Reprogramming command PIC-M
    port_byte_out(0xA0, 0x11); // Reprogramming command PIC-S

    port_byte_out(0x21, 0x20); // vector offset PIC-M to 0x20 (32 decimal)
    port_byte_out(0xA1, 0x28); // vector offset PIC-S to 0x28 (40 decimal)

    port_byte_out(0x21, 0x04); // PIC-S at IRQ2 (0000 0100)
    port_byte_out(0xA1, 0x02); // PIC-M at IRQ9 (0000 0010)

    port_byte_out(0x21, 0x01); // 8086 CPU PIC-M
    port_byte_out(0xA1, 0x01); // 8086 CPU PIC-S

    port_byte_out(0x21, 0xFF); // disable all IRQs on PIC-M
    port_byte_out(0xA1, 0xFF); // disable all IRQs on PIC-S
    
    //port_byte_out(0x21, 0x00); // enable all IRQs on PIC-M
    //port_byte_out(0xA1, 0x00); // enable all IRQs on PIC-S
}

void mask_irq(unsigned char IRQline) {
    unsigned short port;
    unsigned char value;
    
    if(IRQline < 8) {
        port = 0x21;
    } else {
        port = 0xA1;
        IRQline -= 8;
    }
    value = port_byte_in(port) | (1 << IRQline);
    port_byte_out(port, value);     
}

void unmask_irq(unsigned char IRQline) {
    unsigned short port;
    unsigned char value;
 
    if(IRQline < 8) {
        port = 0x21;
    } else {
        port = 0xA1;
        IRQline -= 8;
    }
    value = port_byte_in(port) & ~(1 << IRQline);
    port_byte_out(port, value); 
}

Una vez remapeadas las IRQs debemos definir nuestras rutinas de servicio en ASM, estas a su vez llamarán al código en C que se encargará de obtener el id de interrupción y actuar en consecuencia.

Hay varios aspectos que debemos señalar para comprender el funcionamiento de las ISRs:

  • C por defecto utiliza el call convention stdcall, esto quiere decir que si desde ASM llamamos a una función en C que necesita argumentos debemos pushearlos en la pila antes de realizar la llamada
  • Para que C pueda hacer referencia a símbolos externos como las funciones ISR del código ASM debemos definir estos símbolos como externos
  • Hay interrupciones que pushean de forma automática un código de error en la pila antes de generar la interrupción, por lo tanto para que todo el proceso sea mas uniforme pushearemos un byte nulo en la pila en las interrupciones que no proveen de dicha información
  • Cuando se pushea un dato en la pila aunque este sea de un tipo el dato final en RAM siempre es de 16/32/64 bits con el padding que corresponda

El procedimiento a seguir por las ISRs es el siguiente:

  • Deshabilitamos todas interrupciones externas que sean enmascarables, no afecta a las excepciones ni a las interrupciones NMI
  • Pushea el id de interrupción
  • Pushea todos los registros generales
  • Copia ds a ax
  • Pushea ax(ds)
  • Necesitamos el descriptor de la tabla GDT correspondiente al segmento de datos, si hacemos memoria teníamos 3 descriptores cada uno de ellos ocupa 8 bytes por lo tanto el offset a aplicar en la GDT para leer el descriptor del segmento de datos es 8*2=16 que si lo pasamos a hexa: 0x10. Seteamos el registro ax con este valor
  • Seteamos todos los registros de segmento al descriptor del segmento de datos
  • Llamamos a la ISR en C(esta leerá los argumentos de la pila), acto seguido empieza el proceso de restauración de los registros de la CPU al estado en el que se encontrban antes de la interrupción
  • Cuando retornemos del mundo de C popeamos a ax el ds guardado
  • Seteamos todos los registros de segmento al ds original
  • Popeamos todos los registros generales
  • Movemos el stack pointer 8 bytes arriba para dejar las posiciones de ram utilizadas por los códigos de error de las interrupciones disponibles para otros usos, recordemos que para subir en la pila hay que sumar y que pusheabamos dos bytes pero al estar en 32bits ocupan un total de 64bits que son los 8bytes que desplazamos el stackpointer(add esp, 8)
  • Habilitamos las interrupciones
  • Popeamos cs, ip, flags, ss y sp a los valores originales con iret

NOTA: Las ISRs asociadas a una IRQ son exactamente iguales pero con la diferencia de que justo después de haber llamado a la ISR en C no se popea el valor de ds en ax si no en ex.

vi kernel/interrupts.asm
; Defined in isr.c
[extern isr_handler]
[extern irq_handler]

; Common ISR code
isr_common_stub:
    ; 1. Save CPU state
	pusha ; Pushes edi,esi,ebp,esp,ebx,edx,ecx,eax
	mov ax, ds ; Lower 16-bits of eax = ds.
	push eax ; save the data segment descriptor
	mov ax, 0x10  ; GDT data segment descriptor: 8*2=16 -> 0x10h
	mov ds, ax
	mov es, ax
	mov fs, ax
	mov gs, ax
	
    ; 2. Call C handler
	call isr_handler
	
    ; 3. Restore state
	pop eax
	mov ds, ax
	mov es, ax
	mov fs, ax
	mov gs, ax
	popa
	add esp, 8 ; Cleans up the pushed error code and pushed ISR number
	sti ; Reenable interruptions
	iret ; pops 5 things at once: CS, EIP, EFLAGS, SS, and ESP


; Common IRQ code. Identical to ISR code except for the 'call' and the 'pop ebx'
irq_common_stub:
    pusha
    mov ax, ds
    push eax
    mov ax, 0x10
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    
    call irq_handler ; Different than the ISR code
    
    pop ebx  ; Different than the ISR code
    mov ds, bx
    mov es, bx
    mov fs, bx
    mov gs, bx
    popa
    add esp, 8
    sti ; Reenable interruptions
    iret

; We don't get information about which interrupt was caller when the handler is run, so we will need to have a different handler for every interrupt.
; Furthermore, some interrupts push an error code onto the stack but others don't, so we will push a dummy error code for those which don't, so that
; we have a consistent stack for all of them.

; First make the ISRs global
global isr0
global isr1
global isr2
global isr3
global isr4
global isr5
global isr6
global isr7
global isr8
global isr9
global isr10
global isr11
global isr12
global isr13
global isr14
global isr15
global isr16
global isr17
global isr18
global isr19
global isr20
global isr21
global isr22
global isr23
global isr24
global isr25
global isr26
global isr27
global isr28
global isr29
global isr30
global isr31

; IRQs ISRs
; PIC-M
global irq0
global irq1
global irq2
global irq3
global irq4
global irq5
global irq6
global irq7
; PIC-S
global irq8
global irq9
global irq10
global irq11
global irq12
global irq13
global irq14
global irq15

; 0: Divide By Zero Exception
isr0:
    cli ; Disable interruptions temporaly
    push byte 0
    push byte 0
    jmp isr_common_stub

; 1: Debug Exception
isr1:
    cli
    push byte 0
    push byte 1
    jmp isr_common_stub

; 2: Non Maskable Interrupt Exception
isr2:
    cli
    push byte 0
    push byte 2
    jmp isr_common_stub

; 3: Int 3 Exception
isr3:
    cli
    push byte 0
    push byte 3
    jmp isr_common_stub

; 4: INTO Exception
isr4:
    cli
    push byte 0
    push byte 4
    jmp isr_common_stub

; 5: Out of Bounds Exception
isr5:
    cli
    push byte 0
    push byte 5
    jmp isr_common_stub

; 6: Invalid Opcode Exception
isr6:
    cli
    push byte 0
    push byte 6
    jmp isr_common_stub

; 7: Coprocessor Not Available Exception
isr7:
    cli
    push byte 0
    push byte 7
    jmp isr_common_stub

; 8: Double Fault Exception (With Error Code!)
isr8:
    cli
    push byte 8
    jmp isr_common_stub

; 9: Coprocessor Segment Overrun Exception
isr9:
    cli
    push byte 0
    push byte 9
    jmp isr_common_stub

; 10: Bad TSS Exception (With Error Code!)
isr10:
    cli
    push byte 10
    jmp isr_common_stub

; 11: Segment Not Present Exception (With Error Code!)
isr11:
    cli
    push byte 11
    jmp isr_common_stub

; 12: Stack Fault Exception (With Error Code!)
isr12:
    cli
    push byte 12
    jmp isr_common_stub

; 13: General Protection Fault Exception (With Error Code!)
isr13:
    cli
    push byte 13
    jmp isr_common_stub

; 14: Page Fault Exception (With Error Code!)
isr14:
    cli
    push byte 14
    jmp isr_common_stub

; 15: Reserved Exception
isr15:
    cli
    push byte 0
    push byte 15
    jmp isr_common_stub

; 16: Floating Point Exception
isr16:
    cli
    push byte 0
    push byte 16
    jmp isr_common_stub

; 17: Alignment Check Exception
isr17:
    cli
    push byte 0
    push byte 17
    jmp isr_common_stub

; 18: Machine Check Exception
isr18:
    cli
    push byte 0
    push byte 18
    jmp isr_common_stub

; 19: Reserved
isr19:
    cli
    push byte 0
    push byte 19
    jmp isr_common_stub

; 20: Reserved
isr20:
    cli
    push byte 0
    push byte 20
    jmp isr_common_stub

; 21: Reserved
isr21:
    cli
    push byte 0
    push byte 21
    jmp isr_common_stub

; 22: Reserved
isr22:
    cli
    push byte 0
    push byte 22
    jmp isr_common_stub

; 23: Reserved
isr23:
    cli
    push byte 0
    push byte 23
    jmp isr_common_stub

; 24: Reserved
isr24:
    cli
    push byte 0
    push byte 24
    jmp isr_common_stub

; 25: Reserved
isr25:
    cli
    push byte 0
    push byte 25
    jmp isr_common_stub

; 26: Reserved
isr26:
    cli
    push byte 0
    push byte 26
    jmp isr_common_stub

; 27: Reserved
isr27:
    cli
    push byte 0
    push byte 27
    jmp isr_common_stub

; 28: Reserved
isr28:
    cli
    push byte 0
    push byte 28
    jmp isr_common_stub

; 29: Reserved
isr29:
    cli
    push byte 0
    push byte 29
    jmp isr_common_stub

; 30: Reserved
isr30:
    cli
    push byte 0
    push byte 30
    jmp isr_common_stub

; 31: Reserved
isr31:
    cli
    push byte 0
    push byte 31
    jmp isr_common_stub



; IRQ ISRs
irq0:
	cli ; Disable interruptions temporaly
	push byte 0
	push byte 32
	jmp irq_common_stub

irq1: ; KEYBOARD
	cli
	push byte 1
	push byte 33
	jmp irq_common_stub

irq2:
	cli
	push byte 2
	push byte 34
	jmp irq_common_stub

irq3:
	cli
	push byte 3
	push byte 35
	jmp irq_common_stub

irq4:
	cli
	push byte 4
	push byte 36
	jmp irq_common_stub

irq5:
	cli
	push byte 5
	push byte 37
	jmp irq_common_stub

irq6:
	cli
	push byte 6
	push byte 38
	jmp irq_common_stub

irq7:
	cli
	push byte 7
	push byte 39
	jmp irq_common_stub

irq8:
	cli
	push byte 8
	push byte 40
	jmp irq_common_stub

irq9:
	cli
	push byte 9
	push byte 41
	jmp irq_common_stub

irq10:
	cli
	push byte 10
	push byte 42
	jmp irq_common_stub

irq11:
	cli
	push byte 11
	push byte 43
	jmp irq_common_stub

irq12:
	cli
	push byte 12
	push byte 44
	jmp irq_common_stub

irq13:
	cli
	push byte 13
	push byte 45
	jmp irq_common_stub

irq14:
	cli
	push byte 14
	push byte 46
	jmp irq_common_stub

irq15:
	cli
	push byte 15
	push byte 47
	jmp irq_common_stub

El siguiente paso es declarar como externas las ISRs de nuestro ASM, algunas funciones relacionadas con las ISRs y las interrupciones y una estructura llamada interrupt_stack_data que contendrá todos los valores que se hayan pusheado a la pila antes de llamar a la función, de este modo podremos leer el id de interrupción y el código de error generado por la interrupción, recordemos que no todas las interrupciones generan estos códigos.

vi kernel/isrs.h
#ifndef ISRS_H
#define ISRS_H

/* ISRs reserved for CPU exceptions, extern because they are defined in ASM code */
extern void isr0(void);
extern void isr1(void);
extern void isr2(void);
extern void isr3(void);
extern void isr4(void);
extern void isr5(void);
extern void isr6(void);
extern void isr7(void);
extern void isr8(void);
extern void isr9(void);
extern void isr10(void);
extern void isr11(void);
extern void isr12(void);
extern void isr13(void);
extern void isr14(void);
extern void isr15(void);
extern void isr16(void);
extern void isr17(void);
extern void isr18(void);
extern void isr19(void);
extern void isr20(void);
extern void isr21(void);
extern void isr22(void);
extern void isr23(void);
extern void isr24(void);
extern void isr25(void);
extern void isr26(void);
extern void isr27(void);
extern void isr28(void);
extern void isr29(void);
extern void isr30(void);
extern void isr31(void);

/* IRQs interruptions, extern because they are defined in ASM code */
// PIC-M
extern void irq0(void);
extern void irq1(void);
extern void irq2(void);
extern void irq3(void);
extern void irq4(void);
extern void irq5(void);
extern void irq6(void);
extern void irq7(void);
// PIC-S
extern void irq8(void);
extern void irq9(void);
extern void irq10(void);
extern void irq11(void);
extern void irq12(void);
extern void irq13(void);
extern void irq14(void);
extern void irq15(void);


/* Struct which aggregates many registers */
typedef struct {
   unsigned int ds; /* Data segment selector */
   unsigned int edi, esi, ebp, esp, ebx, edx, ecx, eax; /* Pushed by pusha. */
   unsigned int int_no, err_code; /* Interrupt number and error code (if applicable) */
   unsigned int eip, cs, eflags, useresp, ss; /* Pushed by the processor automatically */
} interrupt_stack_data;

void enable_interrupts();
void disable_interrupts();

void isr_install();
void isr_handler(interrupt_stack_data interrupt_stack_data);
void irq_handler(interrupt_stack_data interrupt_stack_data);

#endif

En isrs.c llamamos a la función set_idt_gate que nos generará los vectores de interrupción en la tabla IDT, le pasamos el número de interrupción y la dirección de memoria donde se encuentran nuestras ISRs escritas en ASM. Definimos un array con los mensajes asociados a cada interrupción/IRQ, el id de interrupción actuará a modo de índice. También podemos ver un par de funciones para habilitar/deshabilitar las interrupciones, finalmente la función a la que llaman todas las ISRs cuando se produce una interrupción es isr_handler o irq_handler en caso de tratarse de una IRQ.

vi kernel/isrs.c
#include "isrs.h"
#include "idt.h"
#include "../drivers/screen.h"
#include "../drivers/ports.h"

// IDT vectors
void isr_install() {
    set_idt_gate(0, (unsigned int)isr0);
    set_idt_gate(1, (unsigned int)isr1);
    set_idt_gate(2, (unsigned int)isr2);
    set_idt_gate(3, (unsigned int)isr3);
    set_idt_gate(4, (unsigned int)isr4);
    set_idt_gate(5, (unsigned int)isr5);
    set_idt_gate(6, (unsigned int)isr6);
    set_idt_gate(7, (unsigned int)isr7);
    set_idt_gate(8, (unsigned int)isr8);
    set_idt_gate(9, (unsigned int)isr9);
    set_idt_gate(10, (unsigned int)isr10);
    set_idt_gate(11, (unsigned int)isr11);
    set_idt_gate(12, (unsigned int)isr12);
    set_idt_gate(13, (unsigned int)isr13);
    set_idt_gate(14, (unsigned int)isr14);
    set_idt_gate(15, (unsigned int)isr15);
    set_idt_gate(16, (unsigned int)isr16);
    set_idt_gate(17, (unsigned int)isr17);
    set_idt_gate(18, (unsigned int)isr18);
    set_idt_gate(19, (unsigned int)isr19);
    set_idt_gate(20, (unsigned int)isr20);
    set_idt_gate(21, (unsigned int)isr21);
    set_idt_gate(22, (unsigned int)isr22);
    set_idt_gate(23, (unsigned int)isr23);
    set_idt_gate(24, (unsigned int)isr24);
    set_idt_gate(25, (unsigned int)isr25);
    set_idt_gate(26, (unsigned int)isr26);
    set_idt_gate(27, (unsigned int)isr27);
    set_idt_gate(28, (unsigned int)isr28);
    set_idt_gate(29, (unsigned int)isr29);
    set_idt_gate(30, (unsigned int)isr30);
    set_idt_gate(31, (unsigned int)isr31);

    // IRQs
    // PIC-M
    set_idt_gate(32, (unsigned int)irq0);
    set_idt_gate(33, (unsigned int)irq1);
    set_idt_gate(34, (unsigned int)irq2);
    set_idt_gate(35, (unsigned int)irq3);
    set_idt_gate(36, (unsigned int)irq4);
    set_idt_gate(37, (unsigned int)irq5);
    set_idt_gate(38, (unsigned int)irq6);
    set_idt_gate(39, (unsigned int)irq7);
    // PIC-S
    set_idt_gate(40, (unsigned int)irq8);
    set_idt_gate(41, (unsigned int)irq9);
    set_idt_gate(42, (unsigned int)irq10);
    set_idt_gate(43, (unsigned int)irq11);
    set_idt_gate(44, (unsigned int)irq12);
    set_idt_gate(45, (unsigned int)irq13);
    set_idt_gate(46, (unsigned int)irq14);
    set_idt_gate(47, (unsigned int)irq15);

    set_idt(); // Load IDT descriptor in idtr register
}

void enable_interrupts(){
    __asm__ ("sti");
}

void disable_interrupts(){
    __asm__ ("cli");
}


/* To print the message which defines every exception */
char *exception_messages[] = {
    "Division By Zero\n",
    "Debug\n",
    "Non Maskable Interrupt\n",
    "Breakpoint\n",
    "Into Detected Overflow\n",
    "Out of Bounds\n",
    "Invalid Opcode\n",
    "No Coprocessor\n",

    "Double Fault\n",
    "Coprocessor Segment Overrun\n",
    "Bad TSS\n",
    "Segment Not Present\n",
    "Stack Fault\n",
    "General Protection Fault\n",
    "Page Fault\n",
    "Unknown Interrupt\n",

    "Coprocessor Fault\n",
    "Alignment Check\n",
    "Machine Check\n",
    "Reserved\n",
    "Reserved\n",
    "Reserved\n",
    "Reserved\n",
    "Reserved\n",

    "Reserved\n",
    "Reserved\n",
    "Reserved\n",
    "Reserved\n",
    "Reserved\n",
    "Reserved\n",
    "Reserved\n",
    "Reserved\n"
};

/* To print the message which defines every IRQ */
char *irq_messages[] = {
    // PIC-M
    "Programmable interval timer IRQ\n",
    "Keyboard IRQ\n",
    "8259A slave controller IRQ\n",
    "COM2 / COM4 IRQ\n",
    "COM1 / COM3 IRQ\n",
    "LPT2 IRQ\n",
    "Floppy controller IRQ\n",
    "LPT1 IRQ\n",
    // PIC-S
    "RTC IRQ\n",
    "Unassigned IRQ\n",
    "Unassigned IRQ\n",
    "Unassigned IRQ\n",
    "Mouse controller IRQ\n",
    "Math coprocessor IRQ\n",
    "Hard disk controller 1 IRQ\n",
    "Hard disk controller 2 IRQ\n"
};

void isr_handler(interrupt_stack_data interrupt_stack_data) {
        print_string("Exception interrupt detected\n", -1, -1);
        if ( interrupt_stack_data.int_no >= 0 && interrupt_stack_data.int_no <= 31){
            print_string(exception_messages[interrupt_stack_data.int_no], -1, -1);
            print_string("--------------------------------\n", -1, -1);
        } else {
            print_string("++ ERROR: Unknown exception interrupt triggered", -1, -1);
        }
}

void irq_handler(interrupt_stack_data interrupt_stack_data) {
    print_string("Inside irq_handler\n", -1, -1);
    // If IRQ was generated by PIC-M send EOI to Master, if IRQ was generated by PIC-S send EOI to Slave/Master
    if ( interrupt_stack_data.int_no >= 32 && interrupt_stack_data.int_no <= 39){
        /*if ( interrupt_stack_data.int_no == 33 ){
            print_string("Keyboard Key Pressed\n", -1, -1);
        }*/
        print_string("PIC-M IRQ detected\n", -1, -1);
        print_string("Sending EOI to PIC-M\n", -1, -1);
        port_byte_out(0x20, 0x20);
    } else if ( interrupt_stack_data.int_no >= 40 && interrupt_stack_data.int_no <= 47){
        print_string("PIC-S IRQ detected\n", -1, -1);
        print_string("Sending EOI to PIC-S\n", -1, -1);
        port_byte_out(0xA0, 0x20);
        print_string("Sending EOI to PIC-M\n", -1, -1);
        port_byte_out(0x20, 0x20);
    } else {
            print_string("++ ERROR: Unknown IRQ interrupt triggered", -1, -1);
    }
    port_byte_in(0x60); // Read keyboard key only to be able to continue testing it
    print_string("--------------------------------\n", -1, -1);
}

El handler de las IRQs debe avisar a los PICs de que ha recibido la petición de interrupción mediante el comando EOI(End Of Interrupt), si la interrupción proviene del PIC-M solo se le envía a él, si proviene del PIC-S se debe enviar a ambos PICs, los PICs no generan mas IRQs hasta que se les haya notificado el EOI.

Un detalle a destacar es que si se produce una IRQ de teclado pero no se lee el dato del buffer del teclado, este se queda a la espera de que se lea y no generará mas IRQs hasta que el buffer quede vacío.

El siguiente paso es definir nuestra tabla IDT, esta tendrá el siguiente aspecto:

La tabla IDT(nterrupt Descriptor Table) no solo contiene los descriptores de las rutinas de servicio si no que además contienen algunos flags y niveles de protección.

Cada una de las entradas de la IDT es llamada gate, cada gate tiene el siguiente aspecto:

Para localizar la ISR necesitamos el descriptor de la tabla GDT que hace referencia al segmento de código y la dirección donde se encuentra la ISR en ASM, como podemos ver en la imagen anterior los bits que componen la dirección no son consecutivos así que habrá que leer sus partes y unirlas.

Los campos de una entrada IDT son los siguientes:

  • Address: Dirección de memoria donde se encuentra la ISR
  • Segment Selector: Descriptor del code segment de la tabla GDT
  • Flags: Información sobre el tipo de interrupción, la arquitectura de la CPU y privilegios

Las flags se detallan a continucación:

  • Type: Indica el tipo, las posibles opciones son
    0b101 task gate
    0b110 interrupt gate
    0b111 trap gate
  • X: Indica si se trata de un sistema de 16/32bits
    0: 16bits
    1: 32bits
  • S: Descriptor type. 1 para segmentos de código o datos, 0 para traps/interrupciones/tasks
  • DPL: Privilege, 2 bits. Contiene el ring level, 0-kernel 3-userspace
  • P: Indica si es una interrupción válida

En nuestro caso el valor final de las FLAGS será 0x8E:

P=1, DPL=00b, S=0, X=1, type=110b => 1000_1110b=10001110=0x8E

Para indicarle al microporcesador donde se encuentra nuestra IDT lo haremos a través del registro idtr:

Declaramos las funciones relacionadas con la IDT y el descriptor de la GDT del segmento de código GDT_CS, recordemos que es el segundo segmento y cada uno ocupa 8bytes partiendo desde 0.

vi kernel/idt.h
#ifndef IDT_H
#define IDT_H

/* GDT Code Segment selector */
#define GDT_CS 0x08

/* Functions implemented in idt.c */
void set_idt_gate(int n, unsigned int isr_address);
void set_idt();

#endif

En el fichero siguiente definimos dos funciones, set_idt_gate que nos rellena la tabla IDT con los id de interrupción y las posiciones de memoria de las ISRs en ASM y set_idt que carga el descriptor de la tabla IDT en el registro idtr, también definiremos algunas estructuras como idt_gate que contendrá la información de una entrada IDT, idt_register es el descriptor que contiene la base de la IDT y su tamaño.

idt es un array de 256 posiciones de estructuras idt_gate y idt_reg es el descriptor de tipo idt_register.

vi kernel/idt.c
#include "idt.h"

/* How every interrupt gate is defined */
typedef struct {
    unsigned short low_address; /* Lower 16 bits of handler function address */
    unsigned short sel; /* GDT Code segment selector */
    unsigned char always0;
    unsigned char flags; 
    unsigned short high_address; /* Higher 16 bits of handler function address */
} __attribute__((packed)) idt_gate;

/* A pointer to the array of interrupt handlers.
 * Assembly instruction 'lidt' will read it */
typedef struct {
    unsigned short limit;
    unsigned int base;
} __attribute__((packed)) idt_register;


#define IDT_ENTRIES 256

idt_gate idt[IDT_ENTRIES];// Array of gates
idt_register idt_reg;


void set_idt_gate(int n, unsigned int isr_address) {
    idt[n].low_address = (isr_address & 0xFFFF);
    idt[n].sel = GDT_CS;
    idt[n].always0 = 0;
    idt[n].flags = 0x8E;
    idt[n].high_address = (isr_address >> 16) & 0xFFFF;
}

void set_idt() {
    idt_reg.base = (unsigned int) &idt;
    idt_reg.limit = IDT_ENTRIES * sizeof(idt_gate) - 1;
    /* Don't make the mistake of loading &idt -- always load &idt_reg */
    __asm__ __volatile__("lidtl (%0)" : : "r" (&idt_reg));
}

Finalmente remapeamos las IRQs e instalamos las ISRs desde la función principal del kernel, generamos algunas interrupciones por código y nos mantenemos a la espera de que se pulse alguna tecla del teclado, si esto ocurre leemos la tecla para liberar el buffer del teclado y así poder seguir generando interrupciones pulsando teclas, todavía no leemos el código de la tecla.

vi kernel/kernel.c
#include "../drivers/screen.h"
#include "util.h"

#include "irqs.h"
#include "isrs.h"

void main() {
    clear_screen();
    print_string("Welcome to StellatorOS v0.1b by Kr0m\n", 0, 0);
    print_string("\n", -1, -1);
    
    print_string(">> Remapping and masking all IRQs\n", -1, -1);
    remap_irqs();
    print_string("Done\n", -1, -1);
    
    print_string(">> Installing ISRs and setting idtr register to IDT table descriptor\n", -1, -1);
    isr_install();
    print_string("Done\n", -1, -1);
    
    print_string(">> Unmasking Keyboard IRQ\n", -1, -1);
    unmask_irq(1);
    print_string("Done\n", -1, -1);
    
    print_string(">> Enabling interrupts: 32 bits protected mode jump disabled it\n", -1, -1);
    enable_interrupts();
    print_string("Done\n", -1, -1);
    
    print_string(">> Generating test interrupts\n", -1, -1);
    /* Test the interrupts */
    print_string("--------------------------------\n", -1, -1);
    __asm__ __volatile__("int $2");
    __asm__ __volatile__("int $3");
    //__asm__ __volatile__("int $33");
    
    print_string(">> Press any key to test IRQ1 keyboard interruption\n", -1, -1);
}

Como el kernel ha crecido habrá que aumentar el tamaño de sectores a cargar en el bootloader:

vi boot_sect.asm
    KERNEL_SIZE equ 19

Tendremos que añadir kernel/interrupts.o a nuetro Makefile.

vi Makefile
OBJ = ${C_SOURCES:.c=.o kernel/interrupts.o}

Compilamos el kernel y lo cargamos en Qemu:

gmake run

Todos los cambios en ficheros existentes se han explicado en detalle pero ahún así dejo un tar.gz del código entero del proyecto.

Si tenemos dudas de si estamos utilizando PICs o algún otro conjunto de chips para atender las interrupciones podemos ejecutar Qemu con la opción -M help:

qemu-system-i386 os-image -M help
Supported machines are:
pc                   Standard PC (i440FX + PIIX, 1996) (alias of pc-i440fx-4.1)
pc-i440fx-4.1        Standard PC (i440FX + PIIX, 1996) (default)
pc-i440fx-4.0        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-3.1        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-3.0        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.9        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.8        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.7        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.6        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.5        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.4        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.3        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.2        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.12       Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.11       Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.10       Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.1        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.0        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-1.7        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-1.6        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-1.5        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-1.4        Standard PC (i440FX + PIIX, 1996)
pc-1.3               Standard PC (i440FX + PIIX, 1996)
pc-1.2               Standard PC (i440FX + PIIX, 1996)
pc-1.1               Standard PC (i440FX + PIIX, 1996)
pc-1.0               Standard PC (i440FX + PIIX, 1996)
pc-0.15              Standard PC (i440FX + PIIX, 1996) (deprecated)
pc-0.14              Standard PC (i440FX + PIIX, 1996) (deprecated)
pc-0.13              Standard PC (i440FX + PIIX, 1996) (deprecated)
pc-0.12              Standard PC (i440FX + PIIX, 1996) (deprecated)
q35                  Standard PC (Q35 + ICH9, 2009) (alias of pc-q35-4.1)
pc-q35-4.1           Standard PC (Q35 + ICH9, 2009)
pc-q35-4.0.1         Standard PC (Q35 + ICH9, 2009)
pc-q35-4.0           Standard PC (Q35 + ICH9, 2009)
pc-q35-3.1           Standard PC (Q35 + ICH9, 2009)
pc-q35-3.0           Standard PC (Q35 + ICH9, 2009)
pc-q35-2.9           Standard PC (Q35 + ICH9, 2009)
pc-q35-2.8           Standard PC (Q35 + ICH9, 2009)
pc-q35-2.7           Standard PC (Q35 + ICH9, 2009)
pc-q35-2.6           Standard PC (Q35 + ICH9, 2009)
pc-q35-2.5           Standard PC (Q35 + ICH9, 2009)
pc-q35-2.4           Standard PC (Q35 + ICH9, 2009)
pc-q35-2.12          Standard PC (Q35 + ICH9, 2009)
pc-q35-2.11          Standard PC (Q35 + ICH9, 2009)
pc-q35-2.10          Standard PC (Q35 + ICH9, 2009)
isapc                ISA-only PC
none                 empty machine

Como podemos ver por defecto utiliza PIIX.

Si te ha gustado el artículo puedes invitarme a un redbull aquí.
Autor: kr0m -- 07/07/2020 03:10:14