In diesem Artikel wird erläutert, wie der Kernel des Betriebssystems auf physische Speicherrahmen zugreifen kann. Wir werden die Funktion zum Konvertieren virtueller Adressen in physische Adressen untersuchen. Wir werden auch herausfinden, wie Sie neue Zuordnungen in Seitentabellen erstellen.
Dieser Blog ist auf
GitHub veröffentlicht . Wenn Sie Fragen oder Probleme haben, öffnen Sie dort das entsprechende Ticket. Alle Quellen für den Artikel finden Sie
hier .
Einführung
Im
letzten Artikel haben wir die Prinzipien des Paging-Speichers und die Funktionsweise der vierstufigen Seitentabellen unter x86_64 kennengelernt. 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 verbessert die Sicherheit, aber das Problem tritt auf: Wie kann auf reale physische Adressen zugegriffen werden, die in Seitentabelleneinträgen oder im
CR3
gespeichert sind?
Im ersten Abschnitt des Artikels werden wir das Problem und verschiedene Lösungsansätze diskutieren. Anschließend implementieren wir eine Funktion, die sich durch die Hierarchie der Seitentabellen schleicht, um virtuelle Adressen in physische Adressen umzuwandeln. Schließlich erfahren Sie, wie Sie neue Zuordnungen in Seitentabellen erstellen und nicht verwendete Speicherrahmen zum Erstellen neuer Tabellen finden.
Abhängigkeitsaktualisierungen
Zum Arbeiten benötigen Sie
x86_64
Version 0.4.0 oder höher. Aktualisieren Sie die Abhängigkeit in unserer
Cargo.toml
:
[dependencies] x86_64 = "0.4.0" # or later
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.
1. 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.
2. Eine andere Möglichkeit besteht darin
, Seitentabellen nur vorübergehend zu
senden, 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 hat der Kernel den Nulldatensatz einer Tabelle der Ebene 1 mit einem Frame bei
24 KiB
abgeglichen. Dies erzeugte eine temporäre Zuordnung der virtuellen Seite bei
0 KiB
zu dem physischen Rahmen der Seitentabelle der Seite 2, der durch den gepunkteten Pfeil angezeigt wird. Jetzt kann der Kernel auf die Tabelle der Ebene 2 zugreifen, indem er auf eine Seite schreibt, die bei
0 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.
3. Obwohl beide oben genannten Ansätze funktionieren, gibt es eine dritte Methode:
rekursive Seitentabellen . Es kombiniert die Vorteile beider Ansätze: Es vergleicht ständig alle Frames der Seitentabellen, ohne temporäre Vergleiche zu erfordern, und hält auch benachbarte Seiten nebeneinander, um eine Fragmentierung des virtuellen Adressraums zu vermeiden. Dies ist die Methode, die wir verwenden werden.
Rekursive Seitentabellen
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 die 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 Ebene 3 zuzugreifen, folgen wir dreimal einem rekursiven Datensatz: 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ür | Adressstruktur ( oktal ) |
---|
Seite | 0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE |
Eintrag in Level 1 Tabelle | 0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD |
Eintrag in eine Level 2 Tabelle | 0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC |
Eintrag in eine Level 3 Tabelle | 0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB |
Eintrag in Level 4 Tabelle | 0o_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.
Implementierung
Nach all dieser Theorie können wir endlich mit der Implementierung fortfahren. Praktischerweise hat der Loader nicht nur Seitentabellen generiert, sondern auch eine rekursive Anzeige im letzten Datensatz der Level 4-Tabelle. Der Loader hat dies getan, da sonst ein Henne-Ei-Problem auftreten würde: Wir müssen auf die Level 4-Tabelle zugreifen, um eine rekursive Karte zu erstellen aber wir können nicht ohne Anzeige darauf zugreifen.
Wir haben diese rekursive Zuordnung bereits am Ende des vorherigen Artikels verwendet, um über die fest codierte Adresse
0xffff_ffff_ffff_f000
auf die Tabelle der Ebene 4
0xffff_ffff_ffff_f000
. Wenn wir diese Adresse in Oktal konvertieren und sie mit der obigen Tabelle vergleichen, werden wir sehen, dass sie genau der Struktur des Datensatzes in der Tabelle der Ebene 4 mit
RRR
=
0o777
,
AAAA
=
0
und den Erweiterungsbits des Vorzeichens
1
:
Struktur: 0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA
Adresse: 0o_177777_777_777_777_777_0000
Dank der Kenntnis rekursiver Tabellen können wir jetzt virtuelle Adressen erstellen, um auf alle aktiven Tabellen zuzugreifen. Und machen Sie die Broadcast-Funktion.
Adressübersetzung
Erstellen Sie als ersten Schritt eine Funktion, die eine virtuelle Adresse in eine physische Adresse konvertiert und dabei die Hierarchie der Seitentabellen durchläuft:
Zunächst führen wir Variablen für den rekursiven Index (511 =
0o777
) und die Vorzeichenerweiterungsbits (jeweils 1) ein. Dann berechnen wir die Indizes der Seitentabellen und den Versatz durch bitweise Operationen, wie in der Abbildung gezeigt:
Der nächste Schritt besteht darin, die virtuellen Adressen der vier Seitentabellen zu berechnen, wie im vorherigen Abschnitt beschrieben. Als Nächstes konvertieren wir in der Funktion jede dieser Adressen in
PageTable
Links. Dies sind unsichere Vorgänge, da der Compiler nicht wissen kann, dass diese Adressen gültig sind.
Nach der Berechnung der Adresse verwenden wir den Indexierungsoperator, um den Datensatz in der Tabelle der Ebene 4 anzuzeigen. Wenn dieser Datensatz Null ist, gibt es keine Tabelle der Ebene 3 für diesen Datensatz der Ebene 4. Dies bedeutet, dass
addr
keinem physischen Speicher zugeordnet ist. Also geben wir
None
. Ansonsten wissen wir, dass eine Level 3-Tabelle existiert. Dann wiederholen wir den Vorgang wie auf der vorherigen Ebene.
Nachdem wir drei Seiten einer höheren Ebene überprüft haben, können wir endlich den Datensatz der Tabelle der Ebene 1 lesen, der uns den physischen Frame angibt, mit dem die Adresse zugeordnet ist. Fügen Sie als letzten Schritt den Seitenversatz hinzu - und geben Sie die Adresse zurück.
Wenn wir sicher wären, dass die Adresse zugeordnet ist, können wir direkt auf die Tabelle der Ebene 1 zugreifen, ohne auf die Seiten einer höheren Ebene zu schauen. Da wir dies jedoch nicht wissen, müssen wir zuerst prüfen, ob eine Tabelle der Ebene 1 vorhanden ist. Andernfalls gibt unsere Funktion einen Fehler für fehlende Adressen zurück.
Versuchen Sie es
Versuchen wir, die Übersetzungsfunktion für virtuelle Adressen in unserer
_start
Funktion zu verwenden:
Nach dem Start sehen wir folgendes Ergebnis:
Wie erwartet wird die dem Bezeichner zugeordnete Adresse 0xb8000 in dieselbe physikalische Adresse übersetzt. Die Codepage und die Stapelseite werden in beliebige physikalische Adressen konvertiert, die davon abhängen, wie der Loader die anfängliche Zuordnung für unseren Kernel erstellt hat.
RecursivePageTable
x86_64 bietet einen
RecursivePageTable
Typ, der sichere Abstraktionen für verschiedene Seitentabellenoperationen implementiert. Mit diesem Typ können Sie die Funktion
translate_addr
viel prägnanter implementieren:
Der Typ
RecursivePageTable
kapselt das unsichere Crawlen von Seitentabellen vollständig ein, sodass der
unsafe
Code in der Funktion
translate_addr
nicht mehr benötigt wird. Die
init
Funktion bleibt unsicher, da die Richtigkeit der übergebenen
level_4_table_addr
garantiert werden
level_4_table_addr
.
Unsere
_start
Funktion muss aktualisiert werden, um die Funktion wie folgt neu zu signieren:
Anstatt
LEVEL_4_TABLE_ADDR
an
translate_addr
und über unsichere
LEVEL_4_TABLE_ADDR
auf die Seitentabellen zuzugreifen, übergeben wir jetzt Verweise auf den Typ
RecursivePageTable
. Somit haben wir jetzt eine sichere Abstraktion und eine klare Semantik des Eigentums. Dies stellt sicher, dass wir die Seitentabelle beim gemeinsamen Zugriff nicht versehentlich ändern können, da für deren Änderung der ausschließliche Besitz von
RecursivePageTable
erforderlich ist.
Diese Funktion liefert das gleiche Ergebnis wie die manuell geschriebene Originalübersetzungsfunktion.
Unsichere Funktionen sicherer machen
memory::init
:
unsafe
, . , 4.
unsafe
,
unsafe
.
level_4_table_ptr
:
pub unsafe fn init(level_4_table_addr: usize) -> RecursivePageTable<'static> { let level_4_table_ptr = level_4_table_addr as *mut PageTable; let level_4_table = &mut *level_4_table_ptr;
, , . ,
RecursivePageTable::new
, . - .
, :
unsafe
level_4_table_ptr
, , . Rust
RFC .
, — .
, . 1, . , 3, 3, 2 1.
, . , , 1. , ,
0x1000
.
0xb8000
, VGA. , .
create_maping
memory
:
RecursivePageTable
( )
FrameAllocator
, .
map_to
Mapper
0x1000
0xb8000
. , .
page
frame
,
map_to
. — .
PRESENT
, ,
WRITABLE
.
,
FrameAllocator
.
map_to
, .
Size4KiB
,
Page
PhysFrame
PageSize
, 4 , 2 M / 1 .
map_to
,
Result
. , ,
expect
.
MapperFlush
, (TLB)
flush
.
Result
,
#[must_use]
, .
,
0x1000
,
FrameAllocator
None
.
EmptyFrameAllocator
:
( 'method
allocate_frame
is not a member of trait
FrameAllocator
',
x86_64
0.4.0.)
:
0x1000
,
create_example_mapping
RecursivePageTable
.
0x1000
VGA, - .
0xf021f077f065f04e
,
“New!” .
0x1000
,
println
,
0x900
, .
« VGA» , VGA ,
write_volatile
.
QEMU, :
.
, 1
0x1000
. , ,
map_to
,
EmptyFrameAllocator
. ,
0xdeadbeaf000
0x1000
:
:
panicked at 'map_to failed: FrameAllocationFailed', /…/result.rs:999:5
, 1,
FrameAllocator
. , ?
, , VGA. BIOS UEFI , , . , . ( ) BIOS.
,
_start
. :
BootInfo
, ,
semver .
p4_table_addr
,
memory_map
package
:
p4_table_addr
4. 0o_177777_777_777_777_777_0000
.
memory_map
, (, ).
package
. , .
memory_map
FrameAllocator
,
boot_info
.
entry_point
_start
, . , , .
,
bootloader
Rust
entry_point
. :
extern "C"
no_mangle
,
_start
.
kernel_main
Rust, . , , , , , .
,
memory::init
,
boot_info.p4_table_addr
. , , 4.
BIOS , . :
frames
.
alloc
Iterator::next .
BootInfoFrameAllocator
init_frame_allocator
:
:
iter
MemoryRegion
. filter
, . , , (, ) , InUse
. , , - .
map
range Rust .
- :
into_iter
, 4096- step_by
. 4096 (4 ), . , . map
flat_map
, Iterator<Item = u64>
Iterator<Item = Iterator<Item = u64>>
.
PhysFrame
, Iterator<Item = PhysFrame>
. BootInfoFrameAllocator
.
kernel_main
,
BootInfoFrameAllocator
EmptyFrameAllocator
:
— -
„New!” .
map_to
:
create_maping
— , . .
Zusammenfassung
, 4 . .
, . BIOS, .
Was weiter
,
.