Um das Betriebssystem tOSh herunterzuladen, kannst du dies über den Bereich Resources dieser Website tun.

Teile

Diese Artikelserie wird aus folgenden Teilen bestehen:

tOSh Kernel Panic!tOSh Kernel Panic!

Interrupts

Dass sowohl die alten als auch die modernen PC-Architekturen auf [Interrupts] basieren, liegt genau an diesem Chip, dem Chip 8259a PIC von Intel.

Ein Interrupt ist ein Signal, das die CPU empfängt und das vorübergehend alles stoppt, was die CPU gerade berechnet, damit sie ein Ereignis mit höherer Priorität handlen (oder verwalten) kann.

Damit ein PC mit einem ausreichend komplexen elektronischen Gerät interagieren kann, muss es in irgendeiner Form eine Interaktion mit einer CPU geben. Eine Möglichkeit war, einfach jedes Gerät direkt zu fragen: "Hey!, was geht?".

Wenn man die angeschlossenen Geräte jedoch ständig fragt, ob sie etwas brauchen, gehen bei jeder einzelnen Frage Zeit und Ressourcen verloren. Diese Strategie nennt man (Polling).

Eine der Lösungen, um Polling zu vermeiden, besteht darin, die CPU einfach ihre Sachen machen und ihre Programme normal ausführen zu lassen und auf ein Signal zu warten, sobald jemand eine Aktion von ihr benötigt. Die beiden häufigsten Arten von CPU-Interrupts sind zum Beispiel:

  • Hardware Interrupts: Sie werden asynchron von physischen Geräten wie Maus, Tastatur und Festplatten gesendet, die von Zeit zu Zeit Aufmerksamkeit benötigen. Zum Beispiel löst das Drücken einer Taste auf der PC-Tastatur einen Hardware-Interrupt aus und UNTERBRICHT ALLES, und man muss das, was gerade ausgeführt wird, egal ob ein Kernel oder ein Programm im "Userspace", stehen lassen und den Interrupt sofort handlen.

  • Software Interrupts: Exceptions oder Traps, die intern vom Prozessor oder von einem laufenden Programm aufgrund eines Fehlers erzeugt werden (zum Beispiel durch eine Division durch null) oder durch einen request an das Betriebssystem über einen system call.

Wie wird ein Interrupt gehandlet (behandelt)?

Diego Silicio erinnert uns in seinen "unterbrochenen Sextillen" daran:

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

Übersetzung:

hier kommen wir, um euch zu singen
im unterbrochenen Takt
Sie, mein dichter Freund
doch niemals 'IST' wie ein Mensch
PICant ist der Landsmann
in seiner Zeile hat er sich vereint

Erklärung: Das Gedicht steckt voller technischer und sprachlicher Wortspiele. „PICante“ kombiniert PIC (Programmable Interrupt Controller) mit picante („scharf“), während „paisano“ den Gaucho-/Landsmann-Ton des Gedichts beibehält.

Dazu kommt ein mehrschichtiges Wortspiel in „usted, amigo tupido / pero nunca ‘ES’ como humano“. Tupido bedeutet etwa „dicht“, „kompakt“ oder auch „begriffsstutzig“. Zusammen mit ES ergibt sich visuell „EStupido“, was auf estúpido („dumm“) anspielt. Gleichzeitig ist ES tatsächlich das spanische Verb es („ist“), sodass „nunca ES como humano“ wörtlich „ist niemals wie ein Mensch“ bedeutet. Die Großschreibung von ES macht die versteckte Konstruktion EStupido noch deutlicher.

Das Gedicht spricht also gleichzeitig über Interrupts und PICs, während es technische Begriffe in einen absichtlich gauchohaften Vers verpackt und mehrere Ebenen von Wortspielen übereinanderlegt.

Der kryptische Gaucho hat uns unterbrochen, aber nachdem wir diesen zermürbenden Artikel beendet haben, wird sein Gesang von allen behandelt.

Wenn ein Interrupt auftritt, sollte die CPU den Ausführungskontext irgendwo sichern. Der stack ist dafür ein guter Ort.

Also: Kontext sichern -> Eine Interrupt Service Routine ISR ausführen (dazu kommen wir gleich) -> Kontext wiederherstellen und weiterarbeiten.

Bevor ich überhaupt anfangen kann, die Interrupt-Routinen zusammenzubauen, von denen wir vorerst nur ein paar handlen werden und nicht alle, muss ich noch mehr Dinge erklären, denn in der Welt des PC ist leider nichts einfach.

Der PIC - Programmable Interrupt Controller

In einem traditionellen PC ist der Programmable Interrupt Controller oder Chip 8259a PIC der Vermittler zwischen den Geräten und dem Prozessor.

Die Geräte sprechen nicht direkt mit dem Prozessor.

Sie sind über Interrupt-Leitungen, genannt Interrupt Request oder IRQs, mit dem PIC verbunden.

Der PIC empfängt diese Signale und kümmert sich darum, der CPU mitzuteilen, dass jemand Aufmerksamkeit verlangt.

Wenn wir ein bisschen im Datasheet des Chip 8259a PIC lesen, stellen wir fest, dass er 8 IRQ-Leitungen hat, von IRQ0 bis IRQ7, und alles ist wunderbar, bis wir 9 oder mehr Geräte gleichzeitig anschließen wollen, weil uns die Leitungen ausgehen.

Irgendwann in den 80ern gingen ihnen tatsächlich die IRQs für die ganzen Geräte aus, und wie es sich für gute Ingenieure gehört, haben sie einfach noch einen drangeklatscht und damit ein master/slave-Schema mit zwei kaskadierten Chip 8259a PICs geschaffen.

Hardware-Leitung 8259a PIC-Chip Standard-Vektor Im Protected Mode remapped Zugewiesenes Gerät Verwendeter I/O-Port
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)

Indem wir IRQ2 als Cascade Line verwenden, verlieren wir einen IRQ, aber die Geräte, die am slave hängen, können darüber auf die CPU zugreifen.

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

Zusätzlich erlaubt der Chip 8259a PIC, Interrupts zu maskieren. Wenn wir gerade die Tastatur behandeln und nicht wollen, dass sich in diesem Moment das Diskettenlaufwerk einmischt, sagen wir dem PIC: "Diese eine IRQ" lässt du jetzt mal links liegen.

Das ist es, was ich beim Konfigurieren der IRQs mache: tOSh implementiert nur 3 davon:

  • IRQ0 (timer / pit)
  • IRQ1 (Tastatur)
  • IRQ6 (Floppy)

Alle Interrupts sind maskiert, außer diesen drei, die ich ausdrücklich entmaskiere, damit sie behandelt werden können:

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

Die IDT - Interrupt Descriptor Table

Die IDT ist eine binäre Datenstruktur, die spezifisch für die Architekturen IA-32 und x86-64 ist, und das Gegenstück zur IVT im [Real Mode] und Long Mode.

Durch das Konfigurieren der IDT können wir der CPU sagen, wo sich unsere ISRs bzw. Interrupt Service Routines befinden. Das sind die Funktionen, die wir selbst schreiben müssen, um die Interrupts zu behandeln, die vom Prozessor oder von Geräten ausgelöst werden. Sie ist ziemlich ähnlich zur Struktur der GDT.

Nachfolgend kommt Intels Interrupt-Tabelle für den [Protected Mode]:

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.

Ein häufiger Fehler ist, die IRQs des Chip 8259a PIC mit den INTerrupts zu verwechseln. DAS IST NICHT DASSELBE!!!!!1

Eine IRQ ist die physische Hardware-Leitung. Die Interrupt-Nummer ist der Eintrag, den die CPU in der IDT verwendet.

Es sind zwei miteinander verbundene Dinge, aber nicht dasselbe, und was sie miteinander verbindet, ist genau das Remapping des PIC.

Die Interrupt-Nummer funktioniert als Index innerhalb der Tabelle: Der Interrupt kommt an, die CPU sucht den entsprechenden Eintrag und springt direkt zu dem Code, den wir dafür konfiguriert haben.

Remapping

Man kann das Problem schon riechen, oder? Die IRQ-Werte und die CPU-Interrupts (DIE NICHT DASSELBE SIND!!!!!!!!!!1) überlappen sich.

Durch diese Überschneidung müssen wir die IRQ-Werte verschieben bzw. remappen, sodass sie hinter der INTerrupt-Tabelle der CPU liegen.

Die ersten 32 Einträge der IDT (Vektoren 0 bis 31) sind für Exceptions des x86 selbst reserviert (divide by zero error, general protection fault, etc..) und die IRQs des PIC beginnen ebenfalls bei 0, wodurch sie sich überschneiden.

Deshalb müssen wir beim Start unseres Kernel den PIC neu konfigurieren, damit er oberhalb des Bereichs der INTerrupt-Tabelle der CPU läuft, und zwar so:

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

Drücken Sie eine beliebige Taste, um fortzufahren…

Nehmen wir als Beispiel das Drücken der beliebigen Taste.

Die Logik jeder PC-Tastatur erzeugt für jede gedrückte Taste einen scancode für den Tastatur-Controller.

Dieser Controller erzeugt einen IRQ, in unserem Fall IRQ1 (siehe Tabelle weiter oben). Empfänger dieses IRQ ist der Chip 8259a PIC, der prüft, ob der Interrupt aktiviert (bzw. entmaskiert) ist, ihn als ausstehend markiert und über die INTR-Leitung ein Signal an die CPU schickt.

Die CPU akzeptiert den Interrupt und der PIC liefert den entsprechenden Vektor. Wenn IRQ1 auf den Vektor 0x21 remapped wurde, verwendet die CPU diesen Vektor, um in der IDT nachzuschauen, an welcher Speicheradresse sie den entsprechenden Interrupt-Handler findet.

Das hier ist der von tOSh; wir unterstützen nur einen Timer, die Tastatur und das 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);
}

Und aus den Routinen in idt.asm, wo wir deklarieren, wie wir den Handler laden werden, erstellen wir diese Funktion:


[...]

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

[...]

Die CPU gelangt über unser extern interrupt_handler in den entsprechenden Handler. Dieser wird in irq_common aufgerufen und macht einen Call in unseren C-Code, der schließlich die Funktion kbd_handler() ausführt.

Wie man sehen kann, sichern wir mit pusha den Ausführungskontext, denn egal, was die CPU gerade gemacht hat, wir müssen das sichern, um zu tun, was wir tun müssen, und nach unserem call interrupt_handler den Kontext wiederherzustellen und mit iret aus unserem Interrupt zurückzukehren.

Wir haben jetzt alles abgedeckt. Der kbd_handler() und alles, was mit der Tastatur zu tun hat, ist ziemlich öde, weil es im Grunde nur darum geht, scancodes auf Tastaturfunktionen zu mappen (CTRL+C macht dies, SHIFT + Kleinbuchstabe wird zum Großbuchstaben usw.) und als phun phakt: Dieser Teil des Kernel hat Jahre gedauert, weil ich beim Anblick dessen, was ich da alles programmieren musste, keinen Bock hatte. Und ein paar Jahre lang hatte ich auch Skrupel, einige ziemlich coole Implementierungen anderer Leute von GitHub zu kopieren. Aber vor kurzem ist mir das dann vergangen und ich habe mich sehr stark inspirieren lassen. Leider weiß ich heute nicht mehr genau, von wem, aber hey, danke.

Wie man in der C-Funktion interrupt_handler() sehen kann, gibt es dort einen Aufruf der Funktion pic_eoi(). Ein iret allein reicht nicht aus, um anzuzeigen, dass wir von einem Interrupt zurückkehren; wir müssen dem PIC auch sagen, dass wir mit der Behandlung des IRQ fertig sind.

Die CPU kehrt schließlich zu dem unterbrochenen Programm zurück und setzt genau bei dem EIP fort, an dem sie angehalten wurde.

Und fertig. Wir sind durch.

wh0a, dir fehlt der APIC! (Advanced Programmable Interrupt Controller)

Oh nein! Moment! Ich muss noch eine Sache klarstellen. APIC ist der Ersatz für den Chip 8259a PIC, und bis heute unterstützt tOSh ihn nicht.

Irgendwie würde uns die Unterstützung für APIC unter anderem ermöglichen, mehr CPUs zu verwalten (Dual-Core aufwärts), aber an diesem Punkt der Entwicklung kämpfe ich gerade mit Interrupt-Maskierung und der Programmierung einer Tastatur.

Also lassen wir das für später. :)

Referenz