In diesem Artikel möchte ich die praktischen Aspekte der Erstellung eines
einfachen Hypervisors auf der Basis der Intel VMX-Hardwarevirtualisierungstechnologie berücksichtigen.
Die Hardwarevirtualisierung ist ein eher eng spezialisierter Bereich der Systemprogrammierung und hat in Russland mit Sicherheit keine große Community. Ich hoffe, dass das Material in diesem Artikel denjenigen hilft, die die Hardwarevirtualisierung und die damit verbundenen Möglichkeiten entdecken möchten. Wie eingangs gesagt, möchte ich nur den praktischen Aspekt betrachten, ohne in die Theorie einzutauchen. Daher wird davon ausgegangen, dass der Leser mit der x86-64-Architektur vertraut ist und zumindest eine allgemeine Vorstellung von VMX-Mechanismen hat.
Quellen für den Artikel .
Beginnen wir mit der Festlegung von Zielen für den Hypervisor:
- Wird ausgeführt, bevor das Gastbetriebssystem geladen wird
- Unterstützung für einen logischen Prozessor und 4 GB physischen Gastspeicher
- Stellen Sie sicher, dass das Gastbetriebssystem ordnungsgemäß mit Geräten funktioniert, die im physischen Speicherbereich projiziert werden
- VMexits-Verarbeitung
- Das Gastbetriebssystem der ersten Befehle sollte in einer virtuellen Umgebung ausgeführt werden.
- Ausgabe von Debugging-Informationen über den COM-Port (universelle Methode, einfach zu implementieren)
Als Gastbetriebssystem habe ich Windows 7 x32 gewählt, in dem die folgenden Einschränkungen festgelegt wurden:
- Es ist nur ein CPU-Kern beteiligt
- Die PAE-Option ist deaktiviert, sodass ein 32-Bit-Betriebssystem mehr als 4 GB physischen Speicher verwenden kann
- BIOS im Legacy-Modus, UEFI deaktiviert
Beschreibung des Bootloaders
Damit der Hypervisor beim Starten des PCs gestartet werden kann, habe ich den einfachsten Weg gewählt, nämlich meinen Bootloader im MBR-Sektor der Festplatte aufzuschreiben, auf der das Gastbetriebssystem installiert ist. Es war auch notwendig, den Hypervisor-Code irgendwo auf der Festplatte zu platzieren. In meinem Fall liest der ursprüngliche MBR den Bootloader ab dem Sektor 2048, der einen bedingt freien Bereich zum Schreiben in (2047 * 512) Kb bietet. Dies ist mehr als genug, um alle Komponenten eines Hypervisors aufzunehmen.
Unten sehen Sie das Layout des Hypervisors auf der Festplatte. Alle Werte sind in Sektoren festgelegt.

Der Download-Vorgang ist wie folgt:

- loader.mbr liest den Loader-Code loader.main von der Festplatte und überträgt die Kontrolle auf diese.
- loader.main wechselt in den Long-Modus und liest dann die Tabelle der geladenen loader.table-Elemente, auf deren Grundlage das weitere Laden der Hypervisor-Komponenten in den Speicher durchgeführt wird.
- Nachdem der Bootloader die Arbeit im physischen Speicher unter der Adresse 0x100000000 beendet hat, gibt es einen Hypervisor-Code. Diese Adresse wurde so gewählt, dass der Bereich von 0 bis 0xFFFFFFFF für die direkte Zuordnung zum physischen Gastspeicher verwendet werden kann.
- Das ursprüngliche Windows-Netzwerk wird unter der physischen Adresse 0x7C00 gestartet.
Ich möchte darauf aufmerksam machen, dass der Bootloader nach dem Wechsel in den Langmodus die BIOS-Dienste nicht mehr für die Arbeit mit physischen Festplatten verwenden kann. Daher habe ich die "Advance Host Controller Interface" zum Lesen der Festplatte verwendet.
Weitere Details dazu finden Sie
hier .
Hypervisor-Jobbeschreibung
Nachdem der Hypervisor die Kontrolle erhalten hat, besteht seine erste Aufgabe darin, die Umgebung zu initialisieren, in der er arbeiten muss. Dazu werden die Funktionen nacheinander aufgerufen:
- InitLongModeGdt () - Erstellt und lädt eine Tabelle mit 4 Deskriptoren: NULL, CS64, DS64, TSS64
- InitLongModeIdt (isr_vector) - initialisiert die ersten 32 Interrupt-Vektoren durch einen gemeinsamen Handler bzw. dessen Stub
- InitLongModeTSS () - Initialisiert das Taskstatussegment
- InitLongModePages () - Initialisierung von Paging:
[0x00000000 - 0xFFFFFFFF] - Seitengröße 2 MB, Cache deaktivieren;
[0x100000000 - 0x13FFFFFFF] - Seitengröße 2 MB, Cache-Rückschreiben, globale Seiten;
[0x140000000 - n] - nicht vorhanden; - InitControlAndSegmenRegs () - Segmentregister neu laden
Als Nächstes müssen Sie sicherstellen, dass der Prozessor VMX unterstützt. Die Überprüfung wird von der Funktion
CheckVMXConditions () durchgeführt :
- CPUID.1: ECX.VMX [Bit 5] muss auf 1 gesetzt sein
- Im MSR-Register IA32_FEATURE_CONTROL muss Bit 2 gesetzt sein - aktiviert VMXON außerhalb des SMX-Betriebs und Bit 0 - Lock (relevant beim Debuggen in Bochs)
Wenn alles in Ordnung ist und der Hypervisor auf einem Prozessor ausgeführt wird, der die Hardwarevirtualisierung unterstützt, fahren Sie mit der anfänglichen Initialisierung von VMX fort.
Weitere Informationen finden Sie in der Funktion
InitVMX () :
- Erstellt Speicherbereiche VMXON und VMCS (Virtual Data Control Data Structures) mit einer Größe von 4096 Byte. Die VMCS-Revisionskennung aus MSR IA32_VMX_BASIC wird in den ersten 31 Bits jedes Bereichs aufgezeichnet.
- Es wird geprüft, ob in den Systemregistern CR0 und CR4 alle Bits gemäß den Anforderungen von VMX gesetzt sind.
- Der logische Prozessor wird durch den Befehl VMXON (die physikalische Adresse der VMXON-Region als Argument) in den VMX-Root-Modus versetzt.
- Der Befehl VMCLEAR (VMCS) setzt den Startstatus von VMCS auf Clear, und der Befehl setzt implementierungsspezifische Werte auf VMCS.
- Der Befehl VMPTRLD (VMCS) lädt die aktuelle VMCS-Adresse, die als Argument übergeben wurde, in den aktuellen VMCS-Zeiger.
Die Ausführung des Gastbetriebssystems beginnt im Real-Modus ab der Adresse 0x7C00, an der, wie wir uns erinnern, der Loader.main-Loader win7.mbr platziert. Um eine virtuelle Umgebung neu zu erstellen, die mit der identisch ist, in der mbr normalerweise ausgeführt wird, wird die Funktion
InitGuestRegisterState () aufgerufen, mit der die vmx-Nicht-Root-Register wie folgt festgelegt werden:
CR0 = 0x10 CR3 = 0 CR4 = 0 DR7 = 0 RSP = 0xFFD6 RIP = 0x7C00 RFLAGS = 0x82 ES.base = 0 CS.base = 0 SS.base = 0 DS.base = 0 FS.base = 0 GS.base = 0 LDTR.base = 0 TR.base = 0 ES.limit = 0xFFFFFFFF CS.limit = 0xFFFF SS.limit = 0xFFFF DS.limit = 0xFFFFFFFF FS.limit = 0xFFFF GS.limit = 0xFFFF LDTR.limit = 0xFFFF TR.limit = 0xFFFF ES.access rights = 0xF093 CS.access rights = 0x93 SS.access rights = 0x93 DS.access rights = 0xF093 FS.access rights = 0x93 GS.access rights = 0x93 LDTR.access rights = 0x82 TR.access rights = 0x8B ES.selector = 0 CS.selector = 0 SS.selector = 0 DS.selector = 0 FS.selector = 0 GS.selector = 0 LDTR.selector = 0 TR.selector = 0 GDTR.base = 0 IDTR.base = 0 GDTR.limit = 0 IDTR.limit = 0x3FF
Es ist zu beachten, dass das Grenzfeld des Deskriptor-Cache für die Segmentregister DS und ES 0xFFFFFFFF ist. Dies ist ein Beispiel für die Verwendung des unwirklichen Modus - eine x86-Prozessorfunktion, mit der Sie die Begrenzung von Segmenten im realen Modus umgehen können. Mehr dazu lesen Sie
hier .
Im VMX-Nicht-Root-Modus kann es vorkommen, dass das Gastbetriebssystem im VMX-Root-Modus die Steuerung an den Host zurückgeben muss. In diesem Fall tritt ein VM-Exit auf, bei dem der aktuelle Status von vmx non-root gespeichert und vmx-root geladen wird. Initialisierung VMX-root läuft
InitHostStateArea () Funktion, die den Wert der folgenden Register setzt:
CR0 = 0x80000039 CR3 = PML4_addr CR4 = 0x420A1 RSP = STACK64 RIP = VMEXIT_handler ES.selector = 0x10 CS.selector = 0x08 SS.selector = 0x10 DS.selector = 0x10 FS.selector = 0x10 GS.selector = 0x10 TR.selector = 0x18 TR.base = TSS GDTR.base = GDT64 IDTR.base = IDTR
Als nächstes wird die Erstellung des physischen
Gastadressraums durchgeführt (InitEPT () -Funktion). Dies ist einer der wichtigsten Momente beim Erstellen eines Hypervisors, da eine falsch eingestellte Größe oder ein falsch eingestellter Typ an einem der Speicherorte zu Fehlern führen kann, die sich möglicherweise nicht sofort manifestieren, aber mit hoher Wahrscheinlichkeit zu unerwarteten Bremsen oder Einfrierungen des Gastbetriebssystems führen. Im Allgemeinen gibt es hier wenig Angenehmes und es ist besser, der Abstimmung des Gedächtnisses genügend Aufmerksamkeit zu schenken.
Das folgende Bild zeigt das Modell des physischen Adressraums des Gastes:

Also, was wir hier sehen:
- [0 - 0xFFFFFFFF] der gesamte Bereich des Gastadressraums. Standardtyp: zurückschreiben
- [0xA0000 - 0xBFFFFF] - Video-RAM. Typ: nicht zwischenspeicherbar
- [0xBA647000 - 0xFFFFFFFF] - Geräte-RAM. Typ: nicht zwischenspeicherbar
- [0x0000000 - 0xCFFFFFFF] - Video-RAM. Typ: Schreibkombination
- [0xD0000000 - 0xD1FFFFFF] - Video-RAM. Typ: Schreibkombination
- [0xFA000000 - 0xFAFFFFFF] - Video-RAM. Typ: Schreibkombination
Ich habe die Informationen zum Erstellen solcher Bereiche aus dem Dienstprogramm RAMMap (Registerkarte Physikalische Bereiche) übernommen und auch die Daten aus dem Windows-Geräte-Manager verwendet. Natürlich unterscheiden sich die Adressbereiche auf einem anderen PC wahrscheinlich. Der Gastspeichertyp wird in meiner Implementierung nur durch den in den EPT-Tabellen angegebenen Wert bestimmt. Es ist einfach, aber nicht ganz korrekt, und im Allgemeinen sollte die Art des Speichers berücksichtigt werden, den das Gastbetriebssystem in seiner Seitenadressierung installieren möchte.
Nachdem die Erstellung des
Gastadressraums abgeschlossen ist, können Sie mit den Feldeinstellungen für die VM-Ausführungssteuerung
fortfahren ( Funktion
InitExecutionControlFields () ). Dies ist eine ziemlich große Auswahl an Optionen, mit denen Sie die Betriebsbedingungen des Gastbetriebssystems im VMX-Nicht-Root-Modus festlegen können. Sie können beispielsweise Anrufe an Eingabe- / Ausgabeports verfolgen oder Änderungen in MSR-Registern überwachen. In unserem Fall verwende ich jedoch nur die Möglichkeit, die Einstellung bestimmter Bits im CR0-Register zu steuern. Tatsache ist, dass 30 (CD) und 29 (NW) Bits sowohl für den vmx-Nicht-Root- als auch für den vmx-Root-Modus gleich sind. Wenn das Gastbetriebssystem diese Bits auf 1 setzt, wirkt sich dies negativ auf die Leistung aus.
Der Konfigurationsprozess des Hypervisors ist fast abgeschlossen. Es bleibt nur die Kontrolle über den Übergang zum Gastmodus vmx non-root und die Rückkehr zum Hostmodus vmx root. Einstellungen werden in den Funktionen vorgenommen:
InitVMEntryControl () -Einstellungen für den Übergang zu vmx non-root:
- Laden Sie den Gast IA32_EFER
- Laden Sie den Gast IA32_PAT
- Gast-MSRs laden (IA32_MTRR_PHYSBASE0, IA32_MTRR_PHYSMASK0, IA32_MTRR_DEF_TYPE)
InitVMExitControl () -Einstellungen für den Wechsel zu vmx root:
- Host laden IA32_EFER;
- Gast speichern IA32_EFER;
- Host laden IA32_PAT;
- Gast speichern IA32_PAT;
- Host.CS.L = 1, Host.IA32_EFER.LME = 1, Host.IA32_EFER.LMA = 1;
- Gast-MSRs speichern (IA32_MTRR_PHYSBASE0, IA32_MTRR_PHYSMASK0, IA32_MTRR_DEF_TYPE);
- Host-MSRs laden (IA32_MTRR_PHYSBASE0, IA32_MTRR_PHYSMASK0, IA32_MTRR_DEF_TYPE);
Nachdem alle Einstellungen abgeschlossen sind,
versetzt die Funktion
VMLaunch () den Prozessor in den VMX-Nicht-Root-Modus und das Gastbetriebssystem wird ausgeführt. Wie bereits erwähnt, können Bedingungen in den Einstellungen für die VM-Ausführungssteuerung festgelegt werden. In diesem Fall gibt der Hypervisor die Steuerung im VMX-Root-Modus an sich selbst zurück. In meinem einfachen Beispiel gebe ich dem Gastbetriebssystem vollständige Handlungsfreiheit. In einigen Fällen muss der Hypervisor jedoch noch eingreifen und das Betriebssystem anpassen.
- Wenn das Gastbetriebssystem versucht, die CD- und NW-Bits im CR0-Register zu ändern, wird der VM-Exit-Handler verwendet
korrigiert die in CR0 aufgezeichneten Daten. Das CR0-Leseschattenfeld wird ebenfalls so geändert, dass das Gastbetriebssystem beim Lesen von CR0 den aufgezeichneten Wert empfängt. - Ausführen des Befehls xsetbv. Dieser Befehl ruft unabhängig von den Einstellungen immer VM Exit auf, daher habe ich gerade seine Ausführung im VMX-Root-Modus hinzugefügt.
- Ausführen des Befehls cupid. Dieser Befehl ruft auch einen bedingungslosen VM-Exit auf. Aber ich habe eine kleine Änderung an seinem Handler vorgenommen. Wenn die Werte im eax-Argument 0x80000002 - 0x80000004 sind, gibt cpuid nicht den Namen der Prozessormarke zurück, sondern die Zeile: VMX Study Core :) Das Ergebnis ist im Screenshot zu sehen:

Zusammenfassung
Der als Beispiel für den Artikel geschriebene Hypervisor kann den stabilen Betrieb des Gastbetriebssystems durchaus unterstützen, obwohl es sich natürlich nicht um eine vollständige Lösung handelt. Intel VT-d wird nicht verwendet, es wird nur ein logischer Prozessor unterstützt, es gibt keine Kontrolle über Unterbrechungen und den Betrieb von Peripheriegeräten. Im Allgemeinen habe ich fast nichts aus den umfangreichen Tools verwendet, die Intel für die Hardwarevirtualisierung bereitstellt. Wenn die Community jedoch interessiert ist, werde ich weiterhin über Intel VMX schreiben, zumal es etwas zu schreiben gibt.
Ja, ich hätte fast vergessen, dass es praktisch ist, den Hypervisor und seine Komponenten mit Bochs zu debuggen. Zunächst ist es ein unverzichtbares Werkzeug. Leider unterscheidet sich das Herunterladen eines Hypervisors in Bochs vom Herunterladen auf einen physischen PC. Einmal habe ich eine spezielle Versammlung zusammengestellt, um diesen Prozess zu vereinfachen. Ich werde versuchen, die Quellen in Ordnung zu bringen und sie in naher Zukunft mit dem Projekt zusammenzustellen.
Das ist alles. Vielen Dank für Ihre Aufmerksamkeit.