Blog
Aus der ELF in den Adressraum
2022-06-23
Für das Laden von ELF müssen bei uns in den allermeisten Fällen die Daten aus dem Bootmodul in den Zielspeicher kopiert werden: Durch die 512 Byte-Blöcke von TAR und den Eigenheiten vom ELF Daten Segment ist es im Gegensatz zu den Flat Binaries nur unter sehr besonderen Umständen möglich, Seiten direkt zu mappen – aus praktischen Gründen würde ich sogar ausschließlich auf das Kopieren setzen.
Dieses Kopieren ist aber gar nicht so trivial, wie es sich im ersten Moment anhören könnte – denn wir müssen zwischen dem physikalischen Speicher, dem aktiven Mapping (= virtueller Speicher des Kernels) und dem virtuellen Speicher des gerade zu ladenden Prozesses unterscheiden!
Zur Erinnerung: Beim Boot wird in der longmode.asm
eine Identitätsabbildung der ersten 4 GB [mittels 2 MB Huge Pages] aufgesetzt (d.h. in diesem Bereich sind virtuelle und physikalische Adressen gleich). In vielen Gruppen wird dann auch bald auf ein eigenes Kernelmapping mit Identitätsabbildung lediglich der ersten 64 MB – des Kernelspaces – durch 4 KB Pages gewechselt, bevor dann irgendwann das Scheduling gestartet und dabei immer der virtuelle Adressraum des gerade laufenden Threads aktiviert wird.
Bitte beachten: Der zweite Schritt, vor dem Starten der Threads bereits in einen eigenen virtuellen Adressraum mit fein-granularer 4 KB Abbildung zu wechseln, wurde in Aufgabe 3 nicht nur zum besseren Testen empfohlen (“After you’ve successfully activated your kernel mapping and verified the correct behavior of your applications, …”), sondern hat auch den Vorteil, dass wir das eigene Mapping bequem anpassen & ändern können – dies ist mit dem vorgegebenen initialen Mapping aus der longmode.asm
nicht möglich!
Wenn wir nun die ELF-LOAD-Segmente in den virtuellen Adressraum des Threads bringen wollen, müssen wir zuerst eine Seite allokieren. Unser PageFrameAllocator gibt uns bekanntlich einen Seitenrahmen / Kachel (= Seite im physikalischen Speicher) zurück, im konkreten Fall soll das jenseits der 64 MB liegen. Diese Kachel muss gemäß der virtualAddress
im ELF Segment in den Zieladressraum eingeblendet werden (was trivial ist). Allerdings müssen noch die Inhalte aus der ELF-Datei im Bootmodul (Position offset
) in diese Zielseite kopiert werden… Und hier dürfen wir nicht davon ausgehen, dass die Kachel bereits im aktiven Mapping eingeblendet ist: Selbst wenn wir noch das initiale Mapping verwenden, könnte die frisch allokierte Seite jenseits der identitätsabgebildeten 4 GB liegen…
Wie löst man das Problem?
Eine Möglichkeit wäre, dass man direkt vor dem Kopieren auf das Mapping des zukünftigen Threads wechselt. Dann kann man die Daten an die Zieladresse kopieren, gleich nachdem diese eingeblendet hat.
Eine generischere (und etwas schönere/performantere) Lösung wäre, dass man die Seite temporär (für den Kopiervorgang) einfach im aktiven Kernelmapping einblendet: Wir blenden die Zielseite nicht nur im Adressraum des Threads, sondern auch noch an einer bestimmten Adresse im aktuellen virtuellen Adressraum ein, kopieren die Inhalte, und dann im Anschluss wird sie wieder ausgeblendet. Wir müssen nicht den ganzen TLB spülen (durch Ändern von cr3
), sondern können mit invlpg
gezielt den TLB Eintrag für diese bestimmte Adresse als ungültig markieren.
Welche bestimmte Adresse nimmt man nun aber da?
Wenn wir uns eine fixe Adresse ausdenken, so kann es sein, dass diese irgendwie anders verwendet wird… Aber der PageFrameAllocator kann uns ja eine Kachel im Kernelspace geben, und somit wird automatisch sicher gestellt, dass diese noch nicht verwendet wird. Und da wir im Kernelspace ja identitätsabbildung haben (phys = virt) können wir an diese Adresse nun die Zielseite mappen, Daten kopieren, und am Ende wird wieder die Identitätsabbildung hergestellt und die Kachel an den PageFrameAllocator zurück gegeben.
Eine Optimierung wäre, diese Kachel von nun an zukünftig für weitere Kopien zu reservieren, dann spart man sich bei jeder Kopie unmap
& free
Beide Varianten werden sind für diese Aufgabe valid, auch wenn wir damit nicht absolut konform mit dem Multibootstandard sind: Wir gehen in beiden Fällen davon aus, dass die Multibootinformationen und die Bootmodule in den ersten 64 MB liegen – was zwar auf HW und Qemu/KVM der Fall ist, aber der Standard gibt uns diese Sicherheit eigentlich nicht:
The Multiboot information structure and its related substructures may be placed anywhere in memory by the boot loader (with the exception of the memory reserved for the kernel and boot modules, of course)
(da Multiboot selbst 32 bit ist, bedeutet das die ersten 4 GB).
Aber um die Komplexität nun nicht zu erhöhen darf dieser Aspekt außer Acht gelassen werden.