Wir schreiben ein Betriebssystem auf Rust. Seitenspeicher implementieren (neu)

Ursprünglicher Autor: Philipp Oppermann
  • Übersetzung
In diesem Artikel werden wir herausfinden, wie die Seitenspeicherunterstützung in unserem Kern implementiert wird. Zunächst werden verschiedene Methoden untersucht, damit die Frames der physischen Seitentabelle für den Kernel verfügbar werden, und deren Vor- und Nachteile erörtert. Anschließend implementieren wir die Adressumsetzungsfunktion 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 sind in diesem Thread .

Ein weiterer Artikel zum Thema Paging?
Wenn Sie diesem Zyklus folgen, haben Sie Ende Januar den Artikel „Page Memory: Advanced Level“ gesehen . Aber sie haben mich kritisiertfür rekursive Seitentabellen. Aus diesem Grund habe ich beschlossen, den Artikel mit einem anderen Ansatz für den Zugriff auf Frames umzuschreiben.


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, aber als veraltet markieren und ihn nicht aktualisieren.

Ich hoffe, Sie genießen die neue Option!


Inhalt



Einleitung


In einem früheren Artikel haben wir die Prinzipien des Paging-Speichers und die Funktionsweise von Seitentabellen mit vier Ebenen kennengelernt x86_64. Wir haben auch festgestellt, dass der Loader die Seitentabellenhierarchie für unseren Kernel bereits eingerichtet hat, sodass der Kernel auf virtuellen Adressen ausgeführt wird. Dies erhöht die Sicherheit, da ein unbefugter Zugriff auf den Speicher einen Seitenfehler verursacht, anstatt den physischen Speicher zufällig zu ändern.

Der Artikel konnte nicht auf Seitentabellen aus unserem Kernel 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 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 neue Zuordnungen in Seitentabellen erstellt werden und wie nicht verwendete Speicherrahmen zum Erstellen neuer Tabellen gefunden werden.

Abhängigkeitsaktualisierungen


Dieser Artikel erfordert eine Registrierung in Abhängigkeiten von bootloaderVersion 0.4.0 oder höher und x86_64Version 0.5.2 oder höher. Sie können die Abhängigkeiten aktualisieren in Cargo.toml:

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

Informationen zu Ä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. Sehen Sie sich die vierstufige Tabellenhierarchie aus dem vorherigen Artikel noch einmal an, um das Problem zu verstehen:



Wichtig ist, dass jeder Seiteneintrag die physikalische Adresse der nächsten Tabelle speichert . Dadurch wird die Übersetzung dieser Adressen vermieden, was die Leistung verringert und leicht zu Endlosschleifen führt.

Das Problem ist, dass wir nicht direkt vom Kernel aus auf physische Adressen zugreifen können, da dies auch für virtuelle Adressen funktioniert. Wenn wir beispielsweise auf die Adresse zugreifen 4 KiB, erhalten wir Zugriff auf die virtuelle Adresse 4 KiBund nicht auf die physische Adresse, in der die Seitentabelle der 4. Ebene gespeichert ist. Wenn wir auf die physische Adresse zugreifen möchten, 4 KiBmüssen wir eine Art 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 Darstellung 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 Register CR3 leicht auf die Seitentabellen aller Ebenen zugreifen können.

Dieser Ansatz überfrachtet jedoch den virtuellen Adressraum und macht es schwierig, große zusammenhängende Bereiche des freien Speichers zu finden. Angenommen, Sie 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 Region beginnen 28  KiB, da sie auf eine bereits belegte Seite stößt 1004  KiB. Deshalb müssen wir weiter suchen, bis wir ein geeignetes großes Fragment finden, zum Beispiel mit 1008  KiB. Es gibt das gleiche Fragmentierungsproblem wie im segmentierten Speicher.

Darüber hinaus ist die Erstellung neuer Seitentabellen sehr viel komplizierter, da physische Frames gesucht werden müssen, deren entsprechende Seiten noch nicht verwendet werden. Zum Beispiel haben wir für unsere Datei einen Bereich von 1000 KB virtuellem Speicher reserviert , beginnend mit der Adresse 1008  KiB. Jetzt können wir keinen Frame mit einer physikalischen Adresse zwischen 1000  KiBund mehr verwenden 2008  KiB, da er nicht identisch angezeigt werden kann.

Feste Versatzkarte


Um den virtuellen Adressraum nicht zu überladen, können Sie die Seitentabellen in einem separaten Speicherbereich anzeigen . Daher ordnen wir Frames mit einem festen Versatz im virtuellen Adressraum zu, anstatt sie identisch abzubilden. Beispielsweise kann der Versatz 10 TiB



betragen : Durch Zuweisen dieses Bereichs des virtuellen Speichers nur zum Anzeigen von Seitentabellen 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. Dies x86_64ist kein Problem, da der 48-Bit-Adressraum 256 TiB beträgt.

Dieser Ansatz hat jedoch den Nachteil, dass Sie beim Erstellen jeder Seitentabelle ein neues Mapping 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 die Seitentabellenrahmen anzeigen : Mit



diesem Ansatz kann der Kernel auf beliebigen physischen Speicher zugreifen, einschließlich der Seitentabellenrahmen anderer Adressräume. Ein Bereich des virtuellen Speichers ist in der gleichen Größe wie zuvor reserviert, es sind jedoch keine unvergleichlichen Seiten mehr 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 auf Geräten mit wenig RAM ein Problem sein kann.

Auf x86_64 können wir jedoch sehr große Seiten zum Anzeigen verwenden2 MiB anstelle der Standardgröße 4 KiB. Zur Anzeige von 32 GB physischem Speicher 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 dynamischen Übersetzungspuffer (TLB) verwenden.

Temporäre Anzeige


Auf 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 MB des virtuellen Adressraums. Dies ist möglich, weil der Zugriff aus dem CR3-Register über Null-Einträge in den Tabellen der Ebenen 4, 3 und 2 erfolgt. Der Datensatz mit dem Index 8übersetzt die virtuelle Seite an der Adresse 32 KiBin den physischen Frame an der Adresse 32 KiBund identifiziert so die Tabelle der Ebene 1 selbst. Die Abbildung zeigt diese horizontale Pfeil.

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:

  • Abgleichen eines Nulldatensatzes einer Tabelle der Ebene 1 mit einem Frame an einer Adresse 24 KiB. Dadurch wird eine temporäre Zuordnung der virtuellen Seite an der Adresse 0 KiBzum physischen Frame der Seitentabelle der Ebene 2 erstellt, die durch den gepunkteten Pfeil gekennzeichnet ist.
  • Ordnen Sie den 9. Datensatz einer Tabelle der Ebene 1 einem Rahmen bei zu 4 KiB. Dadurch wird eine temporäre Zuordnung der virtuellen Seite an der Adresse 36 KiBzum physischen Frame der Tabelle auf Seitenebene 4 erstellt, die durch den gepunkteten Pfeil gekennzeichnet ist.

Jetzt kann der Kernel auf die Tabelle der Ebene 2 zugreifen, indem er auf die Seite schreibt, die an der Adresse beginnt, 0 KiBund auf die Tabelle der Ebene 4, indem er auf die Seite schreibt, die an der Adresse beginnt 33 KiB.

Der Zugriff auf einen beliebigen Frame 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 Frame der Seitentabelle zu, auf die zugegriffen werden soll.
  • Greifen Sie über die dem Eintrag zugeordnete virtuelle Seite auf diesen Frame zu.
  • Setzen Sie den Datensatz auf "Nicht verwendet" zurück, um die temporäre Zuordnung zu entfernen.

Bei diesem Ansatz bleibt der virtuelle Adressraum sauber, da ständig die gleichen 512 virtuellen Seiten verwendet werden. Der Nachteil ist etwas umständlich, zumal für einen neuen Vergleich möglicherweise mehrere Tabellenebenen geändert werden müssen, dh der beschriebene Vorgang muss mehrmals wiederholt werden.

Rekursive Seitentabellen


Ein weiterer interessanter Ansatz, der überhaupt keine zusätzlichen Seitentabellen erfordert, ist das rekursive Matching .

Die Idee ist, einige Datensätze aus der Tabelle der vierten Ebene in die Tabelle selbst zu übersetzen. Daher reservieren wir tatsächlich einen Teil des virtuellen Adressraums und ordnen alle aktuellen und zukünftigen Tabellenrahmen diesem Raum 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 einem Index 511in der Tabelle der Ebene 4, der dem physischen Frame 4 KiBin dieser Tabelle selbst zugeordnet ist.

Wenn die CPU diesen Datensatz aufruft, 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 Datensatz in der Tabelle der Ebene 4 auf eine Tabelle der Ebene 3 verweist, sodass die Tabelle der Ebene 4 jetzt als Tabelle der Ebene 3 behandelt wird.

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 einmal dem rekursiven Datensatz folgen und dann zur Tabelle der Ebene 3 gehen, denkt der Prozessor, dass die Tabelle der Ebene 3 eine Tabelle der Ebene 2 ist. Wenn er fortfährt, 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 Seitentabelle der Ebene 1 lesen und schreiben können, da der Prozessor denkt, dass es sich um einen abgebildeten Frame handelt. Die folgende Abbildung zeigt die fünf Schritte einer solchen Übersetzung:



In ähnlicher Weise können wir einem rekursiven Eintrag zweimal folgen, bevor wir mit der Konvertierung beginnen, um die Anzahl der abgeschlossenen Ebenen auf zwei zu reduzieren:



Gehen wir diesen Vorgang Schritt für Schritt durch. 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 dem rekursiven Datensatz erneut und denkt, dass sie die Ebene 2 erreicht hat und landet in einer Tabelle der Ebene 3, denkt jedoch, dass es sich bereits um eine Tabelle der Ebene 1 handelt. Schließlich glaubt der Prozessor am nächsten Eintrittspunkt in der Tabelle der Ebene 2, dass er auf einen physischen Speicherrahmen zugegriffen hat. Auf diese Weise können wir eine Tabelle der Ebene 2 lesen und beschreiben.

Es wird auch auf die Tabellen der Ebenen 3 und 4 zugegriffen.Um auf die Tabelle der Ebene 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 Ebene 3, die die CPU als abgebildeten Rahmen betrachtet. Um auf die Tabelle der Ebene 4 selbst zuzugreifen, folgen wir einfach viermal dem rekursiven Datensatz, bis der Prozessor die Tabelle der Ebene 4 selbst als zugeordneten Frame verarbeitet (in der folgenden Abbildung blau dargestellt).



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

Adressberechnung


So können wir auf Tabellen aller Ebenen zugreifen, indem wir einem rekursiven Datensatz ein oder mehrere Male 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 durchgehen. Dazu verschieben wir alle Adressblöcke einen Block nach rechts und setzen den Index des rekursiven Datensatzes auf die Stelle des Ausgangsindexes 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 schieben einfach schon drei Adressblöcke nach rechts.



Um auf die Tabelle der Ebene 4 zuzugreifen, verschieben wir schließlich 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 ihren Index mit 8 multiplizieren, der Größe des Seitentabelleneintrags.

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

Virtuelle Adresse für Adressstruktur ( oktal )
Seite 0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE
Eintrag in der Tabelle der Ebene 10o_SSSSSS_RRR_AAA_BBB_CCC_DDDD
Eintrag in eine Level 2 Tabelle0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC
Eintrag in eine Tabelle der Stufe 30o_SSSSSS_RRR_RRR_RRR_AAA_BBBB
Eintrag in der Tabelle der Ebene 40o_SSSSSS_RRR_RRR_RRR_RRR_AAAA

Hier АААist der Index der Ebene 4, ВВВ- Ebene 3, ССС- Ebene 2 und DDD- der Index der Ebene 1 für das angezeigte Bild, EEEE- sein Versatz. RRR- Index des rekursiven Datensatzes. Ein Index (drei Stellen) wird durch Multiplizieren mit 8 (der Größe des Seitentabelleneintrags) in einen Versatz (vier Stellen) umgewandelt. Mit diesem Versatz zeigt die resultierende Adresse direkt auf den entsprechenden Seitentabelleneintrag.

SSSS- Vorzeichenerweiterungsbits, das heißt, 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 früheren Artikel erörtert haben .

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

Rostcode


Sie können solche Adressen in Rust-Code mit bitweisen 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 geht von einer rekursiven Zuordnung des letzten Datensatzes der Ebene 4 aus, wobei der Index 0o777(511) rekursiv abgeglichen wird. 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 RecursivePageTableKistentyp verwenden x86_64, der sichere Abstraktionen für verschiedene Tabellenoperationen bietet. Der folgende Code zeigt beispielsweise, wie eine virtuelle Adresse in ihre entsprechende physikalische 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 das Fehlen level_4_table_addrwie im ersten Codebeispiel berechnet.



Die rekursive Zuordnung ist eine interessante Methode, die zeigt, wie leistungsfähig die Zuordnung über eine einzelne Tabelle sein kann. Es ist relativ einfach zu implementieren und erfordert nur minimale Einstellungen (nur einen rekursiven Eintrag). Daher ist dies eine gute Wahl für die ersten Experimente.

Es hat aber 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 gibt einfach nur Zugriff auf den aktuell aktiven Adressraum. Der Zugriff auf andere Adressräume ist weiterhin durch Ändern des rekursiven Eintrags möglich, für den Wechsel ist jedoch eine vorübergehende Übereinstimmung erforderlich. Wie das geht, haben wir in einem früheren (veralteten) Artikel beschrieben.
  • Dies hängt stark vom x86-Seitentabellenformat 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. Beispielsweise, um 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 die Seitentabellen, sodass er alle Anzeigen erstellen kann, die wir benötigen. In der aktuellen Implementierung unterstützt die Kiste bootloaderdie beiden oben genannten Ansätze mithilfe von Frachtfunktionen :

  • Die Funktion map_physical_memoryzeigt den gesamten physischen Speicher im virtuellen Adressraum an. Auf diese Weise erhält der Kernel Zugriff auf den gesamten physischen Speicher und kann den Ansatz mit der Anzeige des vollständigen physischen Speichers anwenden .
  • Mit dieser Funktion zeigt der recursive_page_tableLoader rekursiv einen Datensatz der Seitentabelle 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 leistungsstärkerer Ansatz ist (der auch Zugriff auf andere Frames ermöglicht, nicht nur auf Seitentabellen). Um Unterstützung vom Bootloader zu erhalten, fügen Sie die Funktion 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 eine Reihe von virtuellen Adressen an den Kernel zu übergeben, übergibt der Bootloader die Struktur der Bootinformationen .

Boot-Informationen


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

  • Das Feld memory_mapbietet eine Übersicht ü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 diese 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. Addiert man diesen Offset zur physikalischen Adresse, erhält man die entsprechende virtuelle Adresse. Dies gibt dem Kernel Zugriff auf einen beliebigen physischen Speicher.

Der Loader übergibt die Struktur BootInfoals Argument &'static BootInfoan die Funktion an den Kernel _start. Füge 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.

Einstiegspunkt-Makro


Da die Funktion _startextern vom Bootloader aufgerufen wird, wird die Signatur der Funktion nicht geprüft. Dies bedeutet, dass wir es beliebige Argumente akzeptieren lassen können, ohne dass Kompilierungsfehler auftreten. Dies kann jedoch zum Absturz führen oder ein undefiniertes Laufzeitverhalten verursachen.

Um sicherzustellen, dass die Einstiegspunktfunktion immer die richtige Signatur aufweist, enthält die Kiste bootloaderein Makro entry_point. Wir schreiben unsere Funktion mit diesem Makro um:

// 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 verwenden no_mangle, da das Makro für uns den tatsächlichen Einstiegspunkt der unteren Ebene definiert _start. Die Funktion ist kernel_mainjetzt zu einer ganz 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 den 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 Seitentabellen zu betrachten, auf denen der Kernel arbeitet, konnten jedoch nicht auf den physischen Frame zugreifen, auf den das Register verweist CR3. Jetzt können wir an dieser Stelle 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 Frame der aktiven Tabelle der 4. Ebene aus dem Register CR3. Dann nehmen wir die physikalische Startadresse und wandeln sie durch Hinzufügen in eine virtuelle Adresse um physical_memory_offset. Konvertieren Sie abschließend die Adresse *mut PageTablemit der Methode in einen as_mut_ptrunformatierten Zeiger , und erstellen Sie dann einen unsicheren Link daraus &mut PageTable. Wir erstellen &mutstattdessen den Link &, da wir diese Seitentabellen später in diesem Artikel ändern werden.

Hier muss kein unsicherer Block eingefügt werden, da Rust den gesamten Körper unsafe fnals einen großen, unsicheren Block ansieht . Dies erhöht die Risiken, da es möglich ist, versehentlich eine unsichere Operation in die vorherigen Zeilen 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();
}

Als physical_memory_offsetperedaom entsprechende Feldstruktur BootInfo. Anschließend verwenden wir die Funktion iter, um die Seitentabelleneinträge zu durchlaufen, und den 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, wird folgendes Ergebnis



angezeigt : Es werden mehrere nicht leere Datensätze angezeigt, die verschiedenen Tabellen der dritten Ebene zugeordnet sind. Es werden so viele Speicherbereiche verwendet, weil separate Bereiche für den Kernelcode, den Kernelstapel, die Übersetzung des physischen Speichers und die Startinformationen benötigt werden.

Um die Seitentabellen zu durchlaufen 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);
        }
    }
}

Um die Tabellen der zweiten und ersten Ebene anzuzeigen, wiederholen wir diesen Vorgang für Datensätze der dritten und zweiten Ebene. Wie Sie sich vorstellen können, nimmt die Codemenge sehr schnell zu, sodass wir nicht die vollständige Auflistung veröffentlichen.

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

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
{
    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 wird jede Operation erneut explizit angegeben 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
{
    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_tableerneut zu verwenden, 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 damit alle Tabellen durchlaufen können for. Außerhalb der Schleife erinnern wir uns an den zuletzt besuchten Frame, um später die physikalische Adresse zu berechnen. frameEs gibt die Rahmen - Seitentabelle während der Iteration, und auf einem Vergleich des Rahmens nach der letzten Iteration, t. H., Nach dem Durchgang der Aufzeichnungsschicht 1

, wir den Zyklus erneut verwenden,physical_memory_offsetum einen Frame in einen Seitentabellenlink umzuwandeln. Dann lesen wir den Datensatz der aktuellen Seitentabelle und verwenden die Funktion PageTableEntry::frame, um den passenden Frame abzurufen. Wenn der Datensatz keinem Frame zugeordnet ist, kehren Sie zurück None. Wenn der Datensatz eine riesige 2-MiB- oder 1-GiB-Seite anzeigt, kommt es bisher zu einer Panik.

Lassen Sie uns die Übersetzungsfunktion an folgenden Adressen überprüfen:

// 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 physikalische Adressen konvertiert. Dies hängt davon ab, 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 bei der Übersetzung große Seiten für die Effizienz verwendet werden. Eine zukünftige Version des Loaders wendet möglicherweise dieselbe Optimierung für die Kernel- und Stapelseiten an.

MappedPageTable verwenden


Die Übersetzung von virtuellen Adressen in physische Adressen ist eine typische Aufgabe des Kernels des Betriebssystems, daher x86_64bietet die Kiste eine Abstraktion dafür. Es werden bereits große Seiten und verschiedene andere Funktionen unterstützt translate_addr. Daher verwenden wir es, 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 Frame der gleichen 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 werden Funktionen bereitgestellt, die mit Seiten unterschiedlicher Größe (einschließlich translate_addroder allgemein) arbeiten translate.

Merkmale definieren nur die Schnittstelle, stellen jedoch keine Implementierung bereit. Jetzt x86_64bietet der Baugruppenträger 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 Abschlusstyp üblich ist. Wir werden dieses Problem mit einem Syntaxkonstrukt umgehen impl Trait. Ein zusätzlicher Vorteil ist, dass Sie dann den Kernel RecursivePageTablewechseln können, ohne die Signatur der Funktion zu ändern.

Die Funktion MappedPageTable::newerwartet zwei Parameter: eine veränderbare Verknüpfung mit der 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. Für das zweite erstellen wir einen Abschluss, mit physical_memory_offsetdem die Transformation 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 auch nur noch riesige Seiten:



Wie erwartet wird die virtuelle Adresse physical_memory_offsetin eine physikalische Adresse umgewandelt 0x0. Durch die Verwendung der Übersetzungsfunktion für den Typ MappedPageTablewird die Implementierung der Unterstützung für große Seiten überflüssig. 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. In der Dokumentation wird angegeben, dass vier Argumente erforderlich sind: die Seite, die angezeigt werden soll; Der Rahmen, dem die Seite zugeordnet werden soll. eine Reihe von Flags zum Schreiben einer Seitentabelle und eines Frame-Verteilers frame_allocator. Ein Frame-Allokator ist erforderlich, da für die Zuordnung dieser Seite möglicherweise zusätzliche Tabellen erstellt werden müssen, die nicht verwendete Frames als Sicherungsspeicher benötigen.

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,
    frame_allocator: &mut impl FrameAllocator,
) {
    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 zuzuordnenden Seite pageerwartet die Funktion eine Instanz von mapperund frame_allocator. Der Typ mapperimplementiert das Merkmal , das die Methode bereitstellt . Gemeinsame Parameter notwendig , weil Merkmal ist häufig für das Merkmal , 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 anstelle der Anforderung verwenden können . Setzen Sie zum Vergleich das Flag , da es für alle gültigen Einträge erforderlich ist, und das Flag , um die angezeigte Seite beschreibbar zu machen. HerausforderungMappermap_toSize4KiBMapperPageSizeMapperMapperAllSizes

PRESENTWRITABLEmap_tounsicher: Sie können die Speichersicherheit durch ungültige Argumente 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 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. So verwendet Resultdieser Typ das Attribut [ #[must_use]], um eine Warnung auszugeben, 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 der Erstellung einer neuen Anzeige von der virtuellen Seite ab, die wir anzeigen möchten. Im einfachsten Fall ist bereits eine Tabelle der Ebene 1 für die Seite vorhanden, und es muss nur ein Datensatz erstellt werden. Im schwierigsten Fall befindet sich die Seite in dem Speicherbereich, für den noch keine Ebene 3 erstellt wurde. Daher müssen Sie zunächst 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. Dafür reicht ein immer wiederkehrender Rahmenverteiler aus None. Wir erstellen eine solche EmptyFrameAllocatorAnzeigefunktion zum Testen:

// in src/memory.rs
/// A FrameAllocator that always returns `None`.
pub struct EmptyFrameAllocator;
impl FrameAllocator for EmptyFrameAllocator {
    fn allocate_frame(&mut self) -> Option {
        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 diese Region eine gültige Tabelle der Ebene 1 vorhanden ist. In unserem Beispiel können wir jede nicht verwendete Seite in diesem Speicherbereich auswählen, beispielsweise 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();
}

Zunächst erstellen wir ein Mapping für die Seite in 0x1000und rufen eine Funktion create_example_mappingmit einer veränderlichen Verknüpfung zu Instanzen mapperund auf frame_allocator. Dadurch wird die Seite 0x1000dem VGA-Textpufferrahmen zugeordnet, sodass wir sehen sollten, was dort auf dem Bildschirm geschrieben ist.

Konvertieren Sie dann die Seite in einen rohen Zeiger und schreiben Sie den Wert in den Versatz 400. Wir schreiben nicht an den oberen Rand der Seite, da die obere 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 einen weißen Hintergrund. Wie wir in dem Artikel gelernt , „Textmodus VGA» , muss die Aufnahme in VGA - Puffer flüchtig sein, so die Methode verwenden write_volatile.

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



Nach dem Schreiben auf die Seite 0x1000wurde die Aufschrift "Neu!" Auf dem Bildschirm angezeigt . Daher haben wir erfolgreich ein neues Mapping in Seitentabellen erstellt.

Diese Sortierung hat funktioniert, weil es bereits eine Tabelle der Ebene 1 für die Sortierung gab 0x1000. Wenn wir versuchen , Seite zu vergleichen , die noch nicht das Niveau der Tabelle 1 nicht vorhanden ist , die Funktion map_tofehlschlägt, wie es versucht , Frames zuzuteilen von EmptyFrameAllocatorneuen 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 Frame-Verteiler erstellen. Beginnen wir mit dem allgemeinen Skelett:

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

Поле frames может быть инициализировано произвольным итератором кадров. Это позволяет просто делегировать вызовы alloc методу Iterator::next.

Для инициализации BootInfoFrameAllocator используем карту памяти memory_map, которую передаёт загрузчик как часть структуры BootInfo. Как объяснилось в разделе «Загрузочная информация», карта памяти предоставляется прошивкой BIOS/UEFI. Её можно запросить только в самом начале процесса загрузки, поэтому загрузчик уже вызвал нужные функции.

Карта памяти состоит из списка структур MemoryRegion, которые содержат начальный адрес, длину и тип (например, неиспользуемый, зарезервированный и т. д.) каждой области памяти. Создав итератор, который выдаёт кадры из неиспользуемых областей, мы можем создать валидный BootInfoFrameAllocator.

Инициализация BootInfoFrameAllocator происходит в новой функции 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> {
    // 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 }
}

Эта функция использует комбинатор для преобразования начальной карты MemoryMap в итератор используемых физических фреймов:

  • Во-первых, вызываем метод iter для преобразования карты памяти в итератор MemoryRegion. Затем используем метод filter для пропуска зарезервированных или недоступных регионов. Загрузчик обновляет карту памяти для всех сопоставлений, которые создаёт, поэтому фреймы, используемые ядром (код, данные или стек) или для хранения информации о загрузке, уже помечены как InUse или аналогично. Таким образом, мы можем быть уверены, что фреймы Usable не используются где-то ещё.
  • На втором этапе запускаем комбинатор map и синтаксисическую конструкцию range из Rust для преобразования итератора областей памяти в итератор диапазонов адресов.
  • Третий шаг самый сложный: преобразуем каждый диапазон в итератор с помощью метода into_iter, а затем выбираем каждый 4096-й адрес с помощью step_by. Поскольку 4096 байт (= 4 КиБ) — это размер страницы, мы получаем начальный адрес каждого фрейма. Страница загрузчика выравнивает все области памяти, так что нам не нужен код выравнивания или округления. Используя flat_map вместо map, мы получаем Iterator вместо Iterator>.
  • На последнем шаге преобразуем начальные адреса в типы PhysFrame, чтобы построить требуемый Iterator. Затем применяем этот итератор для создания и возврата нового BootInfoFrameAllocator.

Теперь можно изменить нашу функцию kernel_main, чтобы передать экземпляр BootInfoFrameAllocator вместо 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);
    […]
}

На этот раз сопоставление адресов прошло успешно и мы снова видим на экране чёрно-белую надпись “New!”. За кулисами метод map_to создаёт отсутствующие таблицы страниц следующим образом:

  • Выделить неиспользуемый фрейм из переданного frame_allocator.
  • Обнулить фрейм для создания новой пустой таблицы страниц.
  • Сопоставить запись таблицы более высокого уровня с этим фреймом.
  • Перейти к следующему уровню таблицы.

Хотя наша функция create_example_mapping — всего лишь пример кода, теперь мы можем создавать новые сопоставления для произвольных страниц. Это будет необходимо для выделения памяти и реализации многопоточности в будущих статьях.

Резюме


В этой статье мы узнали о различных методах доступа к физическим фреймам таблиц страниц, включая тождественное отображение, отображение полной физической памяти, временное отображение и рекурсивные таблицы страниц. Мы выбрали отображение полной физической памяти как простой и мощный метод.

Мы не можем сопоставить физическую память из ядра без доступа к таблице страниц, поэтому нужна поддержка загрузчика. Крейт bootloader создаёт необходимые сопоставления через дополнительные функции cargo. Он передаёт необходимую информацию ядру как аргумент &BootInfo в функции точки входа.

Для нашей реализации мы сначала вручную прошли через таблицы страниц, сделав функцию трансляции, а затем использовали тип MappedPageTable крейта x86_64. Wir haben auch gelernt, wie neue Zuordnungen in der Seitentabelle erstellt und FrameAllocatorauf einer vom Bootloader übertragenen Speicherkarte erstellt werden.

Was weiter?


Im nächsten Artikel werden wir einen Heap-Speicherbereich für unseren Kernel erstellen, der es uns ermöglicht, Speicher zuzuweisen und verschiedene Arten von Sammlungen zu verwenden .

Jetzt auch beliebt: