Benötigen x86-Anweisungen ihre eigene Codierung sowie alle ihre Argumente, um gleichzeitig im Speicher vorhanden zu sein?

64

Ich versuche herauszufinden, ob es möglich ist, eine Linux-VM auszuführen, deren RAM nur von einer einzigen physischen Seite unterstützt wird.

Um dies zu simulieren, habe ich den Handler für verschachtelte Seitenfehler in KVM so geändert, dass das vorhandene Bit aus allen NPT-Einträgen (Nested Page Table) entfernt wird, mit Ausnahme desjenigen, der dem aktuell verarbeiteten Seitenfehler entspricht.

Beim Versuch, einen Linux-Gast zu starten, habe ich festgestellt, dass Assembly-Anweisungen, die Speicheroperanden verwenden, wie z

add [rbp+0x820DDA], ebp

führen zu einer Seitenfehlerschleife, bis ich das aktuelle Bit für die Seite, die die Anweisung enthält, sowie für die Seite, auf die im Operanden verwiesen wird (in diesem Beispiel [rbp+0x820DDA]), wiederherstelle .

Ich frage mich, warum das so ist. Sollte die CPU nicht nacheinander auf die Speicherseiten zugreifen, dh zuerst die Anweisung lesen und dann auf den Speicheroperanden zugreifen? Oder erfordert x86, dass sowohl die Anweisungsseite als auch alle Operandenseiten gleichzeitig zugänglich sind?

Ich teste auf AMD Zen 1.

Savvybug
quelle
2
Warum willst du das tun?
SS Anne
11
Nur aus technischem Interesse :)
savvybug
14
Upvoting für die lustige Projektidee.
Pipe
10
Dies ist verrückt auf der Ebene "Linux auf einem 486-Emulator booten, der in JavaScript im Browser ausgeführt wird". Ich liebe es.
Chrylis -on Streik-
3
Heh, anscheinend habe ich diese Frage zu dem gleichen logischen Schluss gebracht, über den Sie bereits nachgedacht haben, über das Mindestarbeitsvolumen für garantierte Fortschritte. Ich hatte das bereits beantwortet, bevor Sie den neuen ersten Absatz zur Frage hinzugefügt haben. : PI hat an einigen Stellen einige Links und weitere Details hinzugefügt (z. B. darf der Page-Walker einige Einträge im Gast-Seitenverzeichnis intern zwischenspeichern), da diese Frage viel mehr Aufmerksamkeit erhält, als ich erwartet hatte, da sie es irgendwie zu HNQ geschafft hat.
Peter Cordes

Antworten:

56

Ja, sie benötigen den Maschinencode und alle Speicheroperanden.

Sollte die CPU nicht nacheinander auf die Speicherseiten zugreifen, dh zuerst die Anweisung lesen und dann auf den Speicheroperanden zugreifen?

Ja, das passiert logischerweise, aber eine Seitenfehlerausnahme unterbricht diesen zweistufigen Prozess und verwirft jeden Fortschritt. Die CPU kann sich nicht merken, welche Anweisung sich in der Mitte eines Seitenfehlers befand.

Wenn ein Seitenfehler-Handler nach der Behandlung eines gültigen Seitenfehlers zurückkehrt, ist RIP = die Adresse des Fehlerbefehls, sodass die CPU erneut versucht, ihn von Grund auf neu auszuführen .

Es wäre für das Betriebssystem legal, den Maschinencode des fehlerhaften Befehls zu ändern und zu erwarten, dass es nach iretdem Seitenfehler-Handler (oder einem anderen Ausnahme- oder Interrupt-Handler) einen anderen Befehl ausführt . AFAIK ist es architektonisch erforderlich, dass die CPU den Code-Abruf von CS: RIP wiederholt, falls Sie sprechen. (Angenommen, es kehrt sogar zum fehlerhaften CS: RIP zurück, anstatt einen anderen Prozess zu planen, während auf einen Festplattenfehler auf der Festplatte gewartet wird, oder einen SIGSEGV an einen Signalhandler bei einem ungültigen Seitenfehler zu senden.)

Es ist wahrscheinlich auch architektonisch für den Ein- / Ausstieg von Hypervisoren erforderlich. Und selbst wenn dies auf dem Papier nicht ausdrücklich verboten ist, funktionieren CPUs nicht so.

@torek kommentiert, dass einige (CISC) Mikroprozessoren Anweisungen teilweise dekodieren und den Mikroregister-Status bei einem Seitenfehler ausgeben , aber x86 ist nicht so.


Einige Anweisungen sind unterbrechbar und können teilweise Fortschritte machen, z. B. rep movs(memcpy in a can) und andere Zeichenfolgenanweisungen, oder Lasten / Streuspeicher sammeln. Der einzige Mechanismus besteht jedoch darin, Architekturregister wie RCX / RSI / RDI für String-Ops oder die Ziel- und Maskenregister für Gather zu aktualisieren (z. B. manuell für AVX2vpgatherdd ). Wenn der Opcode / Decod nicht beibehalten wird, führt dies zu einem versteckten internen Register und einem Neustart nach dem Iret von einem Seitenfehler-Handler. Dies sind Anweisungen, die mehrere separate Datenzugriffe ausführen.

Denken Sie auch daran, dass x86 (wie die meisten ISAs) garantiert, dass Anweisungen atomar geschrieben sind. Interrupts / Ausnahmen: Sie treten entweder vollständig oder gar nicht vor einem Interrupt auf. Unterbrechen einer Montageanweisung während des Betriebs . So add [mem], regwäre beispielsweise erforderlich, die Last zu verwerfen, wenn das Speicherteil fehlerhaft ist, auch ohne lockPräfix.


Die ungünstigste Anzahl von Gastbenutzerseiten, die vorhanden sind, um Fortschritte zu erzielen, beträgt möglicherweise 6 (plus separate Gastbaum-Seitentabellen-Teilbäume für jede Seite):

  • movsqoder movsw2-Byte-Befehl, der sich über eine Seitengrenze erstreckt, sodass beide Seiten zum Dekodieren benötigt werden.
  • qword source operand [rsi]auch ein seitensplit
  • qword Zieloperand [rdi]auch eine Seitenaufteilung

Wenn eine dieser 6 Seiten fehlerhaft ist, sind wir wieder auf dem ersten Platz.

rep movsdist auch eine 2-Byte-Anweisung, und Fortschritte in einem Schritt zu machen, hätte die gleiche Anforderung. Ähnliche Fälle wie push [mem]oder pop [mem]könnten mit einem falsch ausgerichteten Stapel konstruiert werden.

Einer der Gründe (oder Nebeneffekte) dafür, Sammellasten / Streuspeicher "unterbrechbar" zu machen (Aktualisieren des Maskenvektors mit ihrem Fortschritt), besteht darin, zu vermeiden, dass dieser minimale Platzbedarf erhöht wird, um einen einzelnen Befehl auszuführen. Auch zur Verbesserung der Effizienz bei der Behandlung mehrerer Fehler während einer Erfassung oder Streuung.


@Brandon weist in Kommentaren darauf hin, dass ein Gast seine Seitentabellen im Speicher benötigt und die Seitenaufteilungen für den Benutzerbereich auch 1-GB-Aufteilungen sein können, sodass sich die beiden Seiten in unterschiedlichen Unterbäumen der PML4 der obersten Ebene befinden. HW Page Walk muss alle diese Seiten mit Gastseitentabellen berühren, um Fortschritte zu erzielen. Es ist unwahrscheinlich, dass eine solche pathologische Situation zufällig eintritt.

Der TLB (und die Page-Walker-Interna) dürfen einige der Seitentabellendaten zwischenspeichern und müssen den Page-Walk nicht von Grund auf neu starten, es sei denn, das Betriebssystem hat invlpgein neues CR3-Seitenverzeichnis der obersten Ebene erstellt oder festgelegt. Beides ist nicht erforderlich, wenn eine Seite von nicht vorhanden in vorhanden geändert wird. x86 auf Papier garantiert, dass es nicht benötigt wird (daher ist "negatives Caching" nicht vorhandener PTEs nicht zulässig, zumindest für Software nicht sichtbar). Daher kann es sein, dass die CPU nicht VMexit wird, selbst wenn einige der physischen Seiten mit Gast-Seitentabellen nicht vorhanden sind.

PMU-Leistungsindikatoren können so aktiviert und konfiguriert werden, dass der Befehl auch ein Perf-Ereignis zum Schreiben in einen PEBS-Puffer für diesen Befehl erfordert . Wenn die Maske eines Zählers so konfiguriert ist, dass nur Anweisungen für den Benutzerbereich und nicht der Kernel gezählt werden, kann es durchaus sein, dass bei jeder Rückkehr in den Benutzerbereich immer wieder versucht wird, den Zähler zu überlaufen und ein Sample im Puffer zu speichern, was zu einem Seitenfehler führt.

Peter Cordes
quelle
15
Der schlimmste Fall für eine einzelne Anweisung könnte so etwas wie " push dword [foo" (oder sogar nur call [foo]) sein, bei dem alles über die "Seitenverzeichniszeiger-Tabellengrenze" falsch ausgerichtet ist (Hinzufügen von bis zu 6 Seiten, 6 Seitentabellen, 6 Seitenverzeichnissen, 6 PDPTs und einer PML4). Die Funktion "Präzises ereignisbasiertes Sampling mit PEBS-Puffer" der CPU ist aktiviert und so konfiguriert, dass pushLeistungsüberwachungsdaten zum PEBS-Puffer hinzugefügt werden. Für eine konservative "Mindestseite, die vom Host bereitgestellt wird, damit der Gast in pathologischen Fällen Fortschritte erzielen kann", würde ich mindestens 16 Seiten wünschen.
Brendan
4
Beachten Sie, dass dies in CISC-y-Architekturen schon immer üblich war. Einige Mikroprozessoren decodieren teilweise Befehle und geben den Mikroregisterstatus bei einem Seitenfehler aus, andere verlangen und / oder erfordern jedoch nicht, dass sich Adressoperanden für "loop-y" -Anweisungen (DBRA auf m68k, MOVC3 / MOVC5 auf Vax usw.) in ähnlichen Registern befinden zu Ihrem REP MOVS Beispiel.
Torek
1
@Brendan: Jemand hat einen Worst-Case für eine VAX-Anweisung mit etwa 50 Seiten gezählt. Ich habe die Details vergessen, aber Sie würden die Anweisung offensichtlich selbst an eine Seitengrenze setzen, so etwas wie die Übersetzungstabelle mit der Tabelle verwenden, die eine Seitengrenze überspannt, (rX) [rY] mit den Indirekten an den Seitengrenzen verwenden und bald. Die haarigsten Anweisungen dauerten bis zu 6 Operanden (Laden in r0-r5), und alle sechs könnten doppelte Indirekte sein, denke ich.
Torek
3
Das Betriebssystem kann die Anweisung ändern, aber es kann sich auch ändern EIP. Es gibt also eine logische Folgefrage. Wie viele Seiten werden mindestens benötigt, wenn ein intelligentes Anweisungs-Patch-Schema vorausgesetzt wird? Kopieren Sie beispielsweise den nicht ausgerichteten Wert in einen ausgerichteten Arbeitspuffer, emulieren Sie den Befehl und IRET in den nächsten Befehl.
MSalters
1
Die Seite mit den iretAnweisungen des Betriebssystems muss sich ebenfalls im Speicher befinden. Dies ist eine Ein-Byte-Anweisung, also eine zusätzliche Seite. Die Interrupt-Adresse des Seitenfehler-Handlers muss sich ebenfalls im Speicher befinden, dies kann jedoch dieselbe Seite wie oben sein.
Stig Hemmer