• Navigation überspringen
  • Zur Navigation
  • Zum Seitenende
Organisationsmenü öffnen Organisationsmenü schließen
Friedrich-Alexander-Universität Lehrstuhl für Informatik 4 (Systemsoftware)
  • FAUZur zentralen FAU Website
  1. Friedrich-Alexander-Universität
  2. Technische Fakultät
  3. Department Informatik
Suche öffnen
  • English
  • Campo
  • StudOn
  • FAUdir
  • Stellenangebote
  • Lageplan
  • Hilfe im Notfall
  1. Friedrich-Alexander-Universität
  2. Technische Fakultät
  3. Department Informatik
Friedrich-Alexander-Universität Lehrstuhl für Informatik 4 (Systemsoftware)
Menu Menu schließen
  • Lehrstuhl
    • Team
    • Aktuelles
    • Kontakt und Anfahrt
    • Leitbild
    • 50-jähriges Jubiläum
    Portal Lehrstuhl
  • Forschung
    • Forschungsbereiche
      • Betriebssysteme
      • Confidential Computing
      • Embedded Systems Software
      • Verteilte Systeme
    • Projekte
      • AIMBOS
      • BALu
      • BFT2Chain
      • DOSS
      • Mirador
      • NEON
      • PAVE
      • ResPECT
      • Watwa
    • Projektkampagnen
      • maRE
    • Seminar
      • Systemsoftware
    Portal Forschung
  • Publikationen
  • Lehre
    • Sommersemester 2025
      • Applied Software Architecture
      • Ausgewählte Kapitel der Systemsoftware
      • Betriebssystemtechnik
      • Projekt angewandte Systemsoftwaretechnik
      • System-Level Programming
      • Systemnahe Programmierung in C
      • Systemprogrammierung 1
      • Verteilte Systeme
    • Wintersemester 2024/25
      • Betriebssysteme
      • Middleware – Cloud Computing
      • Systemprogrammierung 2
      • Verlässliche Echtzeitsysteme
      • Virtuelle Maschinen
      • Web-basierte Systeme
    Portal Lehre
  • Examensarbeiten
  1. Startseite
  2. Extern

Extern

Bereichsnavigation: Lehre
  • Betriebssystemtechnik
    • Vorlesung
      • Folien
      • Glossar
    • Übung
      • Aufgaben
      • Dokumentation
        • Blog
          • Entwicklungsumgebung
            • Assembler Crashkurs
              • C++ Crashkurs
                • 🔗 Testrechnerverwaltung
                • 🔗 Adora-Belle (Helpdesk)
                • Kontakt
              • Evaluation

              Blog

              Die Instruktion zur Seiteninvalidierung

              Bernhard Heinloth

              2022-06-28

              In der Tafelübung wurde die Instruktion invlpg gezeigt, welche im Gegensatz zum Schreiben des cr3 Registers nicht zwingend den ganzen TLB, sondern nur eine einzelne Zuordnung von einer (virtuelle) Adresse entfernen kann – und somit auch entsprechend positiv auf die Performance auswirken kann:

              asm volatile("invlpg (%0)" :: "r"(addr) : "memory");

              Sofern der TLB einen Eintrag hat, welcher der virtuellen Seitenadresse addr auf die physikalische Seite zuordnet, wird dieser invalidiert – und beim nächsten Zugriff auf diese virtuelle Adresse wird die MMU die Seitentabellen neu durchlaufen um die Zuordnung zu bestimmen.

              Laut Intel Manual kann es unter Umständen auch mehrere Einträge bis hin zum ganzen TLB leeren. Aber dies sind sonderfälle, es darf davon ausgegangen werden, dass die Operation meist eine bessere, zumindest aber keine schlechter Performance als das vollständige Spülen des TLBs (durch cr3 schreiben) bringt!

              Verwendung

              Die invlpg Instruktion braucht laut Intel Manual eine effektive Adresse als Operand.

              Der Operand darf direkt die Speicheradresse (z.B. 0x1234000) adressieren:

              invlpg (0x1234000)

              Alternativ ist auch invlpg 0x1234000 erlaubt (was aber eine etwas verwirrende Schreibweise ist und in den Bereich des syntaktischen Zuckers anzusiedeln ist), es generiert den gleichen Maschinencode.

              Mit Intelsyntax (statt AT&T) lautet die Instruktion invlpg [0x1234000] (es gibt in NASM keine Kurzform)

              Daneben darf natürlich auch registerindirekt adressiert werden – das gleiche Ergebnis wie oben erreicht man via:

              mov $0x1234000,%rax
              invlpg (%rax)

              bzw in Intel Syntax

              mov rax,0x1234000
              invlpg [rax]

              Weitere Adressierungsmöglichkeiten

              Selbstverständlich sind auch die komplexeren Adressierungsarten erlaubt

              Adressierungsart AT&T Syntax Intelsyntax Maschinencode
              displacement invlpg (0x1234000) invlpg [0x1234000] 0f 01 3c 25 00 40 23 01
              base invlpg (%rax) invlpg [rax] 0f 01 38
              base + index invlpg (%rax,%rbx,1) invlpg [rax + rbx] 0f 01 3c 18
              base + displacement invlpg 0x42(%rax) invlpg [rax + 0x42] 0f 01 78 42
              base + index + displacement invlpg 0x42(%rax,%rbx,1) invlpg [rax + rbx + 0x42] 0f 01 7c 18 42
              base + (index * scale) invlpg (%rax,%rcx,8) invlpg [rax + 8 * rcx] 0f 01 3c c8
              (index * scale) + displacement invlpg var(,%rcx,8) invlpg [8 * rcx + var] 0f 01 3c cd xx xx xx xx
              base + (index * scale) + displacement invlpg var(%rax,%rcx,8) invlpg [rax + 8 * rcx + var] 0f 01 bc c8 xx xx xx xx

              Im Beispiel ist var eine Variable/Symbol, welches durch die Adresse der Speicherstelle ersetzt wird.

              Für weitere Details (und wie die Adressierungsart aus einer Hochsprache generiert werden könnte) siehe blog.yossarian.net/2020/06/13/How-x86_64-addresses-memory

              Bedienfehler

              Was jedoch nicht geht, ist eine reine Registeradressierung (also ohne die Klammer, welche für die Indirektion steht:

              mov $0x1234000,%rax
              invlpg %rax

              Das wirft bei der Übersetzung bzw. Assemblierung einen Fehler.

              D.h. der inline-Assembler muss

              uintptr_t addr = 0x1234000;
              asm("invlpg (%0)" :: "r"(addr));

              lauten (die Klammer um %0 nicht vergessen!)

              Eine speicherindirekte Adressierung ist nicht möglich, der Code

              push $0x1234000
              invlpg (%rsp)

              invalidiert nicht die Adresse 0x1234000, sondern die Seite mit Stack! Die Instruktion ist somit eher mit lea (%rsp),X (statt mit mov (%rsp),X) vergleichbar.

              Alternative Schreibweise

              Kann man auch statt einem Registeroperand ("r") einen Speicheroperand ("m") im Inline-Assembler übergeben?

              Ja, aber nur als Hack.

              asm volatile("invlpg %0" :: "m"(*(void**)addr));

              Wieso nun hier keine Klammer? Weil wir durch die Dereferenzierung den Übersetzer zwingen die Adressierungsart (Indirektion) zu generieren:

              void invalidate(void **addr) {
                  asm volatile("invlpg %0" :: "m"(*addr));
              }

              wird zu

              invalidate:
                  invlpg (%rdi)
                  retq

              weil der Parameter addr laut der System V ABI im Register rdi übergeben wird, wird durch die Dereferenzierung der registerindirekte Zugriff (rdi), wodurch wieder die effektive Adresse gleich dem Inhalt von rdi ist – und mit der Instruktion im TLB gespühlt wird.

              Flüchtigkeit

              Der Beispielaufruf hat ein volatile – wozu?

              Nun, das volatile Schlüsselwort bei asm verhindert (wie bei Variablen) Optimierungen. Es darf nicht weggelassen werden, noch umgeordnet. Beides wollen wir.

              Allerdings wäre es im konkreten Fall nicht notwendig, da gemäß der Gcc-Doku dies mangels Ausgabeoperanden (nach dem ersten :) implizit als volatile betrachtet wird:

              GCC’s optimizers sometimes discard asm statements if they determine there is no need for the output variables. Also, the optimizers may move code out of loops if they believe that the code will always return the same result (i.e. none of its input values change between calls). Using the volatile qualifier disables these optimizations. asm statements that have no output operands and asm goto statements, are implicitly volatile.

              Aber, es schadet nicht wenn wir dennoch explizit volatile hinzuschreiben.

              Speicherbarriere

              Der Beispielaufruf hat am Ende ein Annotation, welche im Gcc eine Speicherbarriere bewirkt

              Wieso memory nützlich sein kann

              Beispiel:

              // Zeiger auf Adresse 0x1234890 im virtuellen Speicher
              int * ptr = (int*) 0x1234890;
              
              // Zeiger auf den Eintrag der PageTable im aktiven Mapping welche `ptr` beinhaltet
              PageTableEntry * active = get_active_pagetable_entry(0x1234000);
              // Nun ist zum Beispiel `active->address = 0x1bac000`,
              // d.h. die virt. Adresse 0x1234890 zeigt auf die physikalische Adresse 0x1bac890
              
              // Wert in Speicher (an phys. Adresse 0x1bac890) schreiben
              *ptr = 23;
              
              // Wir mappen nun eine neue Seite (mit einer anderen physikalische Adresse) an den gleichen virtuellen Speicher
              active->address = 0x2cbd000;
              // d.h. die virt. Adresse 0x1234890 zeigt nun auf die physikalische Adresse 0x2cbd890
              
              // (virt.) Seite muss nun noch im TLB invalidiert werden
              asm("invlpg (%0)" :: "r"(0x1234000));
              
              // Wert in Speicher (nun an phys. Adresse 0x2cbd890) schreiben
              *ptr = 42;

              Unsere Erwartung ist, dass nun im physikalischen Speicher an Adresse 0x1bac890 der Wert 23 und bei 0x2cbd890 der Wert 42 steht.

              Wenn wir das aber mit Optimierungen kompilieren, wird das nicht der Fall sein: An der ersten Adresse steht nicht 23… Der Übersetzer weiß nicht das wir an der MMU rumspielen und da den virtuellen Adressraum ändern – er sieht nur zwei Schreibzugriffe auf *ptr (welcher auf den virtuellen Speicher 0x1234890 zeigt) ohne einen Lesezugriff und will schlau sein: “Wenn der erste Wert nur geschrieben, nicht gelesen, und kurz drauf wieder überschrieben wird, dann wird doch der erste Schreibzugriff gar nicht gebraucht?”.

              Um den Compiler das nun auszutreiben, kann man im inline-Assembler nun noch den clobber memory angeben (asm("invlpg (%0)" :: "r"(0x1234000) : "memory");), dieser bewirkt, dass alle Speicherinhalte geschrieben werden:

              The “memory” clobber tells the compiler that the assembly code performs memory reads or writes to items other than those listed in the input and output operands (for example, accessing the memory pointed to by one of the input parameters). To ensure memory contains correct values, GCC may need to flush specific register values to memory before executing the asm. Further, the compiler does not assume that any values read from memory before an asm remain unchanged after that asm; it reloads them as needed. Using the “memory” clobber effectively forms a read/write memory barrier for the compiler.

              Der Unterschied ist bei -O3 Optimierungen mit objdump sehr gut zu sehen:

              1091:       c7 04 25 90 48 23 01    movl   $0x17,0x1234890      // *ptr = 23;
              1098:       17 00 00 00 
              109c:       48 ba 00 00 00 bd 2c    movabs $0x2cbd000000,%rdx   // active->address = 0x2cbd000;
              10a3:       00 00 00 
                                       ...
              10b7:       b8 00 40 23 01          mov    $0x1234000,%eax      // asm("invlpg (%0)" ...);
              10bc:       67 0f 01 38             invlpg (%eax)
              10c0:       c7 04 25 90 48 23 01    movl   $0x2a,0x1234890      // *ptr = 42;

              Nur wenn memory angegeben wurde, ist die Instruktion bei 1091 vorhanden, sonst fehlt sie.

              Alternativ könnte man natürlich auch durch volatile bei ptr den gleichen Effekt erzielen, das müsste man aber für jeden entsprechenden Zeiger machen.

              Wieso memory aber in den meisten Fällen doch nicht gebraucht wird

              Der vorherige Beispielcode hat ganz bewusst keinen Funktionsaufurf zwischen den beiden Schreibzugriffen auf *ptr.

              In der Praxis haben wir aber vermutlich statt active->address = 0x2cbd000; einen Funktions- oder Methodenaufruf wie current->map(0x1234000, 0x2cbd000);, welcher im aktuellen Mapping (current) die virtuelle Adresse 0x1234000 auf die physikalische Adresse 0x2cbd000 zeigen lässt.

              Und der Übersetzer wird nun eine call Instruktion bauen, und da er vermutlich nicht weiß, was diese Funktion macht, muss er zwangsweise auch vom worst-case ausgehen – in unserem Fall, dass diese auch auf auf die Adresse 0x1234890 zugreifen könnte. Entsprechend muss er den an diese Adresse auch vor dem Aufruf die 23 schreiben.

              Diese Annahme stützt sich darauf, dass die Methode map in einem anderen Modul ist, und da wir modulweise übersetzen sind übergreifende Optimierungen nicht möglich. In den wenigen Fällen, in dem das im gleichen Modul ist (oder zum Beispiel Link-Time-Optimization verwendet werden), kann es aber passieren, dass er den ersten Schreibzugriff wie oben gezeigt weg optimiert.

              Deswegen: Hier immer die Speicherbarriere verwenden, in den meisten von unseren Fällen hat sie auch keinen Performancenachteil, der Code ist aber auch zukunftskompatibel für “intelligentere” Übersetzer 😉

              Wann invlpg einsetzen?

              Um (Leichtsinns-)Fehler zu vermeiden, empfiehlt es sich, das invalidieren von Einträgen im TLB automatisch bei den entsprechenden Paging-Operationen durchzuführen – sofern notwendig. Ein map zum Einblenden einer neuen Seite müsste entsprechend prüfen, ob es gerade auf dem aktiven virtuellen Adressraum arbeitet (cr3 zeigt auf die PML4, auf welcher das Mapping geändert wird). Ist dies der Fall, so wird nach Abschluss der Änderungen an den Seitentabellen die invlpg Instruktion auf die entsprechende virtuelle Adresse ausgeführt.

              Zurück zur Übersicht

              Friedrich-Alexander-Universität
              Erlangen-Nürnberg

              Schlossplatz 4
              91054 Erlangen
              • Impressum
              • Datenschutz
              • Barrierefreiheit
              • Facebook
              • RSS Feed
              • Xing
              Nach oben