Um das Betriebssystem tOSh herunterzuladen, kannst du dies über den Bereich Resources dieser Website tun.
Teile
Diese Artikelserie wird aus folgenden Teilen bestehen:
- Teil I - Bootloader
- Teil II - Stage 1
- Teil III - Sprung zu C
- Teil IV - Interrupts <— Du bist hier
- Teil V - Dateisystem und Diskettenlaufwerk-Treiber
- Teil VI - Booten auf verschiedenen Maschinentypen
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. :)