Entwicklung eines monolithischen Unix-ähnlichen Betriebssystems - GDT & IDT (5)

Im vorherigen Artikel haben wir einen dynamischen Speichermanager implementiert.
Heute werden wir die Grundlagen der Arbeit im geschützten Modus des Intel i386-Prozessors behandeln.
Nämlich: die globale Deskriptortabelle und die Interruptvektortabelle.


Inhaltsverzeichnis


Build-System (make, gcc, gas). Erster Start (Multiboot). Starten Sie (qemu). C-Bibliothek (strcpy, memcpy, strext).
C-Bibliothek (sprintf, strcpy, strcmp, strtok, va_list ...). Erstellen der Bibliothek im Kernelmodus und im Benutzeranwendungsmodus.
Das Kernel-Systemprotokoll. Videospeicher Ausgabe an das Terminal (kprintf, kpanic, kassert).
Dynamischer Speicher, Heap (kmalloc, kfree).
Organisation der Speicher- und Interrupt-Behandlung (GDT, IDT, PIC, Syscall). Ausnahmen
Virtueller Speicher (Seitenverzeichnis und Seitentabelle).
Prozess. Planer Multitasking. Systemaufrufe (kill, exit, ps).
Das Dateisystem des Kernels (initrd), elf und seiner Interna. Systemaufrufe (exec).
Zeichengerätetreiber. Systemaufrufe (ioctl, fopen, fread, fwrite). C-Bibliothek (fopen, fclose, fprintf, fscanf).
Shell als komplettes Programm für den Kernel.
Benutzerschutzmodus (Ring3). Aufgabenstatussegment (tss).

Lineare Adressierung


Intel-Prozessoren verfügen über zwei Hauptbetriebsarten: Protected Mode x32 und IA-32e x64.
Im Allgemeinen schreibt Zubkov sehr gut und verständlich darüber, ich empfehle es zu lesen, obwohl im Prinzip auch das Intel-Handbuch möglich ist, es ist nicht kompliziert, aber redundant und groß.
Sie haben einen separaten Band für die Systemprogrammierung, ich empfehle und lese es.
Es gibt viel mehr russischsprachige Informationen zum ersten, daher werden wir kurz auf die wichtigsten Punkte eingehen.
Es gibt zwei Arten der Adressierung: linear und seitlich. Linear bedeutet, dass der gesamte physikalische Raum kontinuierlich beschrieben wird und mit dem physikalischen zusammenfällt, da die Grundlagen der Segmentdeskriptoren in der Regel Null sind, weil dies einfacher ist.
In diesem Fall müssen Sie für den Kernelmodus drei Deskriptoren erstellen, die den Speicher beschreiben: für Code, Stapel und Daten. Sie zeichnen sich durch einen gewissen Hardwareschutz aus.
Jedes solche Segment hat eine Basis von Null und eine Grenze, die durch die maximale Größe eines Maschinenworts adressiert wird. Der Stapel wächst in die entgegengesetzte Richtung, und dazu gibt es auch ein Flag im Deskriptor.
Mit drei Datensätzen dieses Formats sprechen wir also alles an, was wir brauchen:

/* * Global descriptor table entry */ struct GDT_entry_t { u16 limit_low: 16; u16 base_low: 16; u8 base_middle: 8; u8 type: 4; /* whether code (0b1010), data (0b0010), stack (0b0110) or tss (0b1001) */ u8 s: 1; /* whether system descriptor */ u8 dpl: 2; /* privilege level */ u8 p: 1; /* whether segment prensent */ u8 limit_high: 4; u8 a: 1; /* reserved for operation system */ u8 zero: 1; /* zero */ u8 db: 1; /* whether 16 or 32 segment */ u8 g: 1; /* granularity */ u8 base_high: 8; } attribute(packed); 


Jedes Segmentregister (cs, ds, ss) hat einen eigenen Deskriptor in GDT. Wenn wir also etwas in den Codeabschnitt schreiben, wird eine Fehlermeldung angezeigt, da der Deskriptor einen schriftlichen Schutz enthält.
Damit dies funktioniert, müssen wir eine Struktur des folgenden Formats in das GDTR-Register laden:

 /* * Global descriptor table pointer */ struct GDT_pointer_t { u16 limit; u32 base; } attribute(packed); 


Das Limit ist das Ende der GDT-Tabelle minus 1, die Basis ist der Anfang im Speicher.
GDT wird wie folgt in das Register geladen:

/*
* Load global descriptor table
* void asm_gdt_load(void *gdt_ptr)
*/
asm_gdt_load:
mov 4(%esp),%eax # eax = gdt_ptr
lgdt (%eax)
mov $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
mov %ax,%ss
jmp $0x08,$asm_gdt_load_exit
asm_gdt_load_exit:
ret


Und unmittelbar danach laden wir die Kernel-Datenselektoren in alle Segmentregister, die den Datendeskriptor anzeigen (Nullschutzring).
Danach ist alles bereit, um Paging einzuschließen, aber dazu später mehr.
Übrigens empfehlen Multiboot-Bootloader, ihre GDT sofort einzurichten, obwohl sie dies selbst tun, sagen sie dies zuverlässiger.
Wie das alles technisch korrekt funktioniert, erfahren Sie im Video-Tutorial.

Behandlung unterbrechen


In Analogie zu GDT verfügt die Interrupt-Tabelle über ein eigenes IDTR-Register, in das Sie ebenfalls einen ähnlichen Zeiger laden müssen, der sich jedoch bereits in IDT befindet.
Die Interrupt-Tabelle selbst wird durch folgende Einträge beschrieben:

 /* * Interrupt table entry */ struct IDT_entry_t { u16 offset_lowerbits; u16 selector; u8 zero; u8 type_attr; u16 offset_higherbits; }; 


Das Unterbrechungsgateway fungiert normalerweise als Typ, da wir Unterbrechungen speziell behandeln möchten. Wir betrachten noch keine Fallen und ein Anruf-Gateway, da es näher an TSS und Schutzringen liegt.
Lassen Sie uns eine Schnittstelle für die Arbeit mit diesen Tabellen mit Ihnen erstellen. Sie müssen nur einmal eingerichtet und vergessen werden.

 /* * Api */ extern void gdt_init(); extern void idt_init(); 


Und jetzt deklarieren wir die in den IDT-Datensätzen selbst aufgeführten Interrupt-Handler.
Schreiben Sie zunächst die Hardware-Fehlerbehandlungsroutinen:

 /* * Api - IDT */ extern void ih_double_fault(); extern void ih_general_protect(); extern void ih_page_fault(); extern void ih_alignment_check(); extern void asm_ih_double_fault(); extern void asm_ih_general_protect(); extern void asm_ih_page_fault(); extern void asm_ih_alignment_check(); 


Dann der Tastatur-Interrupt-Handler:

 /* * Api - IRQ */ extern void ih_keyboard(); extern void asm_ih_keyboard(); 


Es ist Zeit, die IDT-Tabelle zu initialisieren.
Es sieht ungefähr so ​​aus:

 extern void idt_init() { size_t idt_address; size_t idt_ptr[2]; pic_init(); /* fill idt */ idt_fill_entry(INT_DOUBLE_FAULT, (size_t)asm_ih_double_fault); idt_fill_entry(INT_GENERAL_PROTECT, (size_t)asm_ih_general_protect); idt_fill_entry(INT_ALIGNMENT_CHECK, (size_t)asm_ih_alignment_check); idt_fill_entry(INT_KEYBOARD, (size_t)asm_ih_keyboard); /* load idt */ idt_address = (size_t)IDT; idt_ptr[0] = (LOW_WORD(idt_address) << 16) + (sizeof(struct IDT_entry_t) * IDT_SIZE); idt_ptr[1] = idt_address >> 16; asm_idt_load(idt_ptr); } 


Hier haben wir drei Hardware-Fehlerbehandler und einen Interrupt registriert.
Damit dies funktioniert, müssen wir einen speziellen Zeiger mit der Basis und dem Limit in das IDTR-Register laden:

/*
* Load interrupt table
* void asm_idt_load(unsigned long *addr)
*/
asm_idt_load:
push %edx
mov 8(%esp), %edx
lidt (%edx)
pop %edx
ret


Es sind Grenzwerte erforderlich, um zu verstehen, wie viele Datensätze in der Tabelle enthalten sind.
Es ist Zeit, einen Tastatur-Interrupt-Handler zu schreiben:

/*
* Handle IRQ1
* void asm_ih_keyboard(unsigned int)
*/
asm_ih_keyboard:
pushal
call ih_keyboard
popal
iretl


Hinweis: Im Folgenden und überall im Code entsprechen die "unteren Hälften" den "oberen Hälften" unter Linux. Und das "obere" bzw. das Gegenteil. Ich entschuldige mich, das Gegenteil wurde mir in den Kopf gesetzt: D.

Tatsächlich wird Code an einen übergeordneten Handler übergeben.
Dadurch wird wiederum der Handler der unteren Hälften des entsprechenden Treibers aufgerufen, der die Anforderung zur Verarbeitung dieses Interrupts registriert hat.
In unserem Fall handelt es sich um einen Zeichengerätetreiber.
Die unteren Hälften werden benötigt, um Interrupts schnell zu verarbeiten, ohne die anderen zu verlangsamen, und dann, wenn Zeit ist, führt der Prozessor der oberen Hälften nach und nach zusätzliche Arbeit aus, da ein solcher Prozessor bereits verdrängt (unterbrochen) werden kann.

 /* * Api - Keyboard interrupt handler */ extern void ih_keyboard() { printf("[IH]: irq %u\n", 1); u_char status = asm_read_port(KEYBOARD_STATUS_PORT); if (status & 0x01) { char keycode = asm_read_port(KEYBOARD_DATA_PORT); if (keycode < 1) { goto end; } /* call low half (bottom) interrupt handler */ } end: asm_write_port(PIC1_CMD_PORT, 0x20); /* end of interrupt */ } 


Wenn wir nun die Tastaturtaste drücken, wird jedes Mal der entsprechende Eintrag im Kernel-Systemprotokoll angezeigt.

Referenzen


Öffnen Sie jetzt das Video-Tutorial für diesen Artikel.
Und schauen Sie sich das Git-Repository parallel an (Sie benötigen einen Lektion5-Zweig).

Referenzliste


1. James Molloy. Rollen Sie Ihr eigenes UNIX-Klon-Betriebssystem.
2. Zähne. Assembler für DOS, Windows, Unix
3. Kalaschnikow. Assembler ist einfach!
4. Tanenbaum. Betriebssysteme. Implementierung und Entwicklung.
5. Robert Love. Linux-Kernel Beschreibung des Entwicklungsprozesses.

Source: https://habr.com/ru/post/de467289/


All Articles