Von Detlef Jantz, tecChannel.de
Mit Überschreiten der GHz-Grenze hat das Rennen um die reine Rechenleis-tung bei PCs an Bedeutung verloren. Durch die ständige Bedroh-ung von Viren und Trojanern ist die Sicherheit und Zuverlässigkeit von Computer-systemen inzwischen zum Hauptinteresse des PC-Anwenders geworden.
Die Fortschritte in der Absicherung eines PCs können mit den Fortschritten in der Rechenleistung jedoch nicht mithalten. Vor fast 20 Jahren hat Intel mit dem 80386 die Basis der gültigen x86-Prozessorarchitektur und deren Sicherheitsmechanismen gelegt. Doch seit 1985 hat es keine echte Weiterentwicklung für eine höhere Systemsicherheit gegeben, bis AMD 2003 mit dem Opteron vorpreschte.
AMD hat in alle Prozessoren der achten Generation ein neues Sicherheitsfeature implementiert: Das so genannte NX- (No-Execution-) Bit verspricht beim Opteron und Athlon 64 einen Schutz vor einem der häufigsten Angriffsvektoren von Schadpro-grammen, dem Buffer Overflow mit Code auf dem Stack. Marketing-gerecht bezeichnet AMD dieses Feature offiziell mit "Enhanced Virus Protection". Auch Intel hat diese Hardware-Erweiterung bei seinen neuen Prozessoren integriert, bezeichnet es aber als XD- (Execution-Disable-)Technologie.
Ausnahmezustand
Das NX-Bit liegt als zusätz- liches Steuerbit in den Deskrip-toren und Sicherungsbits für Speicherseiten. Setzt das Be-triebssystem dieses Bit für den Stack-Bereich, erzeugt das Aus-führen von Code auf dem Stack einen Ausnahmezustand in der CPU und informiert so das Betriebssystem. Ohne dessen Unterstützung bleibt das NX-Feature jedoch wertlos.
Microsofts Windows bietet den zusätzlichen Schutz optional mit dem SP2 für Windows XP und dem SP1 für Windows Ser-ver 2003. Hier sind die Schutzfunk-tionen unter dem Unwort "Datenausführungsver-hinderung" zusammengefasst.
Interessanterweise verbirgt sich dahinter weit mehr als nur das NX-Bit, so dass alle Benut- zer von der erhöhten Sicherheit profitieren können - wenn sie den Schutz nur aktivieren würden. Auch etliche Linux-Distributionen sind inzwischen dazu übergegangen, das NX-Bit zu unterstützen. Höchste Zeit also, sich die zu Grunde liegenden Techniken einmal näher anzusehen und Marketing-Versprechen auf ihren Realitätsgehalt hin zu überprüfen.
Grundlagen des Speichermanagementes
Um zu verstehen, wie das NX-Bit mit den schon vorhandenen Prozessorkomponenten und bestehender Software zusammenarbeitet, ist Basiswissen über die x86-Architektur nötig. Die prinzipiellen Grundlagen, wie Spei-cherschutz und Adressverfahren überhaupt funktionieren, werden im Folgenden im Zusammen-hang mit dem Einsatz bei den Schutzverfahren vorgestellt. Falls Sie diesen komplexen Hinter-grund überspringen möchten, können Sie auf der Seite "Kern-frage NX - Funktionsprinzip" weiterlesen. Möchten Sie noch mehr Grundlagen zur X86-Speicherarchitektur erfahren, empfehlen wir unseren Beitrag x86-Programmierung und -Be-triebsarten.
Die x86-Architektur verwaltet den Hauptspeicher immer in Blöcken mit typischen Größen von 4 KByte bis 4 MByte. Für das Management benötigt die Me-mory-Management-Unit (MMU) im Prozessor daher nicht alle Adressbits der logischen und der linearen Adresse (dazu mehr auf den nächsten Seiten). Vielmehr nutzt sie nur den oberen Teil des so genannten Speicher-Adressbitvektors der kompletten Adresse. Als großer Vorteil reduzie-ren sich dadurch der interne Speicherbedarf für Adressen und der Aufwand bei Adressberech-nungen erheblich.
Diese Speicherblöcke lassen sich ohne die unteren, nicht-signifikanten Bits adressieren und mit speziellen Eigenschaften und Rechten ausstatten. Der Adress-bitvektor wird während seiner Benutzung zu Speicherverwal-tungszwecken in mehrere Teile geschnitten, wie beispielhaft im Bild oben dargestellt.
Diese einzelnen Teile aus einem Bitvektor kann der Prozessor dann an verschiedenen Stellen in unterschiedlichen Kombinatio-nen nutzen. Für das Paging werden beispielsweise im Standard-modus von Windows die unteren 12 Bits nicht berücksichtigt. Hier arbeitet die CPU mit vier KByte-Speicherblöcken als kleinste Struktur. Dieses Verfahren der so genannten Teilbitvektoren ist außer bei Adressen auch in noch feinerer Unterteilung bei den Deskriptoren anzutreffen, dazu später mehr.
Teilbitvektoren adressieren Einträge
Teilbitvektoren führen zum nächsten wichtigen Verfahren, der Look-up- oder Indextabelle. Ein prinzipielles Beispiel mit einem 10-Bit-Teilvektor ist im nächsten Bild dargestellt. Ein Teilbitvektor kann innerhalb einer vorgegebenen Tabelle einzelne Einträge adressieren. Die maximale Größe der Tabelle ergibt sich aus der Länge des Teilbitvektors und der größten damit darstellbaren Adresse. Im Bild findet der Pro-zessor anhand des 10-Bit-Teilbit-vektors mit dem Adresswert 600 in der Tabelle den Eintrag in Zeile 600 und liest deren 32-Bit-Wert 120000 aus.
Dieses Verfahren kommt in einem Prozessor an den unterschiedlichsten Stellen zum Ein-satz, sei es beim Cache-Zugriff, beim Paging oder dem Auslesen von Deskriptoren für Speicher-blöcke.
Für den Speicherschutz in x86-Prozessoren stellen Look-up-Tabellen das wichtigste Grund-prinzip dar. Das Betriebssystem erzeugt für jeden Speicherblock einen Eintrag in einer Tabelle und legt darin die speziellen Eigen-schaften eines Blocks fest. Die Kontrolle erfolgt also nicht für jedes Byte, sondern immer nur für zusammengehörige Bereiche. Bei jedem Zugriff auf eine Speicherstelle ermittelt der Pro-zessor aus den höherwertigen Bits der Adresse den zugehörigen Eintrag in der Schutztabelle. Diesen wertet er aus und prüft die Zugriffsberechtigungen anhand der darin eingetragenen Schutz-bits.
Arten von Speicherzugriff
Die Speicheradressierungen und der dabei mögliche Speicherschutz sind Teilschritte einer festgelegten Reihenfolge innerhalb eines Pro-grammablaufs. Seit seiner Erfin-dung durch J. von Neumann arbeitet ein Prozessor in einer Endlosschleife und führt fortwährend bestimmte Operationsschrit-te aus. Für den Speicherschutz interessieren hier besonders die drei prinzipiell möglichen Arten von Speicherzugriffen.
w Der Prozessor liest ein Code-wort (CR, Code Read) als so genannten ,Fetch‘ und folgt den Anweisungen des Operations- codes.
w Der Prozessor liest Daten zur Bearbeitung ein (DR, Data Read).
w Der Prozessor schreibt Daten in den Speicher zurück (DW, Data Write).
Codewörter schreibt ein Prozessor im Normalfall nicht in den Speicher. Typische Schadpro-gramme nutzen jedoch den gemeinsamen Speicher der Von-Neumann-Architektur, um Da-tenwörter ersatzweise an Stellen zu schreiben, die der Prozessor später als Code interpretiert. Für mehr Schutz im Programmablauf gilt es, dies zu verhindern.
Für die Kodierung der drei möglichen Speicherzugriffsarten wären mindestens zwei Bits zum Definieren einer erlaubten oder verbotenen Aktion in den Index-tabellen der Speicherverwaltung notwendig. Alternativ könnte man auch je ein Bit für jede der drei Aktionen nutzen. Das erhöht zwar den Speicherbedarf geringfügig, erlaubt aber eine einfachere Verifikation beim Speicherzugriff. In der Praxis werden diese einfach zu implementierenden Verfahren aber bei kaum einem Prozessor-design genutzt.
Speicher-Segmentierung
Beim 80386, dem Urahn des heutigen Pentium, hat Intel 1985 die Segmentprüfung mit geschach-telten Zugriffsebenen für den Speicherschutz eingebaut. Neben dieser Weiterentwicklung des 80286-Prozessors bekam er zu-sätzlich eine Paging-Einheit inklusive einer weiteren Kontroll-funktion für Schreib/Lesezugriffe.
Intels Speicherschutz nutzt eine Segmentierung des Arbeits-speichers in Blöcke. Einzelne Speicherstellen darin adressiert der Prozessor über eine Start-adresse des zugehörigen Seg-ments und eine Offset-Adresse innerhalb des Blocks. Die sich aus dem Segment ergebende lineare Adresse rechnet die MMU jedoch nochmals um. Durch dieses nachgeschaltete Paging ist der Einsatz von virtuellem Speicher durch eine Auslagerung auf die Fest-platte möglich. Diese Pages verwaltet der Prozessor wie vorher schon die Segmente in einer Tabelle, die auch die Reihenfolge der Blöcke definiert.
Segment Translation und Page Translation laufen in der MMU aller x86-Prozessoren nacheinander bei jedem Speicherzugriff im so genannten Protected Mode (=geschützten Modus) ab. Die Segmentwandlung lässt sich prinzipiell nicht ausschalten. Das Paging könnte man zwar umgehen, doch gängige x86-Betriebs-systeme wie Windows und Linux verwenden es intensiv zum Spei-chermanagement.
Anwendungsprogramme bekommen von dieser doppelten Umsetzung überhaupt nichts mit. Sie arbeiten intern nur mit logischen Adressen, die aus einem 16-Bit-Selektor und einem 32-Bit-Offset bestehen. Die Segment Translation wandelt die logische zunächst in eine lineare Adresse um. Dazu sucht sie mit dem Selektor als Index in der lokalen oder globalen Deskriptortabelle die passende Segmentbeschrei-bung, den so genannten Segment-deskriptor. Der acht Byte große Deskriptor enthält die lineare Basisadresse sowie die Größe des Segments. Die lineare Adresse ergibt sich dann aus der Basis-adresse plus dem Offset. Durch die Größenangabe im Tabellen-eintrag kann die MMU ermitteln, ob ein Zugriff eventuell außerhalb des Speicherblocks erfolgt und entsprechend eine Exception auslöst.
Page Translation
Die lineare Adresse ist aber noch nicht die physikalische Speicher-adresse, also die Adresse, mit der die CPU Hardwareseitig den Hauptspeicher anspricht. Die physikalische Adresse wird erst durch die Page Translation aus der linearen Adresse berechnet. Dazu dienen die oberen 20 Bits der linearen Adresse wiederum als Indizes in einer Page Directory Tabelle und einer Page Tabelle. Diese Tabellen bestimmen dann die physikalische Lage der Speicherseiten. Die unteren 12 Bits der linearen Adresse durchlaufen den ganzen Prozess unverändert. Sie dienen als Offset innerhalb der 4 KByte großen Seiten und ergeben schlussendlich die komplette physikalische Adresse.
Durch Paging im Standard-modus von Windows und Linux werden die 20 höchstwertigen Adressbits also auf neue Werte abgebildet, während die zwölf niederwertigsten Adressbits als Offset unverändert übernommen werden. Durch Manipulation der Page-Deskriptoren lassen sich Bereiche im Speicher austauschen, ohne Daten umkopieren zu müssen. Zudem enthalten die Page-Deskriptoren zwei Zugriffs-rechtebits. Eines davon erlaubt den Schreibzugriff. Das andere regelt, ob nur das Betriebssystem oder auch Anwendungen auf die Page zugreifen dürfen.
Simulation eines idealen Speichers
Bekannt ist das Paging-Verfahren auch als Virtual Memory. Die linearen Adressen simulieren dabei einen großen, idealen Speicher, der von den Einschränkungen des tatsächlich vorhandenen physischen Speichers entkoppelt ist. Das Prinzip der Page-Deskriptoren erlaubt es auch, Seiten auf die Festplatte auszulagern und den physikalischen Speicher abwechselnd für mehrere Seiten im Bereich der linearen Adresse zu verwenden.
Ursprünglich war dies einmal der wichtigste Anwendungsfall für das Paging. Bei Hauptspei-chergrößen von einigen MByte erwies sich ein Auslagern der 4 KByte großen Seiten auf Fest-platte als sehr effizientes Speicher-management. Mit den derzeit üblichen Hauptspeichergrößen haben sich die Ziele aber gewandelt. Dies zeigen Adresserweiterungen wie PSE (Page Size Extension) und PAE (Physical Address Extension), die mit Seitengrößen von 2 und 4 MByte arbeiten.