Wir schreiben ein Betriebssystem auf Rust. Implementieren des Seitenspeichers (neu)

In diesem Artikel erfahren Sie, wie Sie die Unterstützung des Seitenspeichers in unserem Kern implementieren. Zunächst werden wir verschiedene Methoden untersuchen, damit die Frames der physischen Seitentabelle für den Kernel verfügbar werden, und ihre Vor- und Nachteile diskutieren. Dann implementieren wir die Adressübersetzungsfunktion und die Funktion zum Erstellen eines neuen Mappings.

Diese Artikelserie wurde auf GitHub veröffentlicht . Wenn Sie Fragen oder Probleme haben, öffnen Sie dort das entsprechende Ticket. Alle Quellen für den Artikel befinden sich in diesem Thread .

Ein weiterer Artikel über Paging?
Wenn Sie diesem Zyklus folgen, haben Sie Ende Januar den Artikel „Seitenspeicher: Fortgeschrittene Ebene“ gesehen . Aber ich wurde für rekursive Seitentabellen kritisiert . Aus diesem Grund habe ich beschlossen, den Artikel neu zu schreiben und einen anderen Ansatz für den Zugriff auf Frames zu verwenden.

Hier ist eine neue Option. Der Artikel erklärt immer noch, wie rekursive Seitentabellen funktionieren, aber wir verwenden eine einfachere und leistungsfähigere Implementierung. Wir werden den vorherigen Artikel nicht löschen, sondern als veraltet markieren und nicht aktualisieren.

Ich hoffe, Sie genießen die neue Option!

Inhalt



Einführung


Im letzten Artikel haben wir die Prinzipien des Paging-Speichers und die Funktionsweise der vierstufigen Seitentabellen unter x86_64 . Wir haben auch festgestellt, dass der Loader bereits die Seitentabellenhierarchie für unseren Kernel eingerichtet hat, sodass der Kernel auf virtuellen Adressen ausgeführt wird. Dies erhöht die Sicherheit, da nicht autorisierter Zugriff auf den Speicher einen Seitenfehler verursacht, anstatt den physischen Speicher zufällig zu ändern.

Der Artikel konnte von unserem Kernel aus nicht auf Seitentabellen zugreifen, da diese im physischen Speicher gespeichert sind und der Kernel bereits auf virtuellen Adressen ausgeführt wird. Hier setzen wir das Thema fort und untersuchen verschiedene Optionen für den Zugriff auf die Frames der Seitentabelle vom Kernel aus. Wir werden die Vor- und Nachteile jedes einzelnen von ihnen diskutieren und dann die geeignete Option für unseren Kern auswählen.

Bootloader-Unterstützung ist erforderlich, daher werden wir sie zuerst konfigurieren. Anschließend implementieren wir eine Funktion, die die gesamte Hierarchie der Seitentabellen durchläuft, um virtuelle Adressen in physische Adressen zu übersetzen. Schließlich lernen wir, wie Sie neue Zuordnungen in Seitentabellen erstellen und nicht verwendete Speicherrahmen zum Erstellen neuer Tabellen finden.

Abhängigkeitsaktualisierungen


In diesem Artikel müssen Sie den bootloader Version 0.4.0 oder höher und x86_64 Version 0.5.2 oder höher in den Abhängigkeiten registrieren. Sie können die Abhängigkeiten in Cargo.toml :

 [dependencies] bootloader = "0.4.0" x86_64 = "0.5.2" 

Änderungen in diesen Versionen finden Sie im Bootloader-Protokoll und im x86_64-Protokoll .

Zugriff auf Seitentabellen


Der Zugriff auf Seitentabellen über den Kernel ist nicht so einfach, wie es scheint. Um das Problem zu verstehen, werfen Sie einen weiteren Blick auf die vierstufige Tabellenhierarchie aus dem vorherigen Artikel:



Wichtig ist, dass in jedem Seiteneintrag die physikalische Adresse der nächsten Tabelle gespeichert wird. Dies vermeidet die Übersetzung dieser Adressen, was die Leistung verringert und leicht zu Endlosschleifen führt.

Das Problem ist, dass wir vom Kernel nicht direkt auf physische Adressen zugreifen können, da dies auch für virtuelle Adressen funktioniert. Wenn wir beispielsweise zur Adresse 4 KiB , erhalten wir Zugriff auf die virtuelle Adresse 4 KiB und nicht auf die physische Adresse, an der die Seitentabelle der 4. Ebene gespeichert ist. Wenn wir auf die physische Adresse von 4 KiB zugreifen möchten, müssen wir eine virtuelle Adresse verwenden, die in diese übersetzt wird.

Um auf die Frames der Seitentabellen zuzugreifen, müssen Sie diesen Frames einige virtuelle Seiten zuordnen. Es gibt verschiedene Möglichkeiten, solche Zuordnungen zu erstellen.

Identitätszuordnung


Eine einfache Lösung ist die identische Anzeige aller Seitentabellen .



In diesem Beispiel sehen wir die identische Anzeige von Frames. Die physischen Adressen der Seitentabellen sind gleichzeitig gültige virtuelle Adressen, so dass wir ab dem Register CR3 problemlos auf die Seitentabellen aller Ebenen zugreifen können.

Dieser Ansatz überfüllt jedoch den virtuellen Adressraum und macht es schwierig, große zusammenhängende Bereiche des freien Speichers zu finden. Angenommen, wir möchten in der obigen Abbildung einen virtuellen Speicherbereich von 1000 KB erstellen, um beispielsweise eine Datei im Speicher anzuzeigen . Wir können nicht mit der 28 KiB Region beginnen, da sie bei 1004 KiB auf einer bereits belegten Seite liegt. Daher müssen Sie weiter suchen, bis wir ein geeignetes großes Fragment finden, beispielsweise mit 1008 KiB . Es gibt das gleiche Fragmentierungsproblem wie im segmentierten Speicher.

Darüber hinaus ist die Erstellung neuer Seitentabellen viel komplizierter, da physische Frames gefunden werden müssen, deren entsprechende Seiten noch nicht verwendet werden. Für unsere Datei haben wir beispielsweise einen Bereich von 1000 KB virtuellem Speicher reserviert, beginnend bei der Adresse 1008 KiB . Jetzt können wir keinen Frame mit einer physischen Adresse zwischen 1000 KiB und 2008 KiB , da dieser nicht identisch angezeigt werden kann.

Versetzte Karte korrigiert


Um eine Überlastung des virtuellen Adressraums zu vermeiden, können Sie die Seitentabellen in einem separaten Speicherbereich anzeigen. Anstatt die Zuordnung zu identifizieren, ordnen wir daher Frames mit einem festen Versatz im virtuellen Adressraum zu. Zum Beispiel kann der Versatz 10 TiB betragen:



Indem wir diesen Bereich des virtuellen Speichers ausschließlich für die Anzeige von Seitentabellen zuweisen, vermeiden wir die Probleme einer identischen Anzeige. Das Reservieren eines so großen Bereichs des virtuellen Adressraums ist nur möglich, wenn der virtuelle Adressraum viel größer als der physische Speicher ist. Unter x86_64 dies kein Problem, da der 48-Bit-Adressraum 256 TiB beträgt.

Dieser Ansatz hat jedoch den Nachteil, dass Sie beim Erstellen jeder Seitentabelle eine neue Zuordnung erstellen müssen. Darüber hinaus ist der Zugriff auf Tabellen in anderen Adressräumen nicht zulässig, was beim Erstellen eines neuen Prozesses hilfreich wäre.

Vollständige Zuordnung des physischen Speichers


Wir können diese Probleme lösen, indem wir den gesamten physischen Speicher und nicht nur Seitentabellenrahmen anzeigen :



Dieser Ansatz ermöglicht es dem Kernel, auf einen beliebigen physischen Speicher zuzugreifen, einschließlich Seitentabellenrahmen anderer Adressräume. Ein Bereich des virtuellen Speichers ist in der gleichen Größe wie zuvor reserviert, es sind jedoch nur keine nicht übereinstimmenden Seiten vorhanden.

Der Nachteil dieses Ansatzes besteht darin, dass zusätzliche Seitentabellen erforderlich sind, um den physischen Speicher anzuzeigen. Diese Seitentabellen sollten irgendwo gespeichert werden, damit sie einen Teil des physischen Speichers belegen, was bei Geräten mit wenig RAM ein Problem sein kann.

Auf x86_64 können wir jedoch riesige 2-MiB- Seiten anstelle der Standardgröße von 4 KiB für die Anzeige verwenden. Um 32 GB physischen Speicher anzuzeigen, sind daher nur 132 KB pro Seitentabelle erforderlich: nur eine Tabelle der dritten Ebene und 32 Tabellen der zweiten Ebene. Riesige Seiten werden auch effizienter zwischengespeichert, da sie weniger Einträge im TLB (Dynamic Translation Buffer) verwenden.

Temporäre Anzeige


Bei Geräten mit sehr wenig physischem Speicher können Sie Seitentabellen nur vorübergehend anzeigen, wenn Sie darauf zugreifen müssen. Für temporäre Vergleiche ist eine identische Anzeige nur der Tabelle der ersten Ebene erforderlich:



In dieser Abbildung verwaltet eine Tabelle der Ebene 1 die ersten 2 MiB des virtuellen Adressraums. Dies ist möglich, weil der Zugriff vom CR3-Register über Null-Einträge in den Tabellen der Ebenen 4, 3 und 2 erfolgt. Der Datensatz mit Index 8 übersetzt die virtuelle Seite mit 32 KiB in einen physischen Rahmen mit 32 KiB , wodurch die Tabelle der Ebene 1 selbst identifiziert wird. In der Abbildung ist dies durch einen horizontalen Pfeil dargestellt.

Durch Schreiben in die identisch zugeordnete Tabelle der Ebene 1 kann unser Kernel bis zu 511 Zeitvergleiche erstellen (512 abzüglich des für die Identitätszuordnung erforderlichen Datensatzes). Im obigen Beispiel erstellt der Kernel zwei Zeitvergleiche:

  • Zuordnen eines Nulleintrags in einer Tabelle der Ebene 1 zu einem Frame mit 24 KiB . Dadurch wird eine temporäre Zuordnung der virtuellen Seite bei 0 KiB zu dem physischen Rahmen der Tabelle der Seitenebene 2 erstellt, der durch den gepunkteten Pfeil angezeigt wird.
  • Ordnen Sie den 9. Datensatz eines Level 1-Tisches einem Frame von 4 KiB . Dadurch wird eine temporäre Zuordnung der virtuellen Seite mit 36 KiB zum physischen Rahmen der Tabelle der Seitenebene 4 erstellt, die durch den gepunkteten Pfeil gekennzeichnet ist.

Jetzt kann der Kernel auf eine Tabelle der Ebene 2 zugreifen, indem er auf eine Seite schreibt, die bei 0 KiB beginnt, und auf eine Tabelle der Ebene 4, indem er auf eine Seite schreibt, die bei 33 KiB beginnt.

Der Zugriff auf einen beliebigen Rahmen der Seitentabelle mit temporären Zuordnungen besteht daher aus den folgenden Aktionen:

  • Suchen Sie einen freien Eintrag in der identisch angezeigten Tabelle der Ebene 1.
  • Ordnen Sie diesen Eintrag dem physischen Rahmen der Seitentabelle zu, auf die wir zugreifen möchten.
  • Greifen Sie über die dem Eintrag zugeordnete virtuelle Seite auf diesen Frame zu.
  • Setzen Sie den Datensatz wieder auf nicht verwendet, wodurch die temporäre Zuordnung entfernt wird.

Bei diesem Ansatz bleibt der virtuelle Adressraum sauber, da ständig dieselben 512 virtuellen Seiten verwendet werden. Der Nachteil ist eine gewisse Umständlichkeit, insbesondere da für einen neuen Vergleich möglicherweise mehrere Tabellenebenen geändert werden müssen, dh der beschriebene Vorgang mehrmals wiederholt werden muss.

Rekursive Seitentabellen


Ein weiterer interessanter Ansatz, für den überhaupt keine zusätzlichen Seitentabellen erforderlich sind, ist der rekursive Abgleich .

Die Idee ist, einige Datensätze aus der Tabelle der vierten Ebene in sie selbst zu übersetzen. Daher reservieren wir tatsächlich einen Teil des virtuellen Adressraums und ordnen diesem Raum alle aktuellen und zukünftigen Tabellenrahmen zu.

Schauen wir uns ein Beispiel an, um zu verstehen, wie das alles funktioniert:



Der einzige Unterschied zum Beispiel am Anfang des Artikels besteht in einem zusätzlichen Datensatz mit dem Index 511 in der Tabelle der Ebene 4, der dem physischen Frame 4 KiB , der sich in dieser Tabelle selbst befindet.

Wenn die CPU in diesen Datensatz wechselt, bezieht sie sich nicht auf die Tabelle der Ebene 3, sondern erneut auf die Tabelle der Ebene 4. Dies ähnelt einer rekursiven Funktion, die sich selbst aufruft. Es ist wichtig, dass der Prozessor davon ausgeht, dass jeder Eintrag in der Tabelle der Ebene 4 auf eine Tabelle der Ebene 3 verweist. Daher wird die Tabelle der Ebene 4 jetzt als Tabelle der Ebene 3 behandelt. Dies funktioniert, da Tabellen aller Ebenen in x86_64 dieselbe Struktur haben.

Indem Sie einem rekursiven Datensatz ein oder mehrere Male folgen, bevor Sie mit der eigentlichen Konvertierung beginnen, können Sie die Anzahl der Ebenen, die der Prozessor durchläuft, effektiv reduzieren. Wenn wir beispielsweise dem rekursiven Datensatz einmal folgen und dann zur Tabelle der Ebene 3 wechseln, denkt der Prozessor, dass die Tabelle der Ebene 3 eine Tabelle der Ebene 2 ist. Im weiteren Verlauf betrachtet er die Tabelle der Ebene 2 als Tabelle der Ebene 1 und die Tabelle der Ebene 1 als zugeordnet Frame im physischen Speicher. Dies bedeutet, dass wir jetzt in die Tabelle der Seitenebene 1 lesen und schreiben können, da der Prozessor dies für einen zugeordneten Frame hält. Die folgende Abbildung zeigt die fünf Schritte einer solchen Übersetzung:



Ebenso können wir einem rekursiven Eintrag zweimal folgen, bevor wir mit der Konvertierung beginnen, um die Anzahl der übergebenen Ebenen auf zwei zu reduzieren:



Lassen Sie uns diese Prozedur Schritt für Schritt durchgehen. Zuerst folgt die CPU einem rekursiven Eintrag in der Tabelle der Ebene 4 und denkt, dass sie die Tabelle der Ebene 3 erreicht hat. Dann folgt sie erneut dem rekursiven Datensatz und denkt, dass sie die Ebene 2 erreicht hat. In Wirklichkeit befindet sie sich jedoch immer noch auf Ebene 4. Dann geht die CPU zur neuen Adresse und gelangt in die Level 3-Tabelle, denkt jedoch, dass sie sich bereits in der Level 1-Tabelle befindet. Schließlich glaubt der Prozessor am nächsten Einstiegspunkt in der Level 2-Tabelle, auf den physischen Speicherrahmen zugegriffen zu haben. Dies ermöglicht uns das Lesen und Schreiben in eine Tabelle der Ebene 2.

Auf die Tabellen der Ebenen 3 und 4 wird ebenfalls zugegriffen. Um auf die Tabelle der Ebenen 3 zuzugreifen, folgen wir dreimal einem rekursiven Eintrag: Der Prozessor glaubt, dass er sich bereits in der Tabelle der Ebene 1 befindet, und im nächsten Schritt erreichen wir die Ebene 3, die die CPU als zugeordneten Frame betrachtet. Um auf die Level 4-Tabelle selbst zuzugreifen, folgen wir einfach viermal dem rekursiven Datensatz, bis der Prozessor die Level 4-Tabelle selbst als zugeordneten Frame verarbeitet (in der folgenden Abbildung blau).



Das Konzept ist zunächst schwer zu verstehen, aber in der Praxis funktioniert es ziemlich gut.

Adressberechnung


Wir können also auf Tabellen aller Ebenen zugreifen, indem wir einem oder mehreren rekursiven Datensätzen folgen. Da Indizes in Tabellen mit vier Ebenen direkt von der virtuellen Adresse abgeleitet werden, müssen für diese Methode spezielle virtuelle Adressen erstellt werden. Wie wir uns erinnern, werden Seitentabellenindizes wie folgt aus der Adresse extrahiert:



Angenommen, wir möchten auf eine Tabelle der Ebene 1 zugreifen, in der eine bestimmte Seite angezeigt wird. Wie wir oben erfahren haben, müssen Sie einmal einen rekursiven Datensatz und dann die Indizes der 4., 3. und 2. Ebene durchlaufen. Dazu verschieben wir alle Adressblöcke einen Block nach rechts und setzen den Index des rekursiven Datensatzes an die Stelle des Anfangsindex der Ebene 4:



Um auf die Tabelle der Ebene 2 dieser Seite zuzugreifen, verschieben wir alle Indexblöcke zwei Blöcke nach rechts und setzen den rekursiven Index an die Stelle beider Quellblöcke: Ebene 4 und Ebene 3:



Um auf die Tabelle der Ebene 3 zuzugreifen, machen wir dasselbe, wir verschieben einfach bereits drei Adressblöcke nach rechts.



Um auf die Tabelle der Ebene 4 zuzugreifen, verschieben Sie alle vier Blöcke nach rechts.



Jetzt können Sie virtuelle Adressen für Seitentabellen aller vier Ebenen berechnen. Wir können sogar eine Adresse berechnen, die genau auf einen bestimmten Seitentabelleneintrag verweist, indem wir dessen Index mit 8 multiplizieren, der Größe des Seitentabelleneintrags.

Die folgende Tabelle zeigt die Struktur der Adressen für den Zugriff auf verschiedene Arten von Frames:

Virtuelle Adresse fürAdressstruktur ( oktal )
Seite0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE
Eintrag in Level 1 Tabelle0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD
Eintrag in eine Level 2 Tabelle0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC
Eintrag in eine Level 3 Tabelle0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB
Eintrag in Level 4 Tabelle0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA

Hier ist der Level 4-Index, ist Level 3, ist Level 2 und DDD ist Level 1-Index für den angezeigten Frame, EEEE ist sein Offset. RRR ist der Index des rekursiven Datensatzes. Ein Index (drei Ziffern) wird durch Multiplikation mit 8 (der Größe des Seitentabelleneintrags) in einen Versatz (vier Ziffern) umgewandelt. Mit diesem Versatz zeigt die resultierende Adresse direkt auf den entsprechenden Seitentabelleneintrag.

SSSS sind Erweiterungsbits der vorzeichenbehafteten Ziffer, SSSS sie sind alle Kopien von Bit 47. Dies ist eine spezielle Anforderung für gültige Adressen in der x86_64-Architektur, die wir in einem vorherigen Artikel erörtert haben.

Die Adressen sind oktal , da jedes Oktalzeichen drei Bits darstellt, wodurch Sie die 9-Bit-Indizes von Tabellen auf verschiedenen Ebenen klar trennen können. Dies ist im Hexadezimalsystem nicht möglich, bei dem jedes Zeichen vier Bits darstellt.

Rostcode


Sie können solche Adressen im Rust-Code mithilfe bitweiser Operationen erstellen:

 // the virtual address whose corresponding page tables you want to access let addr: usize = […]; let r = 0o777; // recursive index let sign = 0o177777 << 48; // sign extension // retrieve the page table indices of the address that we want to translate let l4_idx = (addr >> 39) & 0o777; // level 4 index let l3_idx = (addr >> 30) & 0o777; // level 3 index let l2_idx = (addr >> 21) & 0o777; // level 2 index let l1_idx = (addr >> 12) & 0o777; // level 1 index let page_offset = addr & 0o7777; // calculate the table addresses let level_4_table_addr = sign | (r << 39) | (r << 30) | (r << 21) | (r << 12); let level_3_table_addr = sign | (r << 39) | (r << 30) | (r << 21) | (l4_idx << 12); let level_2_table_addr = sign | (r << 39) | (r << 30) | (l4_idx << 21) | (l3_idx << 12); let level_1_table_addr = sign | (r << 39) | (l4_idx << 30) | (l3_idx << 21) | (l2_idx << 12); 

Dieser Code setzt voraus, dass eine rekursive Zuordnung des letzten Datensatzes der Ebene 4 mit dem Index 0o777 (511) rekursiv übereinstimmt. Dies ist derzeit nicht der Fall, sodass der Code noch nicht funktioniert. Im Folgenden erfahren Sie, wie Sie den Loader anweisen, eine rekursive Zuordnung einzurichten.

Alternativ zur manuellen Ausführung bitweiser Operationen können Sie den Typ RecursivePageTable der x86_64 Kiste verwenden, der sichere Abstraktionen für verschiedene Tabellenoperationen bietet. Der folgende Code zeigt beispielsweise, wie eine virtuelle Adresse in die entsprechende physische Adresse konvertiert wird:

 // in src/memory.rs use x86_64::structures::paging::{Mapper, Page, PageTable, RecursivePageTable}; use x86_64::{VirtAddr, PhysAddr}; /// Creates a RecursivePageTable instance from the level 4 address. let level_4_table_addr = […]; let level_4_table_ptr = level_4_table_addr as *mut PageTable; let recursive_page_table = unsafe { let level_4_table = &mut *level_4_table_ptr; RecursivePageTable::new(level_4_table).unwrap(); } /// Retrieve the physical address for the given virtual address let addr: u64 = […] let addr = VirtAddr::new(addr); let page: Page = Page::containing_address(addr); // perform the translation let frame = recursive_page_table.translate_page(page); frame.map(|frame| frame.start_address() + u64::from(addr.page_offset())) 

Auch dieser Code erfordert eine korrekte rekursive Zuordnung. Bei dieser Zuordnung wird die fehlende level_4_table_addr wie im ersten Codebeispiel berechnet.



Rekursives Mapping ist eine interessante Methode, die zeigt, wie leistungsfähig Matching durch eine einzelne Tabelle sein kann. Es ist relativ einfach zu implementieren und erfordert nur eine minimale Einrichtung (nur ein rekursiver Eintrag), daher ist dies eine gute Wahl für die ersten Experimente.

Aber es hat einige Nachteile:

  • Eine große Menge an virtuellem Speicher (512 GiB). Dies ist in einem großen 48-Bit-Adressraum kein Problem, kann jedoch zu einem suboptimalen Cache-Verhalten führen.
  • Es bietet nur Zugriff auf den aktuell aktiven Adressraum. Der Zugriff auf andere Adressräume ist weiterhin möglich, indem der rekursive Eintrag geändert wird. Für die Umschaltung ist jedoch ein temporärer Abgleich erforderlich. Wie das geht, haben wir in einem früheren (veralteten) Artikel beschrieben.
  • Dies hängt stark vom Format der x86-Seitentabelle ab und funktioniert möglicherweise nicht auf anderen Architekturen.

Bootloader-Unterstützung


Alle oben beschriebenen Ansätze erfordern Änderungen an den Seitentabellen und den entsprechenden Einstellungen. Zum Beispiel, um den physischen Speicher identisch oder rekursiv Datensätze einer Tabelle der vierten Ebene zuzuordnen. Das Problem ist, dass wir diese Einstellungen nicht ohne Zugriff auf die Seitentabellen vornehmen können.

Also brauche ich Hilfe vom Bootloader. Er hat Zugriff auf Seitentabellen, sodass er alle erforderlichen Anzeigen erstellen kann. In der aktuellen Implementierung unterstützt die bootloader Kiste die beiden oben genannten Ansätze mithilfe von Frachtfunktionen :

  • Die Funktion map_physical_memory ordnet den gesamten physischen Speicher irgendwo im virtuellen Adressraum zu. Somit erhält der Kernel Zugriff auf den gesamten physischen Speicher und kann einen Ansatz mit der Anzeige des vollständigen physischen Speichers anwenden.
  • Mit der Funktion recursive_page_table zeigt der Loader rekursiv einen Seitentabelleneintrag der vierten Ebene an. Dadurch kann der Kernel gemäß der im Abschnitt "Rekursive Seitentabellen" beschriebenen Methode arbeiten.

Für unseren Kernel wählen wir die erste Option, da dies ein einfacher, plattformunabhängiger und leistungsfähigerer Ansatz ist (er ermöglicht auch den Zugriff auf andere Frames, nicht nur auf Seitentabellen). Fügen Sie zur Unterstützung des Bootloaders die Funktion zu den Abhängigkeiten hinzu map_physical_memory:

 [dependencies] bootloader = { version = "0.4.0", features = ["map_physical_memory"]} 

Wenn diese Funktion aktiviert ist, ordnet der Bootloader den gesamten physischen Speicher einem nicht verwendeten Bereich virtueller Adressen zu. Um einen Bereich virtueller Adressen an den Kernel zu übergeben, übergibt der Bootloader die Struktur der Startinformationen .

Boot-Informationen


Die Kiste bootloaderdefiniert die Struktur von BootInfo mit allen Informationen, die an den Kernel übergeben werden. Die Struktur wird noch finalisiert, daher kann es beim Upgrade auf zukünftige Versionen, die nicht mit Semver kompatibel sind, zu Fehlern kommen . Derzeit hat die Struktur zwei Felder: memory_mapund physical_memory_offset:

  • Das Feld memory_mapbietet einen Überblick über den verfügbaren physischen Speicher. Es teilt dem Kernel mit, wie viel physischer Speicher auf dem System verfügbar ist und welche Speicherbereiche für Geräte wie VGA reserviert sind. Eine Speicherkarte kann vom BIOS oder der UEFI-Firmware angefordert werden, jedoch nur zu Beginn des Startvorgangs. Aus diesem Grund muss der Loader dies bereitstellen, da der Kernel diese Informationen dann nicht mehr empfangen kann. Eine Speicherkarte wird später in diesem Artikel nützlich sein.
  • physical_memory_offsetmeldet die virtuelle Startadresse der physischen Speicherzuordnung. Wenn wir diesen Offset zur physischen Adresse hinzufügen, erhalten wir die entsprechende virtuelle Adresse. Dies ermöglicht den Zugriff vom Kernel auf einen beliebigen physischen Speicher.

Der Loader übergibt die Struktur BootInfoals Argument &'static BootInfoan die Funktion an den Kernel _start. Fügen Sie es hinzu:

 // in src/main.rs use bootloader::BootInfo; #[cfg(not(test))] #[no_mangle] pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! { // new argument […] } 

Es ist wichtig, den richtigen Argumenttyp anzugeben, da der Compiler den richtigen Signaturtyp unserer Einstiegspunktfunktion nicht kennt.

Einstiegspunktmakro


Da die Funktion _startextern vom Loader aufgerufen wird, wird die Signatur der Funktion nicht überprüft. Dies bedeutet, dass wir beliebige Argumente ohne Kompilierungsfehler akzeptieren können, dies jedoch abstürzt oder ein undefiniertes Laufzeitverhalten verursacht.

Um sicherzustellen, dass die Einstiegspunktfunktion immer die richtige Signatur hat, stellt die Kiste bootloaderein Makro bereit entry_point. Wir schreiben unsere Funktion mit diesem Makro neu:

 // in src/main.rs use bootloader::{BootInfo, entry_point}; entry_point!(kernel_main); #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] } 

Sie müssen nicht mehr für den Einstiegspunkt extern "C"oder no_mangle, da das Makro für uns den realen Einstiegspunkt der unteren Ebene definiert, verwenden _start. Die Funktion ist kernel_mainjetzt zu einer völlig normalen Rust-Funktion geworden, sodass wir einen beliebigen Namen dafür wählen können. Wichtig ist, dass es nach Typ überprüft wird. Wenn Sie also die falsche Signatur verwenden, z. B. indem Sie ein Argument hinzufügen oder seinen Typ ändern, tritt ein Kompilierungsfehler auf

Implementierung


Jetzt haben wir Zugriff auf den physischen Speicher und können endlich mit der Implementierung des Systems beginnen. Betrachten Sie zunächst die aktuell aktiven Seitentabellen, auf denen der Kernel ausgeführt wird. Erstellen Sie im zweiten Schritt eine Übersetzungsfunktion, die die physische Adresse zurückgibt, der diese virtuelle Adresse zugeordnet ist. Im letzten Schritt werden wir versuchen, die Seitentabellen zu ändern, um eine neue Zuordnung zu erstellen.

Erstellen Sie zunächst ein neues Modul im Code memory:

 // in src/lib.rs pub mod memory; 

Erstellen Sie für das Modul eine leere Datei src/memory.rs.

Zugriff auf Seitentabellen


Am Ende des vorherigen Artikels haben wir versucht, die Seitentabelle zu betrachten, auf der der Kernel arbeitet, konnten jedoch nicht auf den physischen Frame zugreifen, auf den das Register zeigt CR3. Jetzt können wir von diesem Ort aus weiterarbeiten: Die Funktion active_level_4_tablegibt einen Link zur aktiven Seitentabelle der vierten Ebene zurück:

 // in src/memory.rs use x86_64::structures::paging::PageTable; /// Returns a mutable reference to the active level 4 table. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. Also, this function must be only called once /// to avoid aliasing `&mut` references (which is undefined behavior). pub unsafe fn active_level_4_table(physical_memory_offset: u64) -> &'static mut PageTable { use x86_64::{registers::control::Cr3, VirtAddr}; let (level_4_table_frame, _) = Cr3::read(); let phys = level_4_table_frame.start_address(); let virt = VirtAddr::new(phys.as_u64() + physical_memory_offset); let page_table_ptr: *mut PageTable = virt.as_mut_ptr(); &mut *page_table_ptr // unsafe } 

Zuerst lesen wir den physischen Rahmen der aktiven Tabelle der 4. Ebene aus dem Register CR3. Dann nehmen wir die physische Startadresse und konvertieren sie durch Hinzufügen in eine virtuelle Adresse physical_memory_offset. Konvertieren Sie schließlich die Adresse *mut PageTabledurch die Methode in einen as_mut_ptrRohzeiger und erstellen Sie dann unsicher einen Link daraus &mut PageTable. Wir erstellen &mutstattdessen den Link &, da wir später in diesem Artikel diese Seitentabellen ändern werden.

Hier muss kein unsicherer Block eingefügt werden, da Rust den gesamten Körper unsafe fnals einen großen, unsicheren Block betrachtet. Dies erhöht die Risiken, da es möglich ist, in den vorherigen Zeilen versehentlich einen unsicheren Betrieb einzuführen. Es macht es auch schwierig, unsichere Vorgänge zu erkennen. Es wurde bereits ein RFC erstellt , um dieses Verhalten von Rust zu ändern.

Jetzt können wir diese Funktion verwenden, um die Datensätze der Tabelle der vierten Ebene auszugeben:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::active_level_4_table; let l4_table = unsafe { active_level_4_table(boot_info.physical_memory_offset) }; for (i, entry) in l4_table.iter().enumerate() { if !entry.is_unused() { println!("L4 Entry {}: {:?}", i, entry); } } println!("It did not crash!"); blog_os::hlt_loop(); } 

Wir physical_memory_offsetübergeben das entsprechende Feld der Struktur BootInfo. Dann verwenden wir eine Funktion iter, um die Seitentabelleneinträge zu durchlaufen, und einen Kombinator enumerate, um ijedem Element einen Index hinzuzufügen . Es werden nur nicht leere Einträge angezeigt, da nicht alle 512 Einträge auf den Bildschirm passen.

Wenn wir den Code ausführen, sehen wir folgendes Ergebnis:



Wir sehen mehrere nicht leere Datensätze, die verschiedenen Tabellen der dritten Ebene zugeordnet sind. Es werden so viele Speicherbereiche verwendet, weil separate Bereiche für Kernelcode, Kernelstapel, physische Speicherübersetzung und Startinformationen benötigt werden.

Um die Seitentabellen durchzugehen und die Tabelle der dritten Ebene zu betrachten, können wir den angezeigten Frame erneut in eine virtuelle Adresse konvertieren:

 // in the for loop in src/main.rs use x86_64::{structures::paging::PageTable, VirtAddr}; if !entry.is_unused() { println!("L4 Entry {}: {:?}", i, entry); // get the physical address from the entry and convert it let phys = entry.frame().unwrap().start_address(); let virt = phys.as_u64() + boot_info.physical_memory_offset; let ptr = VirtAddr::new(virt).as_mut_ptr(); let l3_table: &PageTable = unsafe { &*ptr }; // print non-empty entries of the level 3 table for (i, entry) in l3_table.iter().enumerate() { if !entry.is_unused() { println!(" L3 Entry {}: {:?}", i, entry); } } } 

Wiederholen Sie diesen Vorgang für Datensätze der dritten und zweiten Ebene, um die Tabellen der zweiten und ersten Ebene anzuzeigen. Wie Sie sich vorstellen können, wächst die Menge an Code sehr schnell, sodass wir nicht die vollständige Liste veröffentlichen werden.

Das manuelle Durchlaufen von Tabellen ist interessant, da es hilft zu verstehen, wie der Prozessor Adressen übersetzt. Normalerweise sind wir jedoch nur daran interessiert, eine physische Adresse für eine bestimmte virtuelle Adresse anzuzeigen. Erstellen wir daher eine Funktion dafür.

Adressübersetzung


Um eine virtuelle Adresse in eine physische Adresse zu übersetzen, müssen wir eine vierstufige Seitentabelle durchgehen, bis wir den zugeordneten Frame erreichen. Erstellen wir eine Funktion, die diese Adressübersetzung ausführt:

 // in src/memory.rs use x86_64::{PhysAddr, VirtAddr}; /// Translates the given virtual address to the mapped physical address, or /// `None` if the address is not mapped. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. pub unsafe fn translate_addr(addr: VirtAddr, physical_memory_offset: u64) -> Option<PhysAddr> { translate_addr_inner(addr, physical_memory_offset) } 

Wir verweisen auf eine sichere Funktion translate_addr_inner, um die Menge an unsicherem Code zu begrenzen. Wie oben erwähnt, betrachtet Rust den gesamten Körper unsafe fnals einen großen unsicheren Block. Durch Aufrufen einer sicheren Funktion machen wir jede Operation erneut explizit unsafe.

Eine spezielle interne Funktion hat echte Funktionalität:

 // in src/memory.rs /// Private function that is called by `translate_addr`. /// /// This function is safe to limit the scope of `unsafe` because Rust treats /// the whole body of unsafe functions as an unsafe block. This function must /// only be reachable through `unsafe fn` from outside of this module. fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: u64) -> Option<PhysAddr> { use x86_64::structures::paging::page_table::FrameError; use x86_64::registers::control::Cr3; // read the active level 4 frame from the CR3 register let (level_4_table_frame, _) = Cr3::read(); let table_indexes = [ addr.p4_index(), addr.p3_index(), addr.p2_index(), addr.p1_index() ]; let mut frame = level_4_table_frame; // traverse the multi-level page table for &index in &table_indexes { // convert the frame into a page table reference let virt = frame.start_address().as_u64() + physical_memory_offset; let table_ptr: *const PageTable = VirtAddr::new(virt).as_ptr(); let table = unsafe {&*table_ptr}; // read the page table entry and update `frame` let entry = &table[index]; frame = match entry.frame() { Ok(frame) => frame, Err(FrameError::FrameNotPresent) => return None, Err(FrameError::HugeFrame) => panic!("huge pages not supported"), }; } // calculate the physical address by adding the page offset Some(frame.start_address() + u64::from(addr.page_offset())) } 

Anstatt die Funktion active_level_4_tablewiederzuverwenden, lesen wir den Frame der vierten Ebene erneut aus dem Register CR3, da dies die Implementierung des Prototyps vereinfacht. Keine Sorge, wir werden die Lösung bald verbessern.

Die Struktur VirtAddrbietet bereits Methoden zur Berechnung von Indizes in Seitentabellen mit vier Ebenen. Wir speichern diese Indizes in einem kleinen Array, da Sie so alle Tabellen durchlaufen können for. Außerhalb der Schleife erinnern wir uns an den zuletzt besuchten Frame, um die physikalische Adresse später zu berechnen. framezeigt auf die Frames der Seitentabelle während der Iteration und auf den zugehörigen Frame nach der letzten Iteration, dh nach dem Übergeben des Level 1-Datensatzes.

Innerhalb der Schleife wenden wir erneut anphysical_memory_offsetum einen Frame in einen Seitentabellenlink zu konvertieren. Dann lesen wir den Datensatz der aktuellen Seitentabelle und verwenden die Funktion PageTableEntry::frame, um den übereinstimmenden Frame abzurufen. Wenn der Datensatz keinem Frame zugeordnet ist, kehren Sie zurück None. Wenn der Datensatz eine riesige Seite von 2 MiB oder 1 GiB anzeigt, haben wir bisher eine Panik.

Überprüfen wir also die Übersetzungsfunktion an einigen Adressen:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::translate_addr; use x86_64::VirtAddr; let addresses = [ // the identity-mapped vga buffer page 0xb8000, // some code page 0x20010a, // some stack page 0x57ac_001f_fe48, // virtual address mapped to physical address 0 boot_info.physical_memory_offset, ]; for &address in &addresses { let virt = VirtAddr::new(address); let phys = unsafe { translate_addr(virt, boot_info.physical_memory_offset) }; println!("{:?} -> {:?}", virt, phys); } println!("It did not crash!"); blog_os::hlt_loop(); } 

Wenn wir den Code ausführen, erhalten wir das folgende Ergebnis:



Wie erwartet wird bei einer identischen Zuordnung die Adresse 0xb8000in dieselbe physikalische Adresse konvertiert. Die Codepage und die Stapelseite werden in beliebige physische Adressen konvertiert, die davon abhängen, wie der Loader die anfängliche Zuordnung für unseren Kernel erstellt hat. Die Zuordnung physical_memory_offsetsollte auf die physische Adresse verweisen 0, schlägt jedoch fehl, da die Übersetzung aus Effizienzgründen große Seiten verwendet. Eine zukünftige Version des Bootloaders wendet möglicherweise dieselbe Optimierung für die Kernel- und Stack-Seiten an.

Verwenden von MappedPageTable


Die Übersetzung virtueller Adressen in physische Adressen ist eine typische Aufgabe des Betriebssystemkerns, daher x86_64bietet die Kiste eine Abstraktion dafür. Es unterstützt bereits große Seiten und einige andere Funktionen, außer dass translate_addrwir es verwenden, anstatt unserer eigenen Implementierung Unterstützung für große Seiten hinzuzufügen.

Die Basis der Abstraktion sind zwei Merkmale, die verschiedene Übersetzungsfunktionen der Seitentabelle definieren:

  • Das Merkmal Mapperbietet Funktionen, die auf Seiten funktionieren. Zum Beispiel, translate_pageum diese Seite in einen Rahmen derselben Größe map_tozu übersetzen und eine neue Zuordnung in der Tabelle zu erstellen.
  • Das Merkmal MapperAllSizesimpliziert die Anwendung Mapperfür alle Seitengrößen. Darüber hinaus bietet es Funktionen, die mit Seiten unterschiedlicher Größe arbeiten, einschließlich translate_addroder allgemein translate.

Merkmale definieren nur die Schnittstelle, bieten jedoch keine Implementierung. Jetzt x86_64bietet die Kiste zwei Typen, die Merkmale implementieren: MappedPageTableund RecursivePageTable. Das erste erfordert, dass jeder Frame der Seitentabelle irgendwo angezeigt wird (z. B. mit einem Versatz). Der zweite Typ kann verwendet werden, wenn die Tabelle der vierten Ebene rekursiv angezeigt wird.

Wir haben den gesamten physischen Speicher zugeordnet physical_memory_offset, sodass Sie den Typ MappedPageTable verwenden können. Erstellen Sie zum Initialisieren eine neue Funktion initim Modul memory:

 use x86_64::structures::paging::{PhysFrame, MapperAllSizes, MappedPageTable}; use x86_64::PhysAddr; /// Initialize a new MappedPageTable. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. Also, this function must be only called once /// to avoid aliasing `&mut` references (which is undefined behavior). pub unsafe fn init(physical_memory_offset: u64) -> impl MapperAllSizes { let level_4_table = active_level_4_table(physical_memory_offset); let phys_to_virt = move |frame: PhysFrame| -> *mut PageTable { let phys = frame.start_address().as_u64(); let virt = VirtAddr::new(phys + physical_memory_offset); virt.as_mut_ptr() }; MappedPageTable::new(level_4_table, phys_to_virt) } // make private unsafe fn active_level_4_table(physical_memory_offset: u64) -> &'static mut PageTable {…} 

Wir können nicht direkt MappedPageTablevon einer Funktion zurückkehren, da dies für einen Verschlusstyp üblich ist. Wir werden dieses Problem mit einem Syntaxkonstrukt umgehen impl Trait. Ein weiterer Vorteil ist, dass Sie den Kernel dann wechseln können, RecursivePageTableohne die Signatur der Funktion zu ändern.

Die Funktion MappedPageTable::newerwartet zwei Parameter: eine veränderbare Verknüpfung zur Seitentabelle der Ebene 4 und einen Abschluss phys_to_virt, der den physischen Frame in einen Seitentabellenzeiger konvertiert *mut PageTable. Für den ersten Parameter können wir die Funktion wiederverwenden active_level_4_table. Zum zweiten erstellen wir einen Abschluss, mit physical_memory_offsetdem die Konvertierung durchgeführt wird.

Wir machen es auch zu einer active_level_4_tableprivaten Funktion, da es von nun an nur noch von aufgerufen wird init.

So verwenden Sie die MethodeMapperAllSizes::translate_addrAnstelle unserer eigenen Funktion memory::translate_addrmüssen wir nur ein paar Zeilen ändern in kernel_main:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS // new: different imports use blog_os::memory; use x86_64::{structures::paging::MapperAllSizes, VirtAddr}; // new: initialize a mapper let mapper = unsafe { memory::init(boot_info.physical_memory_offset) }; let addresses = […]; // same as before for &address in &addresses { let virt = VirtAddr::new(address); // new: use the `mapper.translate_addr` method let phys = mapper.translate_addr(virt); println!("{:?} -> {:?}", virt, phys); } println!("It did not crash!"); blog_os::hlt_loop(); } 

Nach dem Start sehen wir die gleichen Übersetzungsergebnisse wie zuvor, aber jetzt funktionieren nur noch große Seiten:



Wie erwartet wird die virtuelle Adresse physical_memory_offsetin eine physische Adresse konvertiert 0x0. Durch die Verwendung der Übersetzungsfunktion für den Typ MappedPageTableentfällt die Notwendigkeit, die Unterstützung für große Seiten zu implementieren. Wir haben auch Zugriff auf andere Seitenfunktionen, wie map_towir sie im nächsten Abschnitt verwenden werden. Zu diesem Zeitpunkt benötigen wir die Funktion nicht mehr memory::translate_addr. Sie können sie löschen, wenn Sie möchten.

Erstellen Sie eine neue Zuordnung


Bisher haben wir uns nur Seitentabellen angesehen, aber nichts geändert. Erstellen wir eine neue Zuordnung für eine zuvor nicht angezeigte Seite.

Wir werden die Funktion map_toaus dem Merkmal verwenden Mapper, also werden wir zuerst diese Funktion betrachten. Die Dokumentation besagt, dass vier Argumente erforderlich sind: die Seite, die wir anzeigen möchten; Der Rahmen, dem die Seite zugeordnet werden soll. Satz von Flags zum Schreiben von Seitentabellen und Rahmenverteilern frame_allocator. Ein Frame-Allokator ist erforderlich, da für die Zuordnung dieser Seite möglicherweise zusätzliche Tabellen erstellt werden müssen, für die nicht verwendete Frames als Sicherungsspeicher erforderlich sind.

Funktion create_example_mapping


Der erste Schritt in unserer Implementierung besteht darin, eine neue Funktion zu erstellen create_example_mapping, die diese Seite dem 0xb8000physischen Frame des VGA-Textpuffers zuordnet. Wir wählen diesen Rahmen aus, weil es einfach ist zu überprüfen, ob die Anzeige korrekt erstellt wurde: Wir müssen nur auf die zuletzt angezeigte Seite schreiben und sehen, ob sie auf dem Bildschirm angezeigt wird.

Die Funktion create_example_mappingsieht folgendermaßen aus:

 // in src/memory.rs use x86_64::structures::paging::{Page, Size4KiB, Mapper, FrameAllocator}; /// Creates an example mapping for the given page to frame `0xb8000`. pub fn create_example_mapping( page: Page, mapper: &mut impl Mapper<Size4KiB>, frame_allocator: &mut impl FrameAllocator<Size4KiB>, ) { use x86_64::structures::paging::PageTableFlags as Flags; let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000)); let flags = Flags::PRESENT | Flags::WRITABLE; let map_to_result = unsafe { mapper.map_to(page, frame, flags, frame_allocator) }; map_to_result.expect("map_to failed").flush(); } 

Zusätzlich zu der Seite, die pageSie zuordnen möchten, erwartet die Funktion eine Instanz von mapperund frame_allocator. Der Typ mapperimplementiert das Merkmal Mapper<Size4KiB>, das die Methode bereitstellt map_to. Gemeinsame Parameter Size4KiBnotwendig , weil Merkmal Mapperist häufig für das Merkmal PageSize, das Arbeiten mit Standard - 4 KiB Seiten und mit enormen Seiten 2 und MiB 1 GiB. Wir möchten nur 4 KiB-Seiten erstellen, damit wir sie Mapper<Size4KiB>anstelle der Anforderung verwenden können MapperAllSizes.

Setzen Sie zum Vergleich das Flag PRESENT, da alle gültigen Einträge erforderlich sind, und das Flag WRITABLE, um die angezeigte Seite beschreibbar zu machen. Herausforderungmap_tounsicher: Sie können die Speichersicherheit mit ungültigen Argumenten verletzen, daher müssen Sie einen Block verwenden unsafe. Eine Liste aller möglichen Flags finden Sie im Abschnitt „Seitentabellenformat“ des vorherigen Artikels .

Die Funktion map_tokann fehlschlagen und kehrt daher zurück Result. Da dies nur ein Beispiel für Code ist, der nicht zuverlässig sein sollte, verwenden wir ihn einfach, um expectim Fehlerfall in Panik zu geraten. Bei Erfolg gibt die Funktion einen Typ zurück MapperFlush, mit dem die zuletzt angezeigte Seite mithilfe der Methode auf einfache Weise aus dem dynamischen Übersetzungspuffer (TLB) gelöscht werden kann flush. Da Resultdiese Art von Attribut verwendet [ #[must_use]] füreine Warnung ausgeben, wenn wir versehentlich vergessen, sie zu verwenden .

Fiktiv FrameAllocator


Um anzurufen create_example_mapping, müssen Sie zuerst erstellen FrameAllocator. Wie oben erwähnt, hängt die Komplexität beim Erstellen einer neuen Anzeige von der virtuellen Seite ab, die angezeigt werden soll. Im einfachsten Fall ist bereits eine Tabelle der Ebene 1 für die Seite vorhanden, und wir müssen nur einen Datensatz erstellen. Im schwierigsten Fall befindet sich die Seite in einem Speicherbereich, für den noch keine Ebene 3 erstellt wurde. Zunächst müssen Sie Seitentabellen der Ebenen 3, 2 und 1 erstellen.

Beginnen wir mit einem einfachen Fall und gehen davon aus, dass Sie keine neuen Seitentabellen erstellen müssen. Ein Rahmenverteiler, der immer zurückgibt, reicht dafür aus None. Wir erstellen eine solche EmptyFrameAllocatorAnzeigefunktion zum Testen:

 // in src/memory.rs /// A FrameAllocator that always returns `None`. pub struct EmptyFrameAllocator; impl FrameAllocator<Size4KiB> for EmptyFrameAllocator { fn allocate_frame(&mut self) -> Option<PhysFrame> { None } } 

Jetzt müssen Sie eine Seite finden, die angezeigt werden kann, ohne neue Seitentabellen zu erstellen. Der Loader wird in das erste Megabyte des virtuellen Adressraums geladen, sodass wir wissen, dass für diesen Bereich eine gültige Tabelle der Ebene 1 vorhanden ist. In unserem Beispiel können wir jede nicht verwendete Seite in diesem Speicherbereich auswählen, z. B. die Seite an der Adresse 0x1000.

Um die Funktion zu testen, zeigen wir zuerst die Seite 0x1000und dann den Inhalt des Speichers an:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory; use x86_64::{structures::paging::Page, VirtAddr}; let mut mapper = unsafe { memory::init(boot_info.physical_memory_offset) }; let mut frame_allocator = memory::EmptyFrameAllocator; // map a previously unmapped page let page = Page::containing_address(VirtAddr::new(0x1000)); memory::create_example_mapping(page, &mut mapper, &mut frame_allocator); // write the string `New!` to the screen through the new mapping let page_ptr: *mut u64 = page.start_address().as_mut_ptr(); unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e)}; println!("It did not crash!"); blog_os::hlt_loop(); } 

Zuerst erstellen wir eine Zuordnung für die Seite in 0x1000und rufen eine Funktion create_example_mappingmit einem veränderlichen Link zu Instanzen mapperund auf frame_allocator. Dadurch wird die Seite 0x1000dem VGA-Textpufferrahmen zugeordnet, sodass wir sehen sollten, was dort auf dem Bildschirm geschrieben steht.

Konvertieren Sie dann die Seite in einen Rohzeiger und schreiben Sie den Wert in den Versatz 400. Wir schreiben nicht an den Anfang der Seite, da die oberste Zeile des VGA-Puffers wie folgt direkt vom Bildschirm verschoben wird println. Schreiben Sie den Wert 0x_f021_f077_f065_f04e, der der Zeichenfolge "Neu!" Entspricht. auf einem weißen Hintergrund. Wie wir im Artikel „VGA-Textmodus“ erfahren haben , muss das Schreiben in den VGA-Puffer flüchtig sein, daher verwenden wir die Methode write_volatile.

Wenn wir den Code in QEMU ausführen, sehen wir das folgende Ergebnis:



Nach dem Schreiben auf die Seite 0x1000wird die Aufschrift "Neu!" .Daher haben wir erfolgreich eine neue Zuordnung in Seitentabellen erstellt.

Diese Sortierung funktionierte, da bereits eine Tabelle der Ebene 1 für die Sortierung vorhanden war 0x1000. Wenn wir versuchen, eine Seite zuzuordnen, für die noch keine Tabelle der Ebene 1 vorhanden ist, map_toschlägt die Funktion fehl, da versucht wird, Frames zuzuweisen EmptyFrameAllocator, um neue Tabellen zu erstellen. Wir sehen, dass dies passiert, wenn wir versuchen, die Seite anzuzeigen, 0xdeadbeaf000anstatt 0x1000:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] let page = Page::containing_address(VirtAddr::new(0xdeadbeaf000)); […] } 

Wenn dies gestartet wird, tritt eine Panik mit der folgenden Fehlermeldung auf:

 panicked at 'map_to failed: FrameAllocationFailed', /…/result.rs:999:5 

Um Seiten anzuzeigen, die noch keine Tabelle der Seitenebene 1 haben, müssen Sie die richtige erstellen FrameAllocator. Aber woher wissen Sie, welche Frames frei sind und wie viel physischer Speicher verfügbar ist?

Rahmenauswahl


Für neue Seitentabellen müssen Sie den richtigen Rahmenverteiler erstellen. Beginnen wir mit dem allgemeinen Skelett:

 // in src/memory.rs pub struct BootInfoFrameAllocator<I> where I: Iterator<Item = PhysFrame> { frames: I, } impl<I> FrameAllocator<Size4KiB> for BootInfoFrameAllocator<I> where I: Iterator<Item = PhysFrame> { fn allocate_frame(&mut self) -> Option<PhysFrame> { self.frames.next() } } 

Das Feld frameskann mit einem beliebigen Frame-Iterator initialisiert werden. Auf diese Weise können Sie Aufrufe einfach an die allocMethode delegieren Iterator::next.

Für die Initialisierung verwenden wir BootInfoFrameAllocatordie Speicherkarte, memory_mapdie der Bootloader als Teil der Struktur überträgt BootInfo. Wie im Abschnitt Startinformationen erläutert , wird die Speicherkarte von der BIOS / UEFI-Firmware bereitgestellt. Es kann nur zu Beginn des Startvorgangs angefordert werden, sodass der Bootloader die erforderlichen Funktionen bereits aufgerufen hat.

Eine Speicherkarte besteht aus einer Liste von Strukturen MemoryRegion, die die Startadresse, Länge und den Typ (z. B. nicht verwendet, reserviert usw.) jedes Speicherbereichs enthalten. Durch Erstellen eines Iterators, der Frames aus nicht verwendeten Bereichen erzeugt, können wir einen gültigen erstellen BootInfoFrameAllocator.

Die Initialisierung BootInfoFrameAllocatorerfolgt in einer neuen Funktion init_frame_allocator:

 // in src/memory.rs use bootloader::bootinfo::{MemoryMap, MemoryRegionType}; /// Create a FrameAllocator from the passed memory map pub fn init_frame_allocator( memory_map: &'static MemoryMap, ) -> BootInfoFrameAllocator<impl Iterator<Item = PhysFrame>> { // get usable regions from memory map let regions = memory_map .iter() .filter(|r| r.region_type == MemoryRegionType::Usable); // map each region to its address range let addr_ranges = regions.map(|r| r.range.start_addr()..r.range.end_addr()); // transform to an iterator of frame start addresses let frame_addresses = addr_ranges.flat_map(|r| r.step_by(4096)); // create `PhysFrame` types from the start addresses let frames = frame_addresses.map(|addr| { PhysFrame::containing_address(PhysAddr::new(addr)) }); BootInfoFrameAllocator { frames } } 

Diese Funktion verwendet einen Kombinator, um die anfängliche Zuordnung MemoryMapin einen Iterator der verwendeten physischen Frames zu konvertieren :

  • Zuerst rufen wir die Methode iterauf, um die Speicherkarte in einen Iterator umzuwandeln MemoryRegion. Dann verwenden wir die Methode filter, um reservierte oder unzugängliche Regionen zu überspringen. Der Loader aktualisiert die Speicherkarte für alle von ihm erstellten Zuordnungen, sodass die vom Kernel (Code, Daten oder Stapel) oder zum Speichern von Informationen über den Start verwendeten Frames bereits als InUseoder ähnlich markiert sind . Somit können wir sicher sein, dass Frames Usablenicht anderweitig verwendet werden .
  • map range Rust .
  • : into_iter , 4096- step_by . 4096 (= 4 ) — , . , . flat_map map , Iterator<Item = u64> Iterator<Item = Iterator<Item = u64>> .
  • PhysFrame , Iterator<Item = PhysFrame> . BootInfoFrameAllocator .

Jetzt können wir unsere Funktion ändern kernel_main, um BootInfoFrameAllocatorstattdessen eine Instanz zu übergeben EmptyFrameAllocator:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] let mut frame_allocator = memory::init_frame_allocator(&boot_info.memory_map); […] } 

Diesmal war die Adresszuordnung erfolgreich und wir sehen wieder das schwarz-weiße "Neu!" .Hinter den Kulissen erstellt die Methode map_tofehlende Seitentabellen wie folgt:

  • Wählen Sie einen nicht verwendeten Frame aus dem übertragenen aus frame_allocator.
  • Null Frame zum Erstellen einer neuen leeren Seitentabelle.
  • Ordnen Sie diesem Frame einen übergeordneten Tabelleneintrag zu.
  • Gehen Sie zur nächsten Ebene der Tabelle.

Obwohl unsere Funktion create_example_mappingnur Beispielcode ist, können wir jetzt neue Zuordnungen für beliebige Seiten erstellen. Dies ist erforderlich, um Speicher zuzuweisen und Multithreading in zukünftigen Artikeln zu implementieren.

Zusammenfassung


In diesem Artikel haben wir verschiedene Methoden für den Zugriff auf die physischen Frames von Seitentabellen kennengelernt, darunter Identitätszuordnung, Zuordnung des gesamten physischen Speichers, temporäre Zuordnung und rekursive Seitentabellen. Wir haben uns dafür entschieden, den vollständigen physischen Speicher als einfache und leistungsstarke Methode anzuzeigen.

Wir können den physischen Speicher des Kernels nicht ohne Zugriff auf die Seitentabelle zuordnen, daher ist Bootloader-Unterstützung erforderlich. Das Rack bootloadererstellt die erforderlichen Zuordnungen durch zusätzliche Ladefunktionen. Es übergibt die erforderlichen Informationen als Argument &BootInfoan die Einstiegspunktfunktion an den Kernel .

Für unsere Implementierung haben wir zuerst die Seitentabellen manuell durchgesehen, eine Übersetzungsfunktion erstellt und dann den MappedPageTableKistentyp verwendetx86_64. Wir haben auch gelernt, wie man neue Zuordnungen in der Seitentabelle erstellt und wie man sie FrameAllocatorauf einer vom Bootloader übertragenen Speicherkarte erstellt.

Was weiter?


Im nächsten Artikel erstellen wir einen Heap-Speicherbereich für unseren Kernel, in dem wir Speicher zuweisen und verschiedene Arten von Sammlungen verwenden können .

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


All Articles