x86-Programmierung und -Betriebsarten (Teil 4)

05.02.2004 von Hans-Peter Messmer und Klaus Dembowski
Die sechsteilige Artikelserie behandelt die Speicheradressierung und die x86-Betriebsarten. Im vierten Teil der Serie widmen wir uns sich einer besonderen Spezialität der x86-Prozessoren - dem Protected Mode.

Der Protected Virtual Address Mode oder kurz Protected Mode wurde beginnend mit dem 80286 implementiert, um (wie der Name schon sagt) die verschiedenen Tasks unter einem Multitasking-Betriebssystem zu schützen. Zu diesem Zweck prüft die Prozessor-Hardware den Zugriff eines Programms auf Daten und Codes und vergibt Berechtigungen für einen solchen Zugriff auf insgesamt vier Schutzebenen. Damit sind Daten und Code geschützt und ein kompletter Systemabsturz des PCs ist normalerweise nicht möglich.

In der Praxis wird unter Windows der Beweis angetreten, dass dies dennoch möglich ist. Da dies unter Linux aber nicht der Fall ist, kann man sicher die Frage aufwerfen, ob sich Windows auch konsequent an die (Intel-)Vorschriften des Protected Modes hält.

Die Zugriffsprüfungen im Protected Mode dienen vor allem zur Hardwareunterstützung eines Multitasking-Betriebssystems; typische Vertreter sind OS/2, Linux und Windows NT beziehungsweise Windows 2000.

Serie: x86-Programmierung und -Betriebsarten

Teil 1

Code-/Datensegment, Befehlszähler und Stack

Teil 2

Adressierungsarten und Befehlsdekodierung sowie Real Mode

Teil 3

Interrupts und Exceptions

Teil 4

Der Protected Mode

Teil 5

Paging und die MMU

Teil 6

Der Virtual-8086-Modus

Die Artikelserie basiert auf dem Kapitel 6 des "PC Hardwarebuch" von Addison-Wesley. In unserem Buch-Shop können Sie das über 1200 Seiten starke Kompendium bestellen oder als eBook downloaden.

Segmentselektoren, Segmentdeskriptoren und Privilegierungsstufen

Unter einem Multitasking-Betriebssystem laufen mehrere Programme (Tasks) scheinbar parallel ab. Genauer gesagt werden die einzelnen Tasks vom Betriebssystem in kurzen Zeitabständen aktiviert, laufen eine kurze Zeit ab und werden dann vom Betriebssystem wieder unterbrochen, sodass dieses einen anderen Task aktivieren kann. Im Gegensatz zu den TSR-Programmen unter MS-DOS (beispielsweise PRINT) werden die Programme also gezielt vom Betriebssystem aufgerufen, TSR-Programme dagegen aktivieren sich selbst, indem sie beispielsweise den periodischen Timer-Interrupt abfangen. In einem Multitasking-Betriebssystem wird also häufig in kurzer Zeit zwischen den einzelnen Tasks umgeschaltet, das heißt es wird ein Task Switch ausgeführt, sodass der Benutzer den Eindruck hat, die Programme würden parallel arbeiten. Durch die Hardware-Unterstützung der CPUs im Protected Mode können diese Task-Switches schnell und effektiv ausgeführt werden.

Ein weiterer wesentlicher Unterschied des Protected Modes gegenüber dem Real Mode ist die andersartige Berechnung der linearen Adresse eines Speicherobjekts. In den folgenden Kapiteln wollen wir uns den Besonderheiten des Protected Modes im Detail zuwenden.

Wie beschrieben, multipliziert die Adressierungseinheit der CPU im Real Mode den Wert des Segmentregisters einfach mit dem Wert 16, um die lineare Basisadresse des betreffenden Segments zu ermitteln. Die Adresse innerhalb eines solchen 64 KByte-großen Segments wird dann durch den Offset in einem Vielzweckregister angegeben. Im Protected Mode werden die Werte in den Segmentregistern völlig anders interpretiert, sie stellen keine Basisadressen, sondern so genannte Selektoren dar.

Segmentselektor

Der Selektor ist wie im Real Mode 16 Bit lang und wird in einem 16-Bit-Segmentregister gespeichert. Die beiden niederwertigsten Bits 0 und 1 geben die Selektor- oder geforderte Privilegierungsstufe an (RPL: Requested Privilege Level), ab der ein Programm auf das Segment zugreifen darf - wir sind hier erstmals mit einer Zugriffsprüfung des Protected Modes konfrontiert.

Der Wert des Feldes RPL im Segmentselektor des Code-Segments CS wird als gegenwärtige Privilegierungsstufe oder Current-Privilege-Level (CPL) bezeichnet, da dem gegenwärtig aktiven Programmcode diese Privilegierungsstufe zugewiesen ist. Das aktive Programm kann also auf Datensegmente zugreifen, deren Wert der Privilegierungsstufe gleich oder größer als CPL ist. Dabei kann der Wert von RPL größer als CPL sein, das heißt auf das vom Selektor festgelegte Segment wird mit einer geringeren Privilegierungsstufe zugegriffen. Der größere der beiden Werte CPL und RPL definiert die effektive Privilegierungsstufe (EPL) des Tasks.

Der Prozessor (ab 80286) kennt insgesamt vier Privilegierungsstufen (PL) 0 bis 3. Die Null kennzeichnet die höchste und die drei die niedrigste Stufe. Beachten Sie also, dass ein größerer Wert eine niedrigere Privilegierung und umgekehrt bedeutet. Ein RPL mit dem Wert 0 schränkt also die Privilegierungsstufe eines Tasks nicht ein, wohingegen ein Selektor mit einem RPL von 3 unabhängig vom CPL nur Segmente ansprechen kann, die eine Privilegierungsstufe von 3 aufweisen.

Programme mit niedrigerer Privilegierungsstufe (höherem CPL) dürfen nur in Ausnahmefällen auf Segmente mit höherer Stufe (kleinerem CPL) zugreifen. Hierzu dienen so genannte Gates (Tore). Damit wird der Schutz (Protection) innerhalb eines Tasks durch die verschiedenen Privilegierungsstufen vermittelt.

Priviligierungsstufen

Um die verschiedenen Tasks in einer Multitasking-Umgebung vor Fehlern in einem anderen Task zu schützen, wird jedem Task eine von vier Privilegierungsstufen zugeordnet. Der Betriebssystem-Kernel besitzt die höchste Privilegierung PL=0, Anwendungsprogramme dagegen die geringste Stufe PL=3. Ein Sprung zu einem Task höherer Privilegierungsstufe ist nur über ein Gate möglich.

Die höchste Privilegierungsstufe 0 besitzt üblicherweise der kritische Teil oder Kernel des Betriebssystems. Dieser umfasst zumeist die Routinen zur Speicherverwaltung, dem Ausführen von Task-Switches, die Behandlung von kritischen Fehlern et cetera. Viele Befehle, die den Status des Prozessors oder Computers direkt betreffen, wie beispielsweise LGDT (globale Deskriptortabelle laden) oder LMSW (MSW laden) können im Protected Mode nur von einem Programm ausgeführt werden, das die Privilegierungsstufe 0 aufweist. Damit soll verhindert werden, dass Anwendungsprogramme durch einen Programmierfehler das Betriebssystem zerstören oder Hacker Zugriff auf Daten bekommen.

Betriebssysteme verwalten aber den Computer nicht nur, sondern stellen auch bestimmte Funktionen zur Datenverwaltung, Zeichenausgabe et cetera zur Verfügung. In einem PC unter DOS geschieht dies beispielsweise durch den Interrupt 21h, dem DOS-Funktionsverteiler. Solche Betriebssystemfunktionen laufen meist mit PL=1. Auch Einheiten- und Gerätetreiber (beispielsweise zum Ansteuern der Schnittstellen und Laufwerke) arbeiten häufig auf dieser Stufe. Weniger kritische Betriebssystemfunktionen, zum Beispiel Unterstützungen für eine grafische Benutzeroberfläche (API), können dagegen die Stufe 2 aufweisen.

Die niedrigste Privilegierungsstufe aller Tasks besitzen Anwendungsprogramme, da diese den Computer nur benutzen, nicht aber steuern und kontrollieren sollen. Durch die niedrige Stufe (hohen PL) sind die Daten und Codes anderer Programme und des Betriebssystems sehr gut gegen Programmfehler geschützt. Unter DOS führt beispielsweise ein Programmierfehler, der versehentlich die Interrupt-Vektortabelle überschreibt, zu einem völligen Systemabsturz.

Im Protected Mode reagiert das Betriebssystem auf einen solchen Vorgang unter Ausgabe einer Fehlermeldung nur mit einem Abbruch des fehlerhaften Programms, alle anderen Tasks laufen unbeschädigt und unbeeinflusst weiter. Damit können Bugs, das heißt Programmfehler, besser entdeckt werden. Dies alles setzt natürlich voraus, dass das Betriebssystem (beispielsweise OS/2, Linux oder Windows NT) fehlerfrei ist, was wegen der Komplexität von Multitasking-Betriebssystemen leider keine Selbstverständlichkeit darstellt.

Tasks und Gates

In der folgenden Beschreibung tritt öfters der Begriff Task auf. Zunächst also ein paar erklärende Worte zu diesem Begriff. Ein Task umfasst das zugeordnete Programm (zum Beispiel Word), dessen Daten (den Text) sowie die erforderlichen Systemfunktionen (zum Beispiel die zur Datenverwaltung auf Festplattenebene). Unter Windows können Sie zum Beispiel Word für Windows starten und einen Text laden. Um eine weitere Textdatei zu bearbeiten, können Sie einmal den Text in das bereits gestartete Word laden und zwischen den Word-Fenstern wechseln. Sie haben zwei Texte in ein Programm geladen, Word und die beiden Texte bilden einen Task. Eine andere Möglichkeit zur Bearbeitung des zweiten Textes ist, Word nochmals zu starten und ihm unter Windows ein zweites Fenster zuzuweisen. Den zweiten Text laden Sie in die zuletzt gestartete Word-Version. Auch in diesem Fall können Sie zwischen zwei Fenstern hin- und herspringen. Der große Unterschied zu vorher besteht aber darin, dass Sie nun zwei Programme gestartet haben, nämlich zweimal Word, und in jedes der Programme einen Text geladen haben. Jedes Programm bildet zusammen mit dem zugehörigen geladenen Text einen Task. Obwohl Sie beides mal dasselbe Programm gestartet haben, laufen in ihrem PC zwei Tasks ab.

Ab dem 80386 wird für jede Privilegierungsstufe PL=0 bis PL=3 eines Tasks ein eigenes Stack vorgesehen. Für das angeführte Beispiel wäre also jeweils ein Stack für das Anwendungsprogramm Word (PL=3), die Funktionen der Benutzeroberfläche (PL=2), die Betriebssystemfunktionen zur Datenverwaltung (PL=1) und den Kernel (PL=0) vorhanden.

Für einen kontrollierten Zugriff von Programmen auf Daten und Code in Segmenten höherer Privilegierungsstufe stehen Gates zur Verfügung. Diese geben einen maximalen Schutz vor einem unberechtigten oder fehlerhaften Zugriff auf fremde Daten und Programme. Wenn zum Beispiel ein Anwendungsprogramm Betriebssystemfunktionen in Anspruch nimmt, um Dateien zu öffnen oder zu schließen - es also auf fremde Funktionen zugreift - garantieren die Gates, dass der Zugriff fehlerfrei verläuft. Würde das Anwendungsprogramm versuchen, die Funktionen mit einer falschen Einsprungadresse aufzurufen, so wäre ein unvorhersehbares Verhalten des Computers wahrscheinlich. Die Gates definieren daher "Tore", durch die das Anwendungsprogramm Zugriff auf fremde Routinen hat.

Das Bit 2 im Segmentselektor (Abbildung 6.4) gibt als so genannter Tabellenindikator (TI) an, ob die globale (TI=0) oder lokale (TI=1) Deskriptortabelle für die Lokalisierung des Segments im Speicher benutzt werden soll. Diese beiden Tabellen sind wesentlich für die Segmentierung des Speichers im Protected Mode. Der Prozessor verwendet im Protected Mode die Segmentselektoren in den Segmentregistern nämlich als Index in die globale oder lokale Deskriptortabelle.

Segmentdeskriptor

Die globale Deskriptortabelle ist eine Liste im Speicher, die in Form von Segmentdeskriptoren Größe und Adresse von Segmenten im Speicher beschreibt. Der Aufbau eines solchen Segmentdeskriptors ist in Abbildung 6.6 dargestellt. Jeder Deskriptor umfasst acht Byte. Diese Deskriptortabelle wird als global bezeichnet, weil sie Segmente beschreibt, die üblicherweise allen Tasks zur Verfügung stehen (wenn deren Privilegierungsstufen oder entsprechende Gates einen Zugriff gestatten). Auch die lokale Deskriptortabelle ist eine Liste mit gleichartig aufgebauten Segmentdeskriptoren. Im Gegensatz zur globalen steht die lokale Deskriptortabelle aber normalerweise nur dem gerade aktiven Task zur Verfügung, das heißt bei einem Task-Wechsel wird auch die lokale Deskriptortabelle gewechselt.

Die 32 Basisbits des Segmentdeskriptors geben die Startadresse des beschriebenen Segments im Speicher an (siehe Bild "Basis und Limit der Segmentdeskriptoren"). Diese 32 Bit der Basisadresse entsprechen den 32 Adressleitungen. Damit können im Protected Mode 232 Byte oder 4 GByte adressiert werden. Der Basiseintrag eines Segmentdeskriptors gibt die Startadresse des betreffenden Segments in diesem Adressraum an.

Im Real Mode ist jedes Segment 64 KByte groß, selbst wenn nur wenige Daten in einem Segment gespeichert werden. Durch die festgelegte Verzahnung der Segmente bleiben aber höchstens 15 Byte ungenutzt, da die Segmente mit 16 Byte Abstand aufeinander folgen. Die völlige Entkopplung der Segmente im Protected Mode ändert das vollkommen, weil nun entsprechend dem Selektorwert im Segmentregister aufeinanderfolgende Segmente in keiner Weise auch physikalisch im Speicher aufeinanderfolgen müssen. Jeder Segmentdeskriptor weist daher einen Eintrag Limit im niederwertigsten Byte des Deskriptors auf, um die tatsächliche Größe des Segments in Byte zu definieren (siehe Bild "Basis und Limit der Segmentdeskriptoren"). Bei gelöschtem G-Bit (Byte-Granularität) sind durch die 20 Bit des Limiteintrags im Protected Mode also Segmente mit einer maximalen Größe von 2^20*1 Byte (=1 MByte) möglich, bei gesetztem G-Bit (Page-Granularität) hingegen Segmente mit einer Größe von bis zu 2^20*4 KByte (=4 GByte). Das tatsächliche Segmentlimit (oder gleichbedeutend die Größe) des vom Deskriptor beschriebenen Segments hängt also neben dem 20-Bit-Limiteintrag noch vom Granularity-Bit G ab. Beispiel:

Limit=1000, G=0: Segmentlimit 1000 Byte
Limit=1000, G=1: Segmentlimit 1000*4 KByte=4.096.000Byte

OS/2 und auch Windows verwenden aber ein so genanntes flaches Speichermodell, bei dem ein einziges 4 GByte-großes Segment alle Tasks aufnimmt, das heißt die Segmentierung spielt hier fast keine Rolle mehr. Stattdessen wird eine Speicheraufteilung in Pages vorgenommen, für die weitere Schutzmechanismen existieren, denen wir uns im nächsten Teil der Artikel-Serie widmen.

Segmentdeskriptor im Detail

Das Bit DT im Segmentdeskriptor gibt an, um welchen Deskriptortyp es sich handelt. Ist DT gleich 0, so beschreibt der Deskriptor ein Systemsegment, ansonsten ein Applikationssegment. Das Feld Typ im Segmentdeskriptor gibt die Art und DPL die Privilegierungsstufe (von 0 bis 3) des Segments an. DPL wird auch als Deskriptorprivilegierungsstufe bezeichnet. Schließlich ist das Bit P ein Indikator dafür, ob sich das Segment auch tatsächlich im Speicher befindet. Das Bit vf im Segmentdeskriptor kann der Anwender oder das Betriebssystem frei benutzen und kommt nicht für spezielle CPU-Funktionen zum Einsatz.

Sie erinnern sich, dass die 32-Bit-Offsetregister EAX et cetera zu 16-Bit-Registern (zum Beispiel AX) und weiter zu 8-Bit-Teilregistern (zum Beispiel AH, AL) verkürzt werden können. Außerdem arbeitet natürlich jeder Programmcode, der für die 16-Bit-Prozessoren 8086 und 80286 entwickelt worden ist, ausschließlich mit 16-Bit-Adressen. Aus Kompatibilitätsgründen muss ein 32-Bit-Prozessor aber auch solchen Programmcode neben neu erstelltem 32-Bit-Programmcode verarbeiten können. Dazu ist das Bit DB wichtig. Es gibt an, ob der Prozessor das vom Deskriptor beschriebene Segment standardmäßig 16- oder 32-Bit-Operanden im Fall eines Datensegments beziehungsweise 16- oder 32-Bit-Adressen für ein Codesegment verwenden soll. Ist DB=0, so benutzt die CPU 16-Bit-Operanden oder 16-Bit-Adressen. Beachten Sie, dass in den angegebenen Standardwerten für einen 80286-Segmentdeskriptor das Bit DB stets gleich 0 ist. Der 80286-Code kann daher ohne Probleme auf die 32-Bit-Architektur eines 80386 übertragen werden, da er alle Operanden und Adressen standardmäßig als 16-Bit-Größen behandelt. Auch die anderen 80286-Standardwerte tragen der 16-Bit-Architektur mit 24-Bit-Adressbus Rechnung.

Operanden und Adressen können bei gelöschtem Bit DB aber durch ein so genanntes Operandengrößenpräfix 66h beziehungsweise ein Adressgrößenpräfix 67h für einen bestimmten Befehl auf 32 Bit erweitert werden. Das Operandengrößenpräfix kehrt nämlich die Größenfestlegung durch das DB-Bit des gültigen Datensegments, das Adressgrößenpräfix die Festlegung durch das DB-Bit des Codesegments um. Ist DB gelöscht, das heißt das betreffende Segment arbeitet mit 16-Bit-Größen (Operanden beziehungsweise Adressen), so zwingen die Präfixe den 32-Bit-Prozessor statt der üblichen 16-Bit-Größen eine 32-Bit-Größe zu verwenden.

Ist das DB-Bit dagegen gesetzt, so verwendet das Segment standardmäßig 32-Bit-Größen. Ein Operanden- oder Adressgrößenpräfix zwingt den 32-Bit-Prozessor dann, anstelle der 32-Bit-Größen einen 16-Bit-Operanden beziehungsweise eine 16-Bit-Adresse zu verwenden. Durch das DB-Bit und die Präfixe benötigt der Prosessor keine unterschiedlichen Opcodes für gleichartige Befehle wie zum Beispiel MOV ax, mem16 und MOV eax, mem32, die einmal eine 16- und einmal eine 32-Bit-Größe verarbeiten. Das vermindert die Anzahl der notwendigen Opcodes, um einen umfangreichen Befehlssatz zu implementieren und somit auch den durchschnittlichen Kodierungsaufwand. Diese Präfixe führen also genauso wie die Segmentüberschreibungspräfixe zu kompakteren Programmen.

Beispiel

Der Opcode für die beiden Befehle MOV eax, [01h] und MOV ax, [01h] unterscheidet sich nicht, er lautet beidesmal 10111000. Der 32-Bit-Prozessor unterscheidet die beiden anhand von DB-Bit und Operandengrößenpräfix. Angenommen, das DB-Bit im Codesegmentdeskriptor ist gelöscht, das heißt der Prozessor verwendet standardmäßig 16-Bit-Displacements. Bei gelöschtem DB-Bit im Datensegmentdeskriptor muss ein Assembler im ersten Fall also das Operandengrößenpräfix 66h vor dem Opcode einfügen. Wir erhalten dadurch die folgenden Befehlskodierungen:

MOV eax, 01h: 66 b8 00 01

Der Befehl überträgt den 32-Bit-Wert bei Offset 01h in den 32-Bit-Akkumulator eax.

MOV ax, 01h: b8 00 01

Der Befehl überträgt den 16-Bit-Wert bei Offset 01h in den 16-Bit-Akkumulator eax.

Ist DB im betreffenden Datensegmentdeskriptor dagegen gesetzt, so fügt der Assembler im zweiten Fall das Operandengrößenpräfix 66h vor dem Opcode ein, weil der Prozessor jetzt standardmäßig 32-Bit-Größen verwendet. Die Kodierungen lauten:

MOV eax, 01h: b8 00 01

Der Befehl überträgt den 32-Bit-Wert bei Offset 01h in den 32-Bit-Akkumulator eax.

MOV ax, 01h: 66 b8 00 01

Der Befehl überträgt den 16-Bit-Wert bei Offset 01h in den 16-Bit-Akkumulator eax.

Globale und lokale Deskriptortabelle

Für die Verwaltung der lokalen und globalen Deskriptortabelle sowie der weiter unten beschriebenen Interrupt-Deskriptortabelle und der Tasks werden ab dem 80386 mindestens fünf Register implementiert, die Sie in Abbildung 6.8 sehen. Das sind das Steuerregister CR0 und die vier Speicherverwaltungsregister Task-Register (TR) sowie die Register für die lokale Deskriptortabelle (LDTR), die Interrupt-Deskriptortabelle (IDTR) und die globale Deskriptortabelle (GDTR).

Der Index, das heißt die höherwertigen 13 Bit des Segmentselektors (siehe Bild "Register im Detail") geben nun die Nummer des Segmentdeskriptors in der Deskriptortabelle an, der das zugehörige Segment beschreibt. Mit 13 Bit sind maximal 8192 verschiedene Indizes möglich, sodass die globale und lokale Deskriptortabelle jeweils maximal 8192 Einträge zu 8 Byte oder 64 KByte umfassen können. Die Tabellen beschreiben damit jeweils bis zu 8192 verschiedene Segmente. Aufbau und Größe der Segmentdeskriptoren für die lokale Deskriptortabelle (LDT) und die globale Deskriptortabelle (GDT) stimmen überein. Ob sich der Segmentselektor in einem Segmentregister auf die GDT oder die LDT bezieht, gibt ja der Tabellenindikator TI im Selektor an. Möchte der Prozessor auf einen Eintrag in der GDT oder LDT zugreifen, multipliziert er den Indexwert des Selektors mit 8 (Anzahl der Byte je Deskriptor) und addiert das Ergebnis zur Basisadresse der entsprechenden Deskriptortabelle.

Das niederwertige Wort des Steuerregisters ist bereits beim 80286 als Maschinenstatuswort (MSW) vorhanden und kann beim 80386 aus Kompatibilitätsgründen genauso adressiert werden. Das höchstwertige PG-Bit aktiviert die Paging-Einheit oder legt sie still. Dazu mehr im nächsten Teil der Artikelserie. Die Bedeutung der Bits des Steuerregisters CR0 haben wir bereits im Zusammenhang mit den Steuerregistern erläutert.

Von besonderer Bedeutung für den Protected Mode ist das Bit PE (Protection Enable). Wenn Sie es auf 1 setzen, schaltet die CPU sofort in den Protected Mode um. Löschen können Sie es entweder explizit durch einen Befehl MOV CR0, xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxx0b, einen Prozessor-Reset oder einen Triple-Fault des Prozessors.

GTD- und LDT-Register

Die Basisadresse der globalen und lokalen Deskriptortabelle ist im GDT- beziehungsweise LDT-Register abgelegt. Diese Register werden von der Laderoutine des Betriebssystems (GDTR) oder dem Betriebssystem selbst (LDTR) mit den entsprechenden Werten geladen. Im Gegensatz zur LDT darf beim Aufbau der GDT der nullte Eintrag (beginnend bei der Basisadresse der GDT) nicht benutzt werden. Ein Bezug auf den nullten Eintrag führt sofort zu einer Exception "allgemeiner Protection-Fehler". Dadurch wird verhindert, dass ein noch nicht initialisiertes GDTR benutzt wird.

Wesentlich an der GDT ist, dass die gesamte Segment- und damit Speicherverwaltung aus ihr aufgebaut wird. Im GDTR (Bild "Register im Detail") sind sowohl die Basisadresse als auch das Limit (die Größe der GDT in Byte) der globalen Deskriptortabelle gespeichert - das GDTR weist somit auf die GDT im Speicher. Im Gegensatz dazu verwaltet der Prozessor die lokale Deskriptortabelle dynamisch, wodurch mehrere LDTs möglich sind (dagegen ist nur eine einzige GDT vorhanden). Für jede lokale Deskriptortabelle existiert ein Eintrag in der GDT, die LDTs werden dadurch ähnlich wie Segmente verwaltet (siehe Abbildung "Globale und lokale Deskriptortabelle"). Das GDTR enthält also einen Segmentdeskriptor, das LDTR aber einen Segmentselektor.

Das GDTR kann durch den Befehl LGDT mem64 mit dem Segmentdeskriptor mem64 geladen werden. Dieser Schritt ist notwendig, bevor der Betriebssystemlader den Prozessor in den Protected Mode umschaltet. Ansonsten hängt die Speicherverwaltung in der Luft. Demgegenüber wird das LDTR über den Befehl LLDT reg16 oder LLDT mem16 mit einem Segmentselektor geladen. Dieser Segmentselektor gibt den Eintrag in der globalen Deskriptortabelle an, der den Deskriptor für die lokale Deskriptortabelle enthält.

Implementierung im Betriebssystem

Die konsistente Verwaltung der Deskriptortabellen ist alleinige Aufgabe des Betriebssystems (Linux, Windows NT et cetera), der Anwendungsprogrammierer hat keinerlei Einwirkungsmöglichkeit auf diesen Vorgang. Die Befehle zum Laden der Deskriptortabellenregister LDTR und GDTR müssen im Protected Mode von einem Task mit Privilegierungsstufe 0 ausgeführt werden, üblicherweise also vom Kernel des Betriebssystems. Das verwundert auch nicht, weil die Speicherverwaltung eine ureigene Aufgabe des Betriebssystems ist. Alle Versuche, die Deskriptortabellenregister von einem Anwendungsprogramm mit PL=3 aus zu verändern, führen zu einer Exception "allgemeiner Protection-Fehler" und damit zu einem Interrupt 0dh.

Das Betriebssystem liefert für einen Task sowohl die globale als auch eine lokale Deskriptortabelle. Günstig ist es, von mehreren Tasks gemeinsam benutzte Segmente (wie beispielsweise Segmente mit Betriebssystemfunktionen) in der GDT und ausschließlich vom jeweiligen Task belegte Segmente (zum Beispiel Programmcode und Programmdaten) in der LDT zu beschreiben. Mit diesem Verfahren können die verschiedenen Tasks voneinander isoliert werden und es stehen für einen Task maximal zwei vollständige Deskriptortabellen a 8192 Einträge oder 16.384 Segmente zur Verfügung, wobei der nullte Eintrag in der GDT nicht benutzt wird. Das führt zu einem Maximum von 16.383 Segmenten.

Jedes Segment kann bis zu 1 MByte (Granularity-Bit gleich 0) oder 4 GByte (G-Bit gesetzt) umfassen. Wir erhalten damit einen maximalen logischen oder virtuellen Adressraum von 16 GByte beziehungsweise 64 TByte pro Task. Dieser Wert ist kann wesentlich größer sein als der physikalische Adressraum des Prozessors mit 4 GByte.

GDT und LDT im Detail

Schon bei Byte-Granularität mit einem virtuellen Adressraum von 16 GByte können nicht mehr alle Segmente im Speicher vorhanden sein. Wird ein Segment beispielsweise vom Betriebssystem auf die Festplatte ausgelagert, so setzt dieses das P-Bit im Segmentdeskriptor der entsprechenden Deskriptortabelle LDT oder GDT auf 0. Möchte der Prozessor auf dieses Segment zugreifen, so löst die Hardware eine Exception "Segment nicht vorhanden", entsprechend Interrupt 0bh, aus.

Der aufgerufene Interrupt-Handler kann dann das gewünschte Segment erneut in den Speicher laden, wobei er u.U. ein anderes Segment auslagert. Die CPU-Hardware unterstützt dieses Swapping oder den Swap-Vorgang, indem es die Exceptions auslöst. Das eigentliche Laden und Auslagern der Segmente muss aber das Betriebssystem erledigen. Der Prozessor liefert also nur einen Trigger (nämlich die Exception) dafür. Der große virtuelle Adressraum setzt sich dann physikalisch aus dem kleinen internen Hauptspeicher (den RAM-Chips) und einem großen externen Massenspeicher (der Festplatte) zusammen.

Umschalten in den Protected Mode

Soll der Prozessor in den Protected Mode umgeschaltet werden, muss das Betriebssystem oder das ROM-BIOS die erforderlichen Tabellen im Speicher aufbauen und wenigstens die beiden Register GDTR und IDTR mit geeigneten Werten initialisieren. Ist das geschehen, so setzt das System oder das ROM-BIOS über den Befehl MOV CR0, wert das Bit PE im Steuerregister CR0.

Der Prozessor arbeitet nun im Protected Mode. Außerdem kann die CPU auch über die Funktion 89h des Interrupts INT 15h in den Protected Mode umschalten. Aus Kompatibilitätsgründen mit dem 80286 kann eine 32-Bit-CPU zudem über den 80286-Befehl LMSW (Lade Maschinenstatuswort) in den Protected Mode versetzt werden. Beachten Sie aber, dass LMSW nur das niederwertige Wort des Steuerregisters CR0 anspricht. Im Gegensatz zu MOV CR0, wert können Sie also zum Beispiel das Paging (Kapitel 6.8) nicht aktivieren.

Der Protected Mode kann auf einfache Weise verlassen werden, indem ein Task mit PL=0 (also der höchsten Privilegierungsstufe) das PE-Bit durch einen Befehl MOV CR0, wert löscht. Der Prozessor schaltet dann sofort in den Real Mode zurück. Beim 80286 ist das nicht möglich.

Speicheradressierung im Protected Mode

Im Real Mode war die Ermittlung einer linearen Speicheradresse ganz einfach: Der Wert des entsprechenden Segmentregisters wurde mit 16 multipliziert und in einem Addierer dem Offset hinzugezählt. Im Protected Mode ist dieser Vorgang erheblich umfangreicher. Es laufen folgende Schritte ab (siehe auch Bild "Ablauf"):

Durch die Segmentdeskriptor-Cache-Register müssen in den oben aufgeführten Schritten die Deskriptoren meistens nicht eingelesen werden. Vielmehr sind deren Werte bereits nach einem ersten Zugriff auf das Segment in den Cache-Registern, die die CPU automatisch verwaltet und für den Anwender nicht sichtbar sind, gespeichert. Alle späteren Zugriffe können dann diese On-Chip-Werte benutzen. Der beschriebene Weg gibt aber das logische Vorgehen des Prozessors an, wenn er eine lineare (oder auch physikalische) Adresse ermittelt. Da alle Prüfungen parallel mit den Berechnungen erfolgen und außerdem alle wichtigen Daten in den Cache-Registern bereitstehen, verursacht diese umfangreiche Berechnungs- und Prüfprozedur keine Verzögerung gegenüber der einfachen Adressberechnung im Real Mode. Mit Ausnahme der Befehle, die explizit oder implizit einen neuen Segmentdeskriptor laden, werden alle Speicherzugriffe im Protected wie im Real Mode gleich schnell ausgeführt.

Steuerungsübergabe und Call Gates

Bei einem Near-Call oder einem Near-Sprung wird die Steuerung an eine Prozedur oder einen Sprungpunkt übergeben, die beziehungsweise der sich im gleichen Segment wie der entsprechende CALL- oder JMP-Befehl befindet. Ein solcher Transfer verändert also nur den Wert des Befehlszählers EIP und der Prozessor prüft lediglich, ob EIP das Limit des Segments übersteigt. Ist der Offset gültig, so wird der Aufruf beziehungsweise Sprung ausgeführt, ansonsten löst die CPU eine Exception "allgemeiner Protection-Fehler" aus.

Tasks bestehen nun aber selten aus nur einem Codesegment. In der Regel sind mehrere Codesegmente vorhanden. Ein Zugriff auf ein anderes Codesegment innerhalb des Tasks findet beispielsweise bei einem Far-Call, einem Far-Sprung oder einem Interrupt statt. In allen drei Fällen wird das Codesegment mit einem neuen Segmentselektor geladen. Im Real Mode werden bei einem solchen Intersegment-Aufruf einfach der Befehlszähler EIP und das Codesegment CS mit neuen Werten geladen, die den Einsprungpunkt der Routine angeben. Im Protected Mode ist dies etwas komplizierter, schließlich verlangt das Laden eines Codesegments eine umfangreiche Prüfprozedur.

Für einen Far-Call oder Far-Sprung stehen drei Möglichkeiten zur Verfügung:

In den ersten beiden Fällen lädt der Prozessor einfach den Zielsegmentselektor in das Register CS und den neuen Befehlszählerwert in EIP und fährt dann mit der Programmausführung fort. Das ist (mit Ausnahme der Überprüfungen) einem Far-Aufruf oder Far-Sprung im Real Mode ähnlich. Im letzten Fall zeigt der neue Segmentselektor nicht auf das Zielsegment selbst, sondern auf ein so genanntes Call-Gate.

Die Behandlung von Interrupts ist im Allgemeinen eine ureigene und auch kritische Aufgabe des Betriebssystem-Kernels, weil Interrupts den Computer unmittelbar beeinflussen. Ein Interrupt-Aufruf führt dadurch meist zu einer Änderung der Privilegierungsstufe (zum Beispiel wenn ein Anwendungsprogramm mit PL=3 durch einen Interrupt unterbrochen und ein Interrupt Handler im Kernel mit PL=0 aufgerufen wird). Der Interrupt muss daher ein Interrupt oder Trap Gate benutzen, um den Interrupt Handler zu aktivieren (siehe unten). Die Bedeutung der Task-Gates wird im Abschnitt über Multitasking erläutert.

Die Call, Interrupt und Trap Gates bilden "Tore" für den Einsprung in eine Routine eines anderen Segments mit anderer Privilegierungsstufe. Gates werden durch ein Bit DT=0 im Segmentdeskriptor und einen Wert des Typfeldes von 4 bis 7 und 12 bis 15 definiert. Sie sind also Teil der Systemsegmente. In Tabelle 6.2 finden Sie die gültigen Gate-Diskriptoren.

Systemsegment- und Gate-Typen

Typ

Bedeutung

Typ

1-7

für 80286

Systemsegment bzw. GateGate

8

reserviert

9

verfügbares TSS

Systemsegment

10

reserviert

11

aktives TSS

Systemsegment

12

Call-Gate

Gate

13

reserviert

14

Interrupt-Gate

Gate

15

Trap-Gate

Gate

Die Tabelle zeigt Systemsegment- und Gate-Typen (DT=0), die für 32-Bit-CPUs gelten. Die Typen 1-7 sind für die Abwärtskompatibilität mit dem 80286 vorgesehen. In Abbildung 6.11 finden Sie das Format der Gate-Deskriptoren. Call Gates werden nicht nur für Prozedur-Aufrufe über einen Far-Call, sondern auch für alle unbedingten und bedingten Sprunganweisungen mit einem Far-Jump verwendet. Call Gates dürfen in der lokalen oder globalen Deskriptortabelle, nicht aber in der Interrupt-Deskriptortabelle auftreten. Dort sind nur Interrupt, Trap und Task Gates erlaubt.

Gate-Deskriptor

Wie das Bild "Gate-Deskriptor" bereits auf den ersten Blick zeigt, unterscheidet sich der Aufbau eines Gate-Deskriptors ganz erheblich von dem eines "normalen" Segmentdeskriptors: Es fehlt zum Beispiel die Basisadresse des Segments. Stattdessen ist ein 5-Bit-Feld DWord-Count vorgesehen, und die Bits 5 bis 7 im zweiten Deskriptordoppelwort sind auf 0 gesetzt. Außerdem ist das zweite Wort für einen Segmentselektor reserviert. Er definiert das Zielsegment für den Far-Aufruf oder Far-Sprung und gibt zusammen mit dem Offset im niederwertigen und höchstwertigen Wort die Einsprungadresse an.

Damit werden bei einem Far-Call über ein Call-Gate zwei Segmentdeskriptorreferenzen ausgeführt (siehe Bild "Far-Call"): die erste, um den Gate-Deskriptor zu laden und die zweite, um die Basisadresse des betreffenden Segments zu ermitteln. Das Gate enthält ja wiederum nur einen Zielsegmentselektor, nicht aber dessen lineare Adresse. Die Adressierungseinheit addiert die Basisadresse des durch den Segmentselektor im Gate-Deskriptor festgelegten Zielsegments und den im Gate-Deskriptor angegebenen Offset. Der ermittelte Wert stellt die lineare Einsprungadresse dar.

Der Prozessor erkennt am Eintrag im Typfeld, ob der Zielsegmentselektor für das CS-Register bei einem Far-Call oder Far-Sprung direkt ein Code-Segment oder einen Gate-Deskriptor darstellt. Im ersten Fall prüft der Prozessor, ob der direkte Aufruf erlaubt ist (ob zum Beispiel das Zielsegment als Conforming gekennzeichnet ist) und führt ihn abhängig davon aus oder erzeugt eine Exception. In letzterem Fall lädt er dagegen zunächst den Segmentdeskriptor, der im Call Gate angegeben ist.

Sinn und Zweck dieses Vorgehens liegen auf der Hand: Es ist ein exakt definierter Einsprungspunkt vorgegeben, sodass das aufrufende Programm versehentlich keinen falschen Einsprungpunkt angeben kann. Das ist besonders wichtig, wenn Funktionen des Betriebssystems aufgerufen werden: Ein falscher Einsprungpunkt in diese Routinen führt gewöhnlich zu einem totalen Systemabsturz, die Angabe eines falschen Gates dagegen nur zum Abbruch des Tasks und der Ausgabe einer Fehlermeldung.

Datenaustausch zwischen Stacks

Wir haben bereits erwähnt, dass jeder Task für die vier verschiedenen Privilegierungsebenen jeweils einen eigenen Stack anlegt. Zwischen diesen Stacks müssen natürlich häufig Daten ausgetauscht werden, damit die Routine einer anderen Stufe Zugriff auf die Daten des aufrufenden Programms hat. Um diesen Zugriff zu ermöglichen, trägt das System oder der Compiler/Assembler in das Feld DWord-Count die Anzahl der zu kopierenden Doppelworte (a vier Byte) ein. Der Prozessor überträgt diese Doppelworte dann bei einem Aufruf des Call-Gates automatisch vom Stack der aufrufenden zum Stack der aufgerufenen Prozedur. Mit fünf Bits lassen sich so maximal 31 Doppelworte, das heißt 124 Byte übergeben.

Aus Kompatibilitätsgründen kann ein 32-Bit-Prozessor auch 80286-Gates (Typ 1-7) verarbeiten. Die 80286-Gates unterscheiden sich nur darin, dass die Bits 31 bis 16 für den Zieloffset des Aufrufs oder Sprungs reserviert (das heißt gleich 0) sind und das Feld DWord-Count keine Doppelworte, sondern gewöhnliche 16-Bit-Worte angibt. Grund für diese Einschränkungen ist die 16-Bit-Architektur des 80286. Trifft der 80386 auf ein 80286-Gate, dann interpretiert er das Feld DWord-Count als Word-Count und kopiert nur entsprechend viele Worte (zu je 16 Bit).

Prüfung der Zugriffsberechtigung

Selbstverständlich führt der Prozessor auch bei einem Aufruf über Gates eine Prüfung der Zugriffsberechtigung aus. In diese Prüfung gehen folgende Privilegierungsstufen ein:

Der DPL-Eintrag des Gate-Deskriptors legt fest, von welchen Privilegierungsstufen aus das Gate benutzt werden kann. Gates werden benutzt, um zum Beispiel die Steuerung an privilegiertere Ebenen (zum Beispiel das Betriebssystem) oder Code gleicher Privilegierungsstufe zu übergeben. Für letzteren Fall sind sie zwar nicht zwingend notwendig (siehe oben), aber dieses Vorgehen ist auch möglich. Wichtig ist, dass nur CALL-Befehle Gates dazu verwenden können, Routinen niedrigerer Privilegierungsstufe (mit größeren PL) aufzurufen. Sprung-Befehle können Call-Gates nur dazu benutzen, die Steuerung an ein Code-Segment gleicher Privilegierungsstufe oder an ein Conforming-Segment gleicher oder höherer Stufe zu übergeben.

Für einen Sprung-Befehl zu einem nicht als Conforming gekennzeichneten Segment müssen die beiden folgenden Bedingungen erfüllt sein:

Für den CALL-Aufruf oder einen Sprung-Befehl zu einem Conforming-Segment müssen dagegen die zwei folgenden Bedingungen erfüllt sein:

Bei einem Aufruf einer Prozedur höherer Privilegierungsstufe über ein Call-Gate führt der Prozessor noch folgende Vorgänge aus:

Die Stacks aller Privilegierungsstufen werden dabei durch das Task-State-Segment des jeweiligen Tasks definiert (siehe unten). Wie Sie aus all dem sehen können, wurden bereits dem 80286, der ja dieselben Prüfungen ausführt, nur beschränkt auf 16 Bit, erheblich mehr Funktionen eingepflanzt, als dies zunächst erscheint. Beispielsweise führen der 80286 und der 80386 die Übertragung der Worte zwischen den Stacks automatisch und selbstständig aus, ohne dass entsprechende Software-Befehle im Befehlsstrom vorliegen. Stattdessen sind im Mikrocode-ROM der Prozessoren umfangreiche Mikroprogramme abgelegt, die im Protected Mode alle Zugriffe online überwachen.

Die Interrupt-Deskriptortabelle

Neben den Registern für die globale und lokale Deskriptortabelle sowie das Task-Register ist für die Interrupt-Deskriptortabelle (IDT, siehe Bild "Interrupt-Tabellen im Real und im Protected Mode") ein spezielles Register implementiert. Im Real Mode waren die 1024 (1 KByte) niederwertigen Byte des Adressraums für die 256 Einträge (entsprechend den 256 Interrupts ab dem 80386) der Interruptvektortabelle reserviert. Jeder Eintrag enthält im Format Segment:Offset die Einsprungadresse (Ziel) des zugehörigen Interrupt-Handlers.

Auch im Protected Mode stehen 256 Interrupts von 0 bis 255 zur Verfügung. Die Interrupt-Handler werden jedoch nicht mehr über ein Doppelwort mit dem Format Segment:Offset angesprochen, sondern über Gates. Als Einträge in der IDT sind nur Task-, Interrupt- und Trap-Gates zulässig. Damit ist jeder Eintrag statt vier nunmehr acht Byte lang. Durch den Eintrag Limit im IDTR (Abbildung 6.8) kann die Größe der Interrupt-Deskriptortabelle jedoch den tatsächlichen Erfordernissen angepasst werden. Benötigt ein System beispielsweise nur die Interrupts 0 bis 63, so genügt eine IDT mit 64 Einträgen zu acht Byte, das heißt insgesamt 512 Byte. Wird ein Interrupt ausgelöst, für den kein Eintrag in der IDT mehr existiert (im angeführten Fall zum Beispiel ein INT 64), so tritt der Prozessor in den Shutdown-Modus ein. Da im IDTR neben dem Limit auch die Basisadresse der IDT angegeben wird, kann sich die Tabelle irgendwo im Speicher befinden (siehe Bild "Interrupt-Tabellen im Real und im Protected Mode").

Bevor der Prozessor in den Protected Mode umgeschaltet wird, muss das im Real Mode laufende Initialisierungsprogramm neben der GDT auch die IDT aufbauen und deren Basisadresse und Limit in das IDTR laden. Geschieht das nicht, so hängt sich der Prozessor mit ziemlicher Sicherheit auf, bevor die IDT im Protected Mode erstellt werden kann, da jede Art von Exception oder Interrupt entweder ins Nirwana weist oder eine weitere Fehler-Exception auslöst, die nicht behandelt werden kann. Beim Einschalten oder einem Prozessor-RESET lädt der Prozessor von

sich aus das IDTR mit einem Wert 000000h für die Basisadresse und 03ffh für das Limit. Diese Werte sind konsistent mit dem reservierten Bereich für die Interrupt-Vektortabelle im Real Mode.

Die Interrupt-, Trap- und Task-Gates weisen denselben Aufbau wie das Call-Gate auf (siehe Bild "Gate-Deskriptor"), nur besitzt der Eintrag DWord-Count keine Bedeutung. Die Interrupt- und Trap-Gates definieren in gleicher Weise wie das Call-Gate den Einsprungpunkt über die Einträge Offset und Segmentselektor. Der Segmentselektor weist wie beim Call-Gate auf den Segmentdeskriptor in der LDT oder GDT, der die Basisadresse des betreffenden Segments enthält. Damit ist der Einsprungpunkt des Interrupt-Handlers ebenfalls eindeutig definiert. Der Unterschied zwischen Interrupt- und Trap-Gate besteht darin, dass ein Interrupt-Aufruf über ein Interrupt-Gate die Flags IE und T löscht, das Trap-Gate hingegen nicht.

Die Besonderheiten, die gelten, wenn der Prozessor bei einem Interrupt oder einem CALL- beziehungsweise Sprungbefehl auf ein Task-Gate trifft, werde ich im folgenden Abschnitt näher erläutern.

Multitasking, TSS und das Task Gate

Die gesamten Protection-Funktionen dienen in erster Linie einem Ziel: Multitasking. Bei einem PC-System sollen mehrere Tasks mehr oder weniger parallel ablaufen. Tatsächlich erreicht man mit einem Prozessor nur eine scheinbare Parallelität, weil die einzelnen Tasks für kurze Zeit ausgeführt, in kurzen Zeitabständen unterbrochen und nach kurzer Zeit an der gleichen Stelle wieder gestartet werden. Um das zu erreichen, muss der Zustand eines Tasks zum Zeitpunkt der Unterbrechung vollständig gesichert werden, weil der Task ja sonst nicht an derselben Stelle unter den Bedingungen zum Zeitpunkt der Unterbrechung neu aufgenommen werden kann.

Ein ähnlicher Vorgang findet auch unter MS-DOS statt: Tritt ein Hardware-Interrupt wie beispielsweise der Timer-Interrupt auf, so werden alle Register auf dem Stack gesichert, der Interrupt bedient und alle Register vom Stack wieder mit den alten Werten geladen. Wichtig ist, dass das Registerpaar CS:EIP gesichert wird, da es die Stelle im Programm angibt, an der es unterbrochen worden ist.

TSS

Auf Grund der umfangreichen Protection-Funktionen des Prozessors ist es nicht damit getan ist, einfach nur ein paar Register zu sichern. Hierzu dient vielmehr das noch nicht näher besprochene Systemsegment mit Namen Task State Segment oder kurz TSS. Wie der Name bereits ausdrückt, speichert dieses den Zustand eines Tasks vollständig. Es stellt ein ganzes Segment dar, das ausschließlich zur Speicherung des Task-Zustands dient.

Im TSS sind neben den gewöhnlichen Offset- und Segmentregistern beispielsweise die Zeiger ESP und Segmente SS für die Stacks der verschiedenen Privilegierungsstufen, die für den Task benutzte lokale Deskriptortabelle und ein Eintrag enthalten, der auf das TSS des zuvor ausgeführten Tasks zeigt. Außerdem ist das CR3-Register abgelegt, das die Basisadresse des Page Directories für den beschriebenen Task angibt.

Der Eintrag I/O-Map-Basis gibt die Adresse einer I/O-Map an, die neben dem IOPL-Flag zum Schutz des I/O-Adressbereichs im Protected Mode dient. Im weiteren Verlauf des Kapitels erfahren Sie mehr hierzu. Das Feld Back Link enthält einen Segmentselektor, der auf das TSS des zuvor unterbrochenen Tasks weist. Der Eintrag ist aber nur dann gültig, wenn das Bit NT (Nested Task) im EFlag-Register gesetzt ist. Wenn das T-Bit (Debug-Trap-Bit) gesetzt ist, erzeugt der Prozessor bei einem Task Switch (das heißt beim Laden des TSS') eine Debug-Exception entsprechend Interrupt 1.

Weist der zugehörige TSS-Deskriptor in der LDT oder GDT im Typfeld den Wert 1 (80286-kompatibles TSS) oder 9 (ab 80386) auf, so ist das durch den Deskriptor beschriebene TSS verfügbar. Dies bedeutet, dass der von diesem TSS beschriebene Task gestartet werden kann. Ist im Typfeld hingegen ein Eintrag 3 (80286-kompatibles TSS) oder 11 (ab 80386) vorhanden, so ist das TSS als aktiv (busy) gekennzeichnet. Der von einem solchen TSS beschriebene Task ist aktiv und muss nicht eigens aktiviert werden. Darüber hinaus darf er nicht einmal aktiviert werden, weil das gespeicherte TSS noch die alten Werte enthält. Tasks sind also im Gegensatz zu Prozeduren prinzipiell nicht reentrant. Erst wenn der gerade laufende (aktive) Task unterbrochen wird, um zum Beispiel einen anderen Task zu aktivieren, sichert der Prozessor alle aktuellen Werte des aktiven Tasks im zugehörigen TSS und lädt die Werte des zu startenden Tasks aus dessen TSS in die Segment-, Offset- und Steuerregister. Das geschieht völlig automatisch und selbstständig ohne einen weiteren Eingriff von Software.

Task-Gate

Woher "weiß" der Prozessor nun aber, wann er einen Task und welchen neuen er aktivieren soll, das heißt was bildet den Trigger für einen Task Switch?

Der Schlüssel liegt in den Task-Gates. Das Bild "Task-Gate-Deskriptor" zeigt den Aufbau eines solchen Task Gates. Beachten Sie, dass sich die Struktur der Task Gates zwischen 80286 und 80386 trotz des Übergangs von 16 auf 32 Bit im Gegensatz zu allen anderen Gates und Systemsegmenten nicht geändert hat.

Der TSS-Segmentselektor im Task-Gate weist auf den Segmentdeskriptor, der das TSS des neu zu aktivierenden Tasks definiert. Trifft der Prozessor bei einem CALL-Befehl, einem Sprungbefehl oder einem Interrupt auf ein solches Task-Gate, führt er einen solchen Task-Switch aus, indem er den gegenwärtigen Zustand des aktiven Tasks im TSS abspeichert, das durch das Task-Register TR (siehe Abbildung 6.8) definiert ist und dem Typfeld des zugehörigen TSS-Deskriptors den Wert 1 (80286-kompatibles TSS) oder 9 (80386-TSS) zuweist. Damit ist das TSS als verfügbares TSS gekennzeichnet. Anschließend lädt er den neuen TSS-Segmentselektor aus dem Task-Gate-Deskriptor in das TR und liest aus der LDT oder GDT (je nachdem, welche Tabelle der Segmentselektor im Task Gate referiert) Basisadresse, Limit und Zugriffsrechte des Task-Gate-Deskriptors.

Um den Task-Switch zu vollenden, kennzeichnet der Prozessor nun den zugehörigen TSS-Deskriptor im Typfeld als busy, das heißt er schreibt einen Wert 3 (80286-kompatibles TSS) oder 11 (80386-TSS) in dieses Feld. Zuletzt lädt er die im neuen TSS abgelegten Werte für die Segmente und Offsets in die entsprechenden Register. Das Registerpaar CS:EIP zeigt nun auf den Befehl des neu aktivierten Tasks, bei dem dieser zuvor unterbrochen worden ist; seine Ausführung wird also an der Unterbrechungsstelle erneut aufgenommen. Das Bild "Task-Switch" zeigt die vier Möglichkeiten, wie ein Task Switch ausgelöst werden kann, sowie die dazu vom Prozessor durchgeführten Prozesse.

Erstmals aktivierte Tasks - also Tasks, die neu geladen werden - aktiviert die CPU in gleicher Weise. Nur zeigt das Registerpaar CS:EIP hier nicht auf den Befehl an der Unterbrechungsstelle, sondern den Startbefehl des Programms.

Beispiel

Der aktive Task sei das Textverarbeitungsprogramm Word, das gerade damit beschäftigt ist, einen Seitenumbruch auszuführen. Nun tritt ein Timer-Interrupt auf. Im Interrupt-Handler trifft der Prozessor auf ein TaskGate, das auf Excel zeigt. Damit suspendiert der Prozessor Word, indem er alle Register im zugehörigen TSS sichert. Anschließend lädt er alle notwendigen Daten aus dem TSS für Excel und startet diesen bereits früher unterbrochenen Task. Nach kurzer Zeit tritt erneut ein Timer-Interrupt auf, nur wird diesmal Excel angehalten und dafür beispielsweise der C-Compiler aktiviert. Dieses Unterbrechen und Wiederaufnehmen von Tasks findet laufend statt.

Wird ein neues Programm gestartet, so stellt das Betriebssystem ein neues TSS für diesen Task zur Verfügung. Sie merken schon, dass ein Multitasking-Betriebssytem sehr komplexe Operationen mit rasender Geschwindigkeit ausführen muss. Um einen TaskSwitch auszuführen, muss das Betriebssystem "nur" ein TaskGate, einen TSS-Deskriptor und ein TSS zur Verfügung stellen. Das Sichern der alten Registerinhalte und das Laden der neuen Werte führt der Prozessor selbstständig und automatisch aus. Es sind keine Software-Anweisungen des Betriebssystems notwendig, das heißt der Prozessor sichert bei einem Task Switch die 104 Byte des alten TSS und lädt die 104 Byte des neuen TSS völlig selbstständig.

Task-Switches im Betriebssystem

Zu betonen ist, dass es allein Aufgabe des Betriebssystems ist, den einzelnen Programmen einen entsprechend großen Anteil an der Prozessorzeit zuzuweisen. Die Steuerung der Task Switches ist alleinige Aufgabe des Betriebssystems, die Programme selbst haben bei einem richtigen Multitasking-Betriebssystem wie zum Beispiel Linux, OS/2 oder Windows NT/2000 keine Einflussmöglichkeiten darauf. Windows 3.x weicht davon erheblich ab. Hier entscheidet die Anwendung (und nicht Windows 3.x als Betriebssystemerweiterung), wann sie die Kontrolle an einen anderen Task (ein anderes Programm oder Windows selbst) abgibt. Eine sehr unangenehme Folge davon ist, dass der PC unter Windows 3.x scheinbar nicht oder nur sehr verspätet auf ein gewolltes Umschalten zu einem anderen Task reagiert (zum Beispiel über Strg-Esc). "Richtige" Multitasking-Betriebssysteme führen nämlich ein so genanntes Preemptive Multitasking aus, Windows 3.x dagegen ein Non-Preemptive Multitasking oder kooperatives Multitasking. Ab Windows 95 wird das Multitasking in beiden Formen implementiert, sodass es von den jeweiligen Programmen abhängt (für welche Windows-Version sie ursprünglich geschrieben worden sind), welches Verfahren zum Einsatz kommt, was in der Praxis jedoch leider mit Problemfällen einhergeht, was letztendlich auch vom jeweiligen "Programm-Mix" auf Ihrer Festplatte abhängt.

Das Betriebssystem DOS verwendet von den oben beschriebenen Funktionen nicht eine einzige. Auch die Treiber SMARTDRV.SYS und RAMDRIVE.SYS erstellen nur eine GDT und eine IDT, um Bytegruppen zwischen den unteren 1 MByte des Speichers und dem Extended Memory zu verschieben. Task-Switches und die umfangreichen und sehr nützlichen Zugriffsprüfungen werden in keiner Weise ausgenutzt.

Neben den bereits im Ansatz geschilderten Prüfungen und Besonderheiten beim Aufruf von Prozeduren oder dem Umschalten zwischen verschiedenen Tasks muss ein Systemprogrammierer noch viele weitere Einschränkungen und Vorsichtsmaßnahmen beachten. Erst dann ist es möglich, ein voll funktionsfähiges Betriebssystem zu programmieren, das eine aktuelle CPU voll ausnutzt. Mit diesem Thema könnte man natürlich ohne Probleme mindestens zwei dicke Bücher füllen. Wir wollen uns mit dieser Einführung begnügen. In den nächsten beiden Abschnitten werden dann noch die Schutzvorkehrungen für den zweiten Adressraum erläutert, nämlich die Zugriffsprüfungen für den I/O-Adressraum.

Schutz des I/O-Adressraums

Über die I/O-Ports werden im allgemeinen Register von Hardwarekomponenten angesprochen. Da die Steuerung und Überwachung eine originäre Aufgabe des Betriebssystems ist und hierzu meist Treiber mit Privilegierungsstufe PL=1 benutzt werden, fällt auch der I/O-Adressraum oder -bereich unter den Zugriffsschutz. Ports werden aber nicht mit Hilfe eines Segmentregisters angesprochen, also steht diese Art des Zugriffsschutzes hier nicht zur Verfügung.

Der Schutz des I/O-Adressbereiches erfolgt ab dem 80386 über zwei völlig andere Strategien: einmal das IOPL-Flag im Flag-Register (siehe Abbildung 5.13) und zusätzlich die I/O-Permission-Bit-Map im Task-State-Segment. Zunächst aber zum IOPL-Flag.

Das IOPL-Flag

Der Wert dieses Flags gibt die Privilegierungsstufe an, die ein Code-Segment mindestens aufweisen muss, um auf den I/O-Adressraum zugreifen zu können, das heißt es muss gelten CPL kleiner-gleich IOPL. Ist der CPL des aktuellen Tasks größer (niedrigere Privilegierungsstufe), so führen die I/O-Befehle IN, OUT, INS und OUTS zu einer bereits hinlänglich bekannten Exception "allgemeiner Protection-Fehler", was unter Windows sicher schon fast jedem Anwender als Schutzverletzung begegnet ist. Vernünftige Anwendungsprogramme unter einem "richtigen" Betriebssys

tem führen solche Zugriffe ausschließlich über das Betriebssystem aus. Weniger vernünftige Anwendungsprogramme versuchen das direkt, um einerseits die Performance zu erhöhen oder andererseits bestimmte Komponenten überhaupt ansprechen zu können. Neben den vier bereits erwähnten I/O-Befehlen sind auch CLI und STI vom IOPL-Flag abhängig. Diese sechs Befehle werden als IOPL-sensitive Befehle bezeichnet, da der Wert des IOPL-Flag einen Einfluss auf ihre Ausführung hat.

Sinn und Zweck dieser Einschränkung werden sofort einsichtig, betrachtet man den Fall, dass eine Systemfunktion beispielsweise einen Datensatz von der Festplatte liest, dabei durch einen Task-Switch unterbrochen wird und der neu aufgerufene Task durch einen unmittelbaren Zugriff auf die Steuerregister im Controller "dazwischenfunkt". In welchem Zustand sich die unterbrochene Systemroutine nach einem erneuten TaskSwitch befindet, ist völlig unvorhersehbar, der PC verabschiedet sich oder zerstört sogar Daten.

Ein Task kann das IOPL-Flag nur über die Befehle POPF (POP Flags) und PUSHF (PUSH Flags) verändern. Zur Änderung des IOPL-Flags steht kein expliziter Befehl zur Verfügung (wie zum Beispiel CLI oder STI für das Interrupt-Flag). Die beiden genannten Befehle POPF und PUSHF sind jedoch privilegiert, das heißt sie können nur von einem Code-Segment mit CPL=0 ausgeführt werden. Diese Stufe ist üblicherweise dem Betriebssystem-Kernel vorbehalten - die Anwendungsprogramme können das IOPL-Flag nicht verändern. Bei einem solchen Versuch löst der Prozessor eben eine Exception "allgemeiner Protection-Fehler" aus. Da die Flags jedoch Teil des TSS sind und sich somit von Task zu Task unterscheiden können, ist es durchaus möglich, dass ein Task Zugriff auf den I/O-Adressraum besitzt, ein anderer dagegen nicht.

Diese Strategie der globalen Absicherung des I/O-Adressbereichs über das IOPL-Flag ist bereits beim 80286 implementiert. Die nachfolgenden CPUs können die Ports zusätzlich individuell schützen. Diese Schutzstrategie für die Ports ist insbesondere im Hinblick auf den Virtual-8086-Mode implementiert worden.

Die I/O-Permission-Bit-Map

Neben dem globalen Schutz durch das IOPL-Flag kennt eine 32-Bit-CPU einen weiteren Schutzmechanismus für Zugriffe auf den I/O-Adressbereich, nämlich die so genannte I/O-Permission-Bit-Map. Sie ist im TSS des jeweiligen Tasks abgelegt, verschiedene Tasks können also unterschiedliche I/O-Permission-Bit-Maps besitzen. Der Eintrag I/O-Map-Basis im TSS-Deskriptor gibt den Offset innerhalb des TSS an, bei dem die I/O-Permission-Bit-Map beginnt. Sie erstreckt sich bis zum Ende des TSS, wie es im Limiteintrag des TSS-Deskriptors festgelegt ist. Den Raum zwischen dem Eintrag I/O-Map-Basis und dem Beginn der I/O-Permission-Bit-Map kann das Betriebssystem verwenden, um eigene Informationen abzulegen. In Abbildung 6.17 sehen Sie das Schema der I/O-Permission-Bit-Map im 80386-TSS. Die I/O-Permission-Bit-Map muss also nicht unmittelbar nach den Einträgen für die Register im TSS beginnen. Vielmehr kann ein nahezu beliebig großer Raum zwischen dem Eintrag I/O-Map-Basis und dem Beginn der I/O-Permission-Bit-Map zur Verwendung durch das Betriebssystem vorgesehen werden, das dort eigene Informationen ablegt. Beachten Sie, dass das höchstwertige Byte der Map, das heißt das Byte unmittelbar vor dem Ende des TSS, den Wert 11111111b (=0ffh) besitzen muss. Für die I/O-Permission-Bit-Map können Sie nur ein 80386-TSS verwenden, das 80286-TSS ist nicht zugelassen, weil es keinen Eintrag I/O-Map-Basis hat.

Eine gültige I/O-Permission-Bit-Map ist immer dann vorhanden, wenn die I/O-Map-Basis im TSS noch innerhalb des TSS' liegt. Zeigt der Wert der Basis über das TSS hinaus, so ignoriert der Prozessor alle Prüfungen im Zusammenhang mit der I/O-Permission-Bit-Map, der Zugriffsschutz für den I/O-Adressbereich erfolgt allein durch das IOPL-Flag.

Zugriffsschutz zweiter Ebene

Die I/O-Permission-Bit-Map stellt praktisch einen Zugriffsschutz zweiter Ebene dar: Wenn die Werte von CPL und IOPL dem aktiven Task einen Zugriff auf den I/O-Adressbereich gestatten, so untersucht der Prozessor anschließend zusätzlich noch die I/O-Permission-Bit-Map, um zu ermitteln, ob der gewünschte Port auch tatsächlich angesprochen werden kann. Das geschieht auf der Basis einer eins-zu-eins-Zuordnung von I/O-Adresse und dem entsprechenden Bit in der Map. Dem Port mit der Adresse 0 ist das Bit mit Offset 0 innerhalb der Map, dem Port mit der Adresse 1 das Bit mit Offset 1 usw. zugeordnet. Ist das einem Port entsprechende Bit in der Map gesetzt, also gleich 1, so löst die CPU bei einem Zugriff auf den zugehörigen Port eine Exception "allgemeiner Protection-Fehler" aus. Ist das Bit gelöscht, so fährt der Prozessor mit der I/O-Operation fort.

Die Länge der Map bestimmt die Zahl der so zusätzlich geschützten Ports. Es ist also nicht erforderlich, dass die I/O-Permission-Bit-Map alle I/O-Adressen abdecken muss. Allen von der Map nicht erfassten I/O-Ports wird automatisch ein gesetztes Bit zugeordnet, das heißt ein Zugriff auf die außerhalb der Map liegenden Ports führt automatisch zu einer Exception. In einem ISA-PC reicht es zum Beispiel aus, die 3ffh niederwertigsten Ports durch eine Map abzudecken. Ein Zugriff auf Ports mit höherer Adresse löst eine Exception aus. Sie sehen erneut, dass die Schutzmechanismen des Protected Modes nicht nur Programme und das System schützen, sondern auch eine wesentlich einfachere Lokalisierung von Bugs ermöglichen. Um den gesamten I/O-Adressraum abzudecken, sind insgesamt (64k Ports)/(8 Bit je Byte) + (8 Bit 11111111), das heißt 8193 Byte notwendig.

Beachten Sie, dass 16-Bit-Ports zwei und 32-Bit-Ports vier aufeinanderfolgenden Bits zugeordnet sind. Nur wenn beide beziehungsweise alle vier zugeordneten Bits gleichzeitig gelöscht sind, kann der Prozessor die I/O-Operation fortsetzen. Ist auch nur eines der Bits gleich 1, so löst der Prozessor eine Exception aus.

Beispiel

Die Bit-Map lautet: 11111111 11001101 00110000 11010100

1. Fall: 8-Bit-Ports

geschützte Ports 2, 4, 6, 7, 12, 13, 16, 18, 19, 22, 23

nicht geschützte Ports 0, 1, 3, 5, 8, 9, 10, 11 14, 15, 17, 20, 21

2. Fall: 16-Bit-Ports

geschützte Ports 2, 4, 6, 12, 16, 18, 22

nicht geschützte Ports 0, 8, 10, 14, 20

3. Fall: 32-Bit-Ports

geschützte Ports 0, 4, 12, 16, 20

nicht geschützte Ports 8

Die 8-, 16- und 32-Bit-Ports können natürlich auch gemischt sein, je nachdem an welcher Adresse sich ein I/O-Gerät mit welcher Registerbreite befindet.

Erwähnen möchte ich an dieser Stelle, dass ein 32-Bit-Prozessor im Virtual-8086-Mode das IOPL-Flag nicht benutzt, sondern den Schutz des I/O-Adressbereichs ausschließlich über die I/O-Permission-Bit-Map bewerkstelligt. Dadurch kann die CPU für ein 8086-Programm, das unter einem Protected-Mode-Betriebssystem im Virtual 8086 Modus läuft, das I/O-Verhalten des 8086 emulieren. Die I/O-Permission-Bit-Map wurde besonders im Hinblick auf den Virtual-8086-Mode implementiert.

Exceptions und Schutzmechanismen im Protected Mode

Im Protected Mode sind gegenüber dem Real Mode weitere Exceptions möglich, die in erster Linie Fehlerbedingungen anzeigen, deren Ursache in einer Verletzung der Schutzbedingungen des Protected Modes liegen. Im Folgenden finden Sie eine Auflistung der neuen Exceptions:

Schutzmechanismen

Von den Schutzmechanismen im Protected Mode sind in erster Linie Befehle betroffen, die den Zustand der CPU steuern und lesen und auf Code- und Datensegmente zugreifen. Es soll verhindert werden, dass eine fehlerhafte oder inadäquate Anweisung die CPU aufhängt oder blockiert (wie zum Beispiel der HLT-Befehl) beziehungsweise Daten- und Code-Segmente in unsauberer Weise benutzt werden und dadurch die Systemintegrität zerstört wird. Zu diesem Zweck sind drei Gruppen von Schutzmechanismen vorgesehen:

Verletzt im Protected Mode ein Vorgang einen dieser Schutzmechanismen, dann löst der Prozessor sofort eine Fehler-Exception aus.

Ausblick

Im nächsten Teil der Artikelserie erfahren Sie, was es mit Paging auf sich hat und welche neuen Möglichkeiten sich damit auftun.

Serie: x86-Programmierung und -Betriebsarten

Teil 1

Code-/Datensegment, Befehlszähler und Stack

Teil 2

Adressierungsarten und Befehlsdekodierung sowie Real Mode

Teil 3

Interrupts und Exceptions

Teil 4

Der Protected Mode

Teil 5

Paging und die MMU

Teil 6

Der Virtual-8086-Modus

Diese Artikelserie basiert auf dem Kapitel 6 des "PC Hardwarebuch" von Addison Wesley. Sie können in unserem Buch-Shop das über 1200 Seiten starke Kompendium bestellen oder als eBook downloaden.

tecCHANNEL Buch-Shop

Weitere Literatur zum Thema Hardware

Titelauswahl

Titel von Pearson Education

Bücher

PDF-Titel (50% billiger als Buch)

Downloads