OS1: Ein primitiver Kernel auf Rust für x86. Teil 2. VGA, GDT, IDT

Erster Teil


Der erste Artikel hatte noch keine Zeit zum Abkühlen, aber ich habe beschlossen, Sie nicht zu faszinieren und eine Fortsetzung zu schreiben.


Im vorherigen Artikel haben wir über das Verknüpfen, Laden der Kerneldatei und die primäre Initialisierung gesprochen. Ich gab einige nützliche Links, erklärte, wie sich der geladene Kernel im Speicher befindet, wie virtuelle und physische Adressen beim Booten verglichen werden und wie die Unterstützung für den Seitenmechanismus aktiviert wird. Zuletzt ging die Kontrolle auf die in Rust geschriebene kmain-Funktion meines Kernels über. Es ist Zeit weiterzumachen und herauszufinden, wie tief das Kaninchenloch ist!


In diesem Teil der Hinweise werde ich kurz meine Rust-Konfiguration beschreiben, allgemein über die Ausgabe von Informationen in VGA und ausführlich über das Einrichten von Segmenten und Interrupts sprechen . Ich frage alle Interessierten unter dem Schnitt, und wir fangen an.


Rostaufbau


Im Allgemeinen ist dieses Verfahren nicht besonders kompliziert. Einzelheiten erfahren Sie im Philippe-Blog . Ich werde jedoch an einigen Stellen aufhören.


Stable Rust unterstützt einige Funktionen, die für die Entwicklung auf niedriger Ebene erforderlich sind, immer noch nicht. Um die Standardbibliothek zu deaktivieren und auf Bare Bones aufzubauen, benötigen wir Rust Nightly. Seien Sie vorsichtig, einmal nach dem Update auf den neuesten Stand bekam ich einen völlig funktionsunfähigen Compiler und musste zum nächsten stabilen zurücksetzen. Wenn Sie sicher sind, dass Ihr Compiler gestern funktioniert hat, aber aktualisiert wurde und nicht funktioniert, führen Sie den Befehl aus und ersetzen Sie das gewünschte Datum


rustup override add nightly-YYYY-MM-DD 

Einzelheiten zum Mechanismus erhalten Sie hier .


Konfigurieren Sie als Nächstes die Zielplattform, für die wir gehen. Ich basierte auf dem Blog von Philip Opperman, so dass viele der Dinge in diesem Abschnitt von ihm genommen, von Knochen zerlegt und an meine Bedürfnisse angepasst wurden. Philip entwickelt in seinem Blog für x64, ich habe ursprünglich x32 gewählt, daher wird meine target.json etwas anders sein. Ich bringe es komplett


 { "llvm-target": "i686-unknown-none", "data-layout": "em:ep:32:32-f64:32:64-f80:32-n8:16:32-S128", "arch": "x86", "target-endian": "little", "target-pointer-width": "32", "target-c-int-width": "32", "os": "none", "executables": true, "linker-flavor": "ld.lld", "linker": "rust-lld", "panic-strategy": "abort", "disable-redzone": true, "features": "-mmx,-sse,+soft-float" } 

Der schwierigste Teil hier ist der Parameter „ Datenlayout “. Aus der LLVM-Dokumentation geht hervor, dass es sich um Datenlayoutoptionen handelt, die durch „-“ getrennt sind. Das allererste "e" -Zeichen ist für die Indianität verantwortlich - in unserem Fall ist es Little-Endian, wie es die Plattform erfordert. Das zweite Zeichen ist m, "Verzerrung". Verantwortlich für Charakternamen während des Layouts. Da unser Ausgabeformat ELF ist (siehe Build-Skript), wählen wir "m: e". Das dritte Zeichen ist die Größe des Zeigers in Bit und ABI (Application Binary Interface). Hier ist alles einfach, wir haben 32 Bits, also setzen wir mutig „p: 32: 32“. Als nächstes kommen Gleitkommazahlen. Wir berichten, dass wir 64-Bit-Nummern gemäß ABI 32 mit Ausrichtung 64 - „f64: 32: 64“ sowie 80-Bit-Nummern mit standardmäßiger Ausrichtung - „f80: 32“ unterstützen. Das nächste Element sind Ganzzahlen. Wir beginnen mit 8 Bit und gehen zum Plattformmaximum von 32 Bit über - „n8: 16: 32“. Der letzte ist die Stapelausrichtung. Ich brauche sogar 128-Bit-Ganzzahlen, also sei es S128. In jedem Fall kann LLVM diesen Parameter ignorieren. Dies ist unsere Präferenz.


In Bezug auf die restlichen Parameter können Sie einen Blick auf Philip werfen, er erklärt alles gut.


Wir brauchen auch Cargo-Xbuild - ein Tool, mit dem Sie den Rostkern beim Bauen unter einer unbekannten Zielplattform überkompilieren können.
Installieren.


 cargo install cargo-xbuild 

Wir werden es so sammeln.


 cargo xbuild -Z unstable-options --manifest-path=kernel/Cargo.toml --target kernel/targets/$(ARCH).json --out-dir=build/lib 

Ich brauchte ein Manifest für die korrekte Operation von Make, da es vom Stammverzeichnis ausgeht und der Kernel im Kernelverzeichnis liegt.


Von den Funktionen des Manifests kann ich nur crate-type = ["staticlib"] hervorheben , wodurch eine verlinkbare Datei zur Ausgabe erstellt wird. Wir werden ihn in LLD füttern.


kmain und Ersteinrichtung


Wenn wir gemäß den Rust-Konventionen eine statische Bibliothek (oder eine „flache“ Binärdatei) erstellen, muss das Stammverzeichnis der Kiste die Datei lib.rs enthalten, die der Einstiegspunkt ist. Darin werden mithilfe von Attributen Sprachfunktionen konfiguriert und auch der geschätzte kmain gefunden.


Im ersten Schritt müssen wir also die Standardbibliothek deaktivieren. Dies geschieht mit einem Makro.


 #![no_std] 

Mit einem so einfachen Schritt vergessen wir sofort Multithreading, dynamischen Speicher und andere Freuden der Standardbibliothek. Darüber hinaus berauben wir uns sogar des println !, Makros, sodass wir es selbst implementieren müssen. Ich werde dir sagen, wie es das nächste Mal geht.


Viele Tutorials irgendwo an diesem Ort enden mit der Ausgabe von „Hello World“ und ohne zu erklären, wie man davon lebt. Wir werden den anderen Weg gehen. Zunächst müssen wir Code- und Datensegmente für den geschützten Modus festlegen, VGA konfigurieren und Interrupts konfigurieren, was wir tun werden.


 #![no_std] #[macro_use] pub mod debug; #[cfg(target_arch = "x86")] #[path = "arch/i686/mod.rs"] pub mod arch; #[no_mangle] extern "C" fn kmain(pd: usize, mb_pointer: usize, mb_magic: usize) { arch::arch_init(pd); ...... } #[panic_handler] fn panic(_info: &PanicInfo) -> ! { println!("{}", _info); loop {} } 

Was ist hier los? Wie gesagt, wir schalten die Standardbibliothek aus. Wir werden auch zwei sehr wichtige Module ankündigen - Debug (in dem wir auf dem Bildschirm schreiben werden) und Arch (in dem alle plattformabhängige Magie leben wird). Ich verwende die Rust-Funktion mit Konfigurationen, um dieselben Schnittstellen in verschiedenen Architekturimplementierungen zu deklarieren und sie in vollem Umfang zu nutzen. Hier höre ich nur auf x86 auf und dann reden wir nur darüber.


Ich erklärte einen völlig primitiven Panikhandler, den Rust benötigt. Dann kann es geändert werden.


kmain akzeptiert drei Argumente und wird auch in C-Notation ohne Namensverzerrung exportiert, damit der Linker die Funktion korrekt mit dem Aufruf von _loader verknüpfen kann, den ich im vorherigen Artikel beschrieben habe. Das erste Argument ist die Adresse der PD-Seitentabelle, das zweite ist die physikalische Adresse der GRUB-Struktur, von der wir die Speicherkarte erhalten, das dritte ist die magische Zahl. In Zukunft möchte ich sowohl die Multiboot 2-Unterstützung als auch meinen eigenen Bootloader implementieren, daher verwende ich eine magische Zahl, um die Bootmethode zu identifizieren.


Der erste kmain-Aufruf ist die plattformspezifische Initialisierung. Wir gehen hinein. Die Funktion arch_init befindet sich in der Datei arch / i686 / mod.rs, ist öffentlich, 32-Bit x86-spezifisch und sieht folgendermaßen aus:


 pub fn arch_init(pd: usize) { unsafe { vga::VGA_WRITER.lock().init(); gdt::setup_gdt(); idt::init_idt(); paging::setup_pd(pd); } } 

Wie Sie sehen können, werden für x86 Ausgabe, Segmentierung, Interrupts und Paging der Reihe nach initialisiert. Beginnen wir mit VGA.


VGA-Initialisierung


Jedes Tutorial sieht es als seine Pflicht an, Hello World zu drucken, sodass Sie überall erfahren, wie Sie mit VGA arbeiten. Aus diesem Grund werde ich so kurz wie möglich gehen, ich werde mich nur auf die Chips konzentrieren, die ich selbst gemacht habe. Über die Verwendung von lazy_static werde ich Sie zu Philippes Blog schicken und nicht im Detail erklären. const fn ist noch nicht in der Version, daher können noch keine statischen Initialisierungen durchgeführt werden. Und wir werden eine Drehsperre hinzufügen, damit es sich nicht als Chaos herausstellt.


 use lazy_static::lazy_static; use spin::Mutex; lazy_static! { pub static ref VGA_WRITER : Mutex<Writer> = Mutex::new(Writer { cursor_position: 0, vga_color: ColorCode::new(Color::LightGray, Color::Black), buffer: unsafe { &mut *(0xC00B8000 as *mut VgaBuffer) } }); } 

Wie Sie wissen, befindet sich der Bildschirmpuffer an der physischen Adresse 0xB8000 und hat eine Größe von 80x25x2 Byte (Breite und Höhe des Bildschirms, Byte pro Zeichen und Attribute: Farben, Flimmern). Da wir den virtuellen Speicher bereits aktiviert haben, stürzt der Zugriff auf diese Adresse ab, sodass wir 3 GB hinzufügen. Wir dereferenzieren auch einen rohen Zeiger, der unsicher ist - aber wir wissen, was wir tun.
Von den interessanten Dingen in dieser Datei ist vielleicht nur die Implementierung der Writer-Struktur, die es nicht nur ermöglicht, Zeichen in einer Reihe anzuzeigen, sondern auch zu scrollen, an eine beliebige Stelle auf dem Bildschirm zu gehen und andere nette kleine Dinge.


Vga Schriftsteller
 pub struct Writer { cursor_position: usize, vga_color: ColorCode, buffer: &'static mut VgaBuffer, } impl Writer { pub fn init(&mut self) { let vga_color = self.vga_color; for y in 0..(VGA_HEIGHT - 1) { for x in 0..VGA_WIDTH { self.buffer.chars[y * VGA_WIDTH + x] = ScreenChar { ascii_character: b' ', color_code: vga_color, } } } self.set_cursor_abs(0); } fn set_cursor_abs(&mut self, position: usize) { unsafe { outb(0x3D4, 0x0F); outb(0x3D5, (position & 0xFF) as u8); outb(0x3D4, 0x0E); outb(0x3D4, ((position >> 8) & 0xFF) as u8); } self.cursor_position = position; } pub fn set_cursor(&mut self, x: usize, y: usize) { self.set_cursor_abs(y * VGA_WIDTH + x); } pub fn move_cursor(&mut self, offset: usize) { self.cursor_position = self.cursor_position + offset; self.set_cursor_abs(self.cursor_position); } pub fn get_x(&mut self) -> u8 { (self.cursor_position % VGA_WIDTH) as u8 } pub fn get_y(&mut self) -> u8 { (self.cursor_position / VGA_WIDTH) as u8 } pub fn scroll(&mut self) { for y in 0..(VGA_HEIGHT - 1) { for x in 0..VGA_WIDTH { self.buffer.chars[y * VGA_WIDTH + x] = self.buffer.chars[(y + 1) * VGA_WIDTH + x] } } for x in 0..VGA_WIDTH { let color_code = self.vga_color; self.buffer.chars[(VGA_HEIGHT - 1) * VGA_WIDTH + x] = ScreenChar { ascii_character: b' ', color_code } } } pub fn ln(&mut self) { let next_line = self.get_y() as usize + 1; if next_line >= VGA_HEIGHT { self.scroll(); self.set_cursor(0, VGA_HEIGHT - 1); } else { self.set_cursor(0, next_line) } } pub fn write_byte_at_xy(&mut self, byte: u8, color: ColorCode, x: usize, y: usize) { self.buffer.chars[y * VGA_WIDTH + x] = ScreenChar { ascii_character: byte, color_code: color } } pub fn write_byte_at_pos(&mut self, byte: u8, color: ColorCode, position: usize) { self.buffer.chars[position] = ScreenChar { ascii_character: byte, color_code: color } } pub fn write_byte(&mut self, byte: u8) { if self.cursor_position >= VGA_WIDTH * VGA_HEIGHT { self.scroll(); self.set_cursor(0, VGA_HEIGHT - 1); } self.write_byte_at_pos(byte, self.vga_color, self.cursor_position); self.move_cursor(1); } pub fn write_string(&mut self, s: &str) { for byte in s.bytes() { match byte { 0x20...0xFF => self.write_byte(byte), b'\n' => self.ln(), _ => self.write_byte(0xfe), } } } } 

Beim Zurückspulen kopieren Sie einfach Speicherbereiche in der Größe der Bildschirmbreite nach hinten und füllen eine neue Zeile mit Leerzeichen aus (so mache ich die Reinigung). Outb-Aufrufe sind etwas interessanter - auf keine andere Weise als mit E / A-Ports zu arbeiten, ist es unmöglich, den Cursor zu bewegen. Wir benötigen jedoch weiterhin Eingabe / Ausgabe über Ports, sodass diese in einem separaten Paket geliefert und in sichere Wrapper verpackt wurden. Unter dem Spoiler befindet sich der Assembler-Code. Im Moment reicht es zu wissen, dass:


  • Der absolute Cursorversatz, nicht die Koordinate, wird angezeigt.
  • Sie können jeweils ein Byte an den Controller ausgeben
  • Die Ausgabe eines Bytes erfolgt in zwei Befehlen - zuerst schreiben wir den Befehl in die Steuerung, dann die Daten.
  • Der Port für Befehle ist 0x3D4, der Datenport ist 0x3D5
  • Drucken Sie zuerst das untere Byte der Position mit dem Befehl 0x0F und dann das obere mit dem Befehl 0x0E

out.asm

Achten Sie darauf, mit übergebenen Variablen auf dem Stapel zu arbeiten. Da der Stapel am Ende des Leerzeichens beginnt und den Stapelzeiger beim Aufrufen der Funktion reduziert, um Parameter, einen Rückgabepunkt usw. abzurufen, müssen Sie die mit der Stapelausrichtung ausgerichtete Argumentgröße zum ESP-Register hinzufügen, in unserem Fall 4 Byte.


 global writeb global writew global writed section .text writeb: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 byte value aligned 4 bytes out dx, al ;write byte by port number an dx - value in al mov esp, ebp pop ebp ret writew: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 word value aligned 4 bytes out dx, ax ;write word by port number an dx - value in ax mov esp, ebp pop ebp ret writed: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 double word value aligned 4 bytes out dx, eax ;write double word by port number an dx - value in eax mov esp, ebp pop ebp ret 

Segmenteinrichtung


Wir kamen zum rätselhaftesten, aber gleichzeitig einfachsten Thema. Wie ich in einem früheren Artikel sagte, war die Seiten- und Segmentorganisation des Speichers in meinem Kopf gemischt, ich lud die Adresse der Seitentabelle in die GDTR und packte meinen Kopf. Ich habe mehrere Monate gebraucht, um das Material ausreichend zu lesen, es zu verdauen und es zu realisieren. Ich bin möglicherweise Opfer von Peter Abels Lehrbuch Assembler geworden. Die Sprache und Programmierung für den IBM PC “(ein großartiges Buch!), In dem die Segmentierung für den Intel 8086 beschrieben wird. In diesen angenehmen Zeiten haben wir die oberen 16 Bits einer 20-Bit-Adresse in das Segmentregister geladen, und das war die Adresse im Speicher. Es stellte sich als grausame Enttäuschung heraus, dass ab i286 im geschützten Modus alles völlig falsch ist.


Die bloße Theorie ist also, dass x86 ein segmentiertes Speichermodell unterstützt, da ältere Programme nur über 640 KB und dann 1 MB Speicher hinaus ausbrechen konnten.


Programmierer mussten darüber nachdenken, wie sie ausführbaren Code platzieren, wie sie Daten platzieren und wie sie ihre Sicherheit gewährleisten können. Das Aufkommen der Seitenorganisation machte eine segmentierte Organisation unnötig, blieb jedoch aus Gründen der Kompatibilität und des Schutzes (Trennung der Berechtigungen für Kernel-Space und User-Space) bestehen. Ohne sie ist es also einfach nirgendwo. Einige Prozessoranweisungen sind verboten, wenn die Berechtigungsstufe schwächer als 0 ist und der Zugriff zwischen Programm- und Kernelsegmenten einen Segmentierungsfehler verursacht.


Machen wir es noch einmal (hoffentlich im letzten) über die Adressübersetzung
Zeilenadresse [0x08: 0xFFFFFFFF] -> Segmentberechtigungen überprüfen 0x08 -> Virtuelle Adresse [0xFFFFFFFF] -> Seitentabelle + TLB -> Physikalische Adresse [0xAAAAFFFF]


Ein Segment wird nur innerhalb des Prozessors verwendet, in einem speziellen Segmentregister (CS, SS, DS, ES, FS, GS) gespeichert und ausschließlich zur Überprüfung der Rechte zur Ausführung von Code und zur Übertragungssteuerung verwendet. Aus diesem Grund können Sie die Kernelfunktion nicht einfach aus dem Benutzerbereich heraus aufrufen und aufrufen. Das Segment mit dem 0x18-Deskriptor (ich habe einen, Ihr ist anders) hat Rechte der Stufe 3, und das Segment mit dem 0x08-Deskriptor hat Rechte der Stufe 0. Gemäß der x86-Konvention kann ein Segment mit weniger Berechtigungen zum Schutz vor unbefugtem Zugriff ein Segment mit großen Berechtigungen nicht direkt aufrufen Rechte über jmp 0x08: [EAX], muss jedoch andere Mechanismen wie Traps, Gates und Interrupts verwenden.


Segmente und ihre Typen (Code, Daten, Leitern, Gates) müssen in der globalen GDT-Deskriptortabelle beschrieben werden, deren virtuelle Adresse und deren Größe in das GDTR-Register geladen wird. Beim Übergang zwischen Segmenten (der Einfachheit halber gehe ich davon aus, dass ein direkter Übergang möglich ist) müssen Sie den Befehl jmp 0x08 aufrufen: [EAX], wobei 0x08 der Offset des ersten gültigen Deskriptors in Bytes vom Anfang der Tabelle und EAX das Register ist, das die Übergangsadresse enthält. Der Offset (Selektor) wird in das CS-Register geladen, und der entsprechende Deskriptor wird in das Schattenregister des Prozessors geladen. Jeder Deskriptor ist eine 8-Byte-Struktur. Es ist gut dokumentiert und seine Beschreibung kann sowohl auf OSDev als auch in der Intel-Dokumentation gefunden werden (siehe den ersten Artikel).


Ich fasse zusammen. Wenn wir GDT initialisieren und den Übergang jmp 0x08: [EAX] ausführen, lautet der Prozessorstatus wie folgt:


  • GDTR enthält eine virtuelle GDT-Adresse
  • CS enthält den Wert 0x08
  • Ein Handle an die Adresse [GDTR + 0x08] wurde aus dem Speicher in das Schattenregister CS kopiert
  • Das EIP-Register enthält die Adresse aus dem EAX-Register

Der Nulldeskriptor muss immer nicht initialisiert sein und der Zugriff darauf ist verboten. Ich werde näher auf den TSS-Deskriptor und seine Bedeutung eingehen, wenn wir über Multithreading sprechen. Meine GDT-Tabelle sieht jetzt so aus:


 extern { fn load_gdt(base: *const GdtEntry, limit: u16); } pub unsafe fn setup_gdt() { GDT[5].set_offset((&super::tss::TSS) as *const _ as u32); GDT[5].set_limit(core::mem::size_of::<super::tss::Tss>() as u32); let gdt_ptr: *const GdtEntry = GDT.as_ptr(); let limit = (GDT.len() * core::mem::size_of::<GdtEntry>() - 1) as u16; load_gdt(gdt_ptr, limit); } static mut GDT: [GdtEntry; 7] = [ //null descriptor - cannot access GdtEntry::new(0, 0, 0, 0), //kernel code GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_0 | GDT_A_SYSTEM | GDT_A_EXECUTABLE | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //kernel data GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_0 | GDT_A_SYSTEM | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //user code GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_SYSTEM | GDT_A_EXECUTABLE | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //user data GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_SYSTEM | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //TSS - for interrupt handling in multithreading GdtEntry::new(0, 0, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_TSS_AVAIL, 0), GdtEntry::new(0, 0, 0, 0), ]; 

Und hier ist die Initialisierung, über die ich oben so viel gesprochen habe. Das Laden der GDT-Adresse und -Größe erfolgt über eine separate Struktur, die nur zwei Felder enthält. Die Adresse dieser Struktur wird an den Befehl lgdt übergeben. Laden Sie in die Datensegmentregister den folgenden Deskriptor mit einem Offset von 0x10.


 global load_gdt section .text gdtr dw 0 ; For limit storage dd 0 ; For base storage load_gdt: mov eax, [esp + 4] mov [gdtr + 2], eax mov ax, [esp + 8] mov [gdtr], ax lgdt [gdtr] jmp 0x08:.reload_CS .reload_CS: mov ax, 0x10 ; 0x10 points at the new data selector mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov ax, 0x28 ltr ax ret 

Dann wird alles etwas einfacher, aber nicht weniger interessant.


Unterbrechungen


Eigentlich ist es Zeit, uns die Möglichkeit zu geben, mit unserem Kern zu interagieren (zumindest um zu sehen, was wir auf der Tastatur drücken). Dazu müssen Sie den Interrupt-Controller initialisieren.


Lyrischer Exkurs über den Codestil.


Dank der Bemühungen der Community und speziell von Philip Opperman wurde Rust die x86-Interrupt-Aufrufkonvention hinzugefügt, mit der Sie Interrupt-Handler schreiben können, die iret ausführen. Ich habe mich jedoch bewusst entschieden, diesen Weg nicht zu gehen, da ich mich entschlossen habe, Assembler und Rust in verschiedene Dateien zu trennen und daher zu funktionieren. Ja, ich verwende unangemessen Stapelspeicher. Ich bin mir dessen bewusst, aber es schmeckt immer noch. Meine Interrupt-Handler sind in Assembler geschrieben und machen genau eines: Sie rufen fast dieselben Interrupt-Handler auf, die in Rust geschrieben sind. Bitte akzeptieren Sie diese Tatsache und lassen Sie sich verwöhnen.


Im Allgemeinen ähnelt das Initialisieren von Interrupts dem Initialisieren eines GDT, ist jedoch leichter zu verstehen. Auf der anderen Seite benötigen Sie viel einheitlichen Code. Die Entwickler von Redox OS treffen eine schöne Entscheidung, indem sie alle Freuden der Sprache nutzen, aber ich ging „auf die Stirn“ und entschied mich, Code-Duplizierung zuzulassen.


Gemäß der x86-Konvention haben wir Unterbrechungen, aber es gibt Ausnahmesituationen. In diesem Zusammenhang sind die Einstellungen für uns praktisch gleich. Der einzige Unterschied besteht darin, dass der Stapel beim Auslösen einer Ausnahme möglicherweise zusätzliche Informationen enthält. Zum Beispiel benutze ich es, um das Fehlen einer Seite zu behandeln, wenn ich mit einem Haufen arbeite (aber alles hat seine Zeit). Sowohl Interrupts als auch Ausnahmen werden aus derselben Tabelle verarbeitet, die Sie und ich ausfüllen müssen. Es ist auch erforderlich, den PIC (Programmable Interrupt Controller) zu programmieren. Es gibt auch APIC, aber ich habe es noch nicht herausgefunden.


Zur Arbeit mit PIC werde ich nicht viele Kommentare abgeben, da es im Netzwerk viele Beispiele für die Arbeit mit PIC gibt. Ich werde mit den Handlern im Assembler beginnen. Sie sind alle völlig identisch, daher werde ich den Code für den Spoiler entfernen.


IRQ
 global irq0 global irq1 ...... global irq14 global irq15 extern kirq0 extern kirq1 ...... extern kirq14 extern kirq15 section .text irq0: pusha call kirq0 popa iret irq1: pusha call kirq1 popa iret ...... irq14: pusha call kirq14 popa iret irq15: pusha call kirq15 popa iret 

Wie Sie sehen können, beginnen alle Aufrufe von Rust-Funktionen mit dem Präfix „k“ - zur Unterscheidung und Bequemlichkeit. Die Ausnahmebehandlung ist genau die gleiche. Für Assembler-Funktionen wird das Präfix "e" ausgewählt, für Rust "k". Der Page Fault-Handler ist anders, aber darüber - in den Hinweisen zur Speicherverwaltung.


Ausnahmen
 global e0_zero_divide global e1_debug ...... global eE_page_fault ...... global e14_virtualization global e1E_security extern k0_zero_divide extern k1_debug ...... extern kE_page_fault ...... extern k14_virtualization extern k1E_security section .text e0_zero_divide: pushad call k0_zero_divide popad iret e1_debug: pushad call k1_debug popad iret ...... eE_page_fault: pushad mov eax, [esp + 32] push eax mov eax, cr2 push eax call kE_page_fault pop eax pop eax popad add esp, 4 iret ...... e14_virtualization: pushad call k14_virtualization popad iret e1E_security: pushad call k1E_security popad iret 

Wir deklarieren Assembler-Handler:


 extern { fn load_idt(base: *const IdtEntry, limit: u16); fn e0_zero_divide(); fn e1_debug(); ...... fn e14_virtualization(); fn e1E_security(); fn irq0(); fn irq1(); ...... fn irq14(); fn irq15(); } 

Wir definieren Rust-Handler, die wir oben aufrufen. Bitte beachten Sie, dass ich zum Unterbrechen der Tastatur einfach den empfangenen Code anzeige, den ich von Port 0x60 erhalte - so funktioniert die Tastatur im einfachsten Modus. Ich hoffe, dass sich dies in Zukunft in einen vollwertigen Fahrer verwandelt. Nach jedem Interrupt müssen Sie das Signal vom Ende der Verarbeitung 0x20 an die Steuerung ausgeben, das ist wichtig! Andernfalls erhalten Sie keine weiteren Interrupts.


 #[no_mangle] pub unsafe extern fn kirq0() { // println!("IRQ 0"); outb(0x20, 0x20); } #[no_mangle] pub unsafe extern fn kirq1() { let ch: char = inb(0x60) as char; crate::arch::vga::VGA_WRITER.force_unlock(); println!("IRQ 1 {}", ch); outb(0x20, 0x20); } #[no_mangle] pub unsafe extern fn kirq2() { println!("IRQ 2"); outb(0x20, 0x20); } ... 

Initialisierung von IDT und PIC. Über PIC und seine Neuzuordnung habe ich eine große Anzahl von Tutorials mit unterschiedlichem Detaillierungsgrad gefunden, angefangen bei OSDev bis hin zu Amateurseiten. Da die Programmierprozedur mit einer konstanten Folge von Operationen und konstanten Befehlen arbeitet, werde ich diesen Code ohne weitere Erklärung geben. , 0x20-0x2F , 0x20 0x28, 16 IDT.


 unsafe fn setup_pic(pic1: u8, pic2: u8) { // Start initialization outb(PIC1, 0x11); outb(PIC2, 0x11); // Set offsets outb(PIC1 + 1, pic1); /* remap */ outb(PIC2 + 1, pic2); /* pics */ // Set up cascade outb(PIC1 + 1, 4); /* IRQ2 -> connection to slave */ outb(PIC2 + 1, 2); // Set up interrupt mode (1 is 8086/88 mode, 2 is auto EOI) outb(PIC1 + 1, 1); outb(PIC2 + 1, 1); // Unmask interrupts outb(PIC1 + 1, 0); outb(PIC2 + 1, 0); // Ack waiting outb(PIC1, 0x20); outb(PIC2, 0x20); } pub unsafe fn init_idt() { IDT[0x0].set_func(e0_zero_divide); IDT[0x1].set_func(e1_debug); ...... IDT[0x14].set_func(e14_virtualization); IDT[0x1E].set_func(e1E_security); IDT[0x20].set_func(irq0); IDT[0x21].set_func(irq1); ...... IDT[0x2E].set_func(irq14); IDT[0x2F].set_func(irq15); setup_pic(0x20, 0x28); let idt_ptr: *const IdtEntry = IDT.as_ptr(); let limit = (IDT.len() * core::mem::size_of::<IdtEntry>() - 1) as u16; load_idt(idt_ptr, limit); } 

IDTR GDTR — . STI — — , , ASCII- -.


 global load_idt section .text idtr dw 0 ; For limit storage dd 0 ; For base storage load_idt: mov eax, [esp + 4] mov [idtr + 2], eax mov ax, [esp + 8] mov [idtr], ax lidt [idtr] sti ret 

Nachwort


, , . setup_pd, . , , , .


- GitLab .


Vielen Dank für Ihre Aufmerksamkeit!


UPD: 3

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


All Articles