Betriebstechnik | Biographien | Biologie | Chemie | Deutsch | Digitaltechnik |
Electronica | Epochen | Fertigungstechnik | Gemeinschaftskunde | Geographie | Geschichte |
Informatik | Kultur | Kunst | Literatur | Management | Mathematik |
Medizin | Nachrichtentechnik | Philosophie | Physik | Politik | Projekt |
Psychologie | Recht | Sonstige | Sport | Technik | Wirtschaftskunde |
Ähnliche Berichte:
|
Projekte:
|
Papers in anderen sprachen:
|
informatik referate |
Fachbereichsarbeit
aus Informatik
Thema:
Die Bedeutung des Protected Mode und seine Funktionsweise, sowie die Programmierung einer Bibliothek zur Nutzung desselben in C++
Die Entwicklung der heutigen Intel Architecture kann über den 8085 und den 8080 zum 4004 (dem ersten Mikroprozessor, den Intel entwickelte) zurückverfolgt werden. Der erste Prozessor der Intel Architecture ist jedoch der 8086, dem schnell der für billige Systeme preisgünstigere 8088 folgte.
Der 8086 besitzt 16-Bit Register und einen 16-Bit breiten externen Bus, mit 20 Adress-Bits kann er einen Speicher von 1 mByte ansprechen. Der 8088 unterscheidet sich nur durch den 8-Bit breiten externen Bus vom 8086. Diese Prozessoren führten die Intel-Architecture-typische Segmentierung des Real Mode ein. 16-Bit Register dienen als Zeiger auf Adressen in Segmenten von bis zu 64kByte Größe. Die vier Segment-Register beinhalten die (effektive) Segment-Adresse der aktiven Segment (20 Bit). Bis zu 256 kByte können adressiert werden, ohne zwischen Segmenten umzuschalten, und insgesamt steht ein Adressraum von 1 mByte zur Verfügung.
Der 80286 brachte den Protected Mode in die Intel Architecture. Dieser neue Modus nutzt den Inhalt der Segment-Register als Selektoren oder Zeiger in Descriptor-Tables. Die Deskriptoren stellen 24-Bit Basisadressen zur Verfügung, und damit einen Adressraum von 16 mByte. Außerdem werden Virtual Memory Management, Segment Swapping und verschiedene Schutzmechanismen unterstützt. Diese Schutzmechanismen beinhalten Segment-Limit-checking, nur lesbare/nur ausführbare Segmente und bis zu vier Privilegstufen, um das Betriebssystem vor Anwenderprogrammen zu schützen. Dazu kommt noch die Hardware-Unterstützung des Task-Switching und die Local-Descriptor-Tables, die es dem Betriebssystem erlauben, die Anwenderprogramme voreinander zu schützen und voneinander zu trennen.
Mit dem i386 kamen 32-Bit-Register in die Intel Architecture, sowohl als Operanden für Berechnungen, als auch als Adressen. Die untere Hälfte der 32-Bit-Register behielt ihre Eigenschaften als 16-Bit-Register der beiden früheren Generationen um komplette Abwärtskompatibilität zu gewährleisten. Ein neuer Modus, der Virtual-8086-Mode, wurde eingeführt, um größere Effizienz beim Abarbeiten von Programmen, die für den 8086/8088 geschrieben worden waren, auf dem 32-Bit System zu erlangen. Die 32-Bit-Address-Register wurden durch einen 32-Bit-Address-Bus ergänzt, der nunmehr einen Adressraum von 4-gByte zuließ, in dem jedes Segment eben diese Länge von 4 gByte erreichen kann. Die ursprünglichen Befehle wurden durch neue 32-Bit-Operanden verbessert und gänzlich neue Instruktionen wurden hinzugefügt. Der i386 enthält als erster mehrere parallele Stufen, um die Arbeitsgeschwindigkeit zu verbessern:
Die Bus Interface Unit (greift auf Speicher und I/O zu)
Die Code Prefetch Unit (bekommt den Objektcode von der BIU und leitet ihn in eine 16-byte Warteschlange weiter
Die Instruction Decode Unit (dekodiert den Objektcode von der CPU in Mikrocode)
Die Execution Unit (führt die mikrocodierten Instruktionen aus)
Die Segment Unit (wandelt logische in lineare Adressen um und führt Schutzprüfungen durch)
Die Paging Unit (wandelt lineare in physikalische Adressen um)
Durch den i486 gelang es, die Parallelisierung noch weiterzutreiben: Die Instruction Decode Unit und die Execution Unit wurden in 5 Stufen zerlegt, die alle gleichzeitig arbeiten. So kann der i486 eine Instruktion pro CPU-Clock-Cycle durchführen. Der i486 beinhaltet außerdem als erster die Floating-Point-Unit auf dem gleichen IC wie die CPU, wodurch die Geschwindigkeit weiter gesteigert werden konnte.
Der Pentium erhielt eine zweite Execution Pipeline, mit deren Hilfe er sogar zwei Instruktionen pro CPU-Clock-Cycle durchführen kann. Die Register sind noch immer 32 Bit breit, doch interne Datenleitungen bestehen aus 128 oder 256 Bit. Die Effektivität des Virtual-8086-Mode wurde außerdem erhöht.
Schon der i386 besitzt die wichtigsten Merkmale der sogenannten Intel Architecture, deshalb ist er wohl der geeignete Einstiegspunkt.
Eine kurze Zusammenfassung der wesentlichen Eigenschaften dieses 32-Bit-Prozessors:
32-Bit Vielzweck- und Offsetregister
16-Byte-Prefetch-Queue
Speicherverwaltungseinheit (Memory Management Unit) mit Segmentierungs- und Paging Unit (PU)
32-Bit Daten- und Adreßbus
4gByte physikalischer Adreßraum
64tByte virtueller Adreßraum
65536 8-, 16- oder 32-Bit Ports
Implementierung von Real, Protected und Virtual 8086 Mode
Zunächst möchte ich die Register des i386 vorstellen, die im Real Mode benutzt werden können. Natürlich hat der Prozessor viele interne Register zur Speicherung von Zwischenergebnissen usw., die jedoch dem Programmierer nicht zugänglich und daher nur von geringer Bedeutung sind.
Die Vielzweckregister dienen als schnelle interne Datenspeicher der CPU. Die Execution Unit liest Werte aus einem oder mehreren von diesen Registern, führt sie der ALU zu, um Manipulationen vorzunehmen, und legt das Ergebnis schließlich wieder in einem oder mehreren Registern ab. Neben den Vielzweck- gibt es noch die Segmentregister zur Speicherverwaltung sowie diverse Steuerregister.
Der i386 liest über seinen 32-Bit-Datenbus Daten aus dem Speicher oder schreibt sie mit Hilfe des Datenbusses in den Speicher. Die betreffende Stelle im Speicher wird dabei durch eine 32-bit-Adresse festgelegt, die der Prozessor mit Hilfe der Addressing Unit berechnet und über den 32-Bit-Adreßbus an das Speichersubsystem übergibt. Die Adreßberechnung ist je nach Betriebsmodus des i386 - Real- ,Protected- oder Virtual8086-Mode - unterschiedlich aufwendig.
Befehle und Daten befinden sich wie heute üblich im selben physikalischen Speicher, wobei aber dieser eine physikalische Speicher bei der Intel Architecture logisch in mehrere verschiedene Abschnitte, sogenannte Segmente, aufgeteilt ist und die Abschnitte Programmcode oder Daten enthalten.
Bereits der 8086 teilte den zur Verfügung stehenden Speicher in die Segmente auf. Das machen auch die neueren Prozessoren, bis hin zum Pentium II MMX. Da der 8086 aber insgesamt nur 20 Adreßleitungen gegenüber den 32 des i386 aufweist, kann er maximal 220Byte=1mByte Speicher adressieren. Damit besteht sein physikalischer Adreßraum aus 1mByte Speicher. Jedes der Vielzweckregister im 16-Bit-Prozessor 8086 (zur damaligen Zeit der 8-Bit-Chips dieselbe Sensation wie später der Übergang von 16 auf 32 Bits mit dem i386) ist jedoch nur 16 Bits lang und kann maximal 216byte=64kByte adressieren. Der 8086 unterteilt also den physikalischen Adreßraum in 64k-Segmente mit einer Größe von jeweils 64kByte. Innerhalb eines Segmentes wird die Stelle eines Bytes durch einen Offset angegeben. Offsets werden in den Vielzweckregistern gespeichert. Demgegenüber werden die Segmente über die Segmentregister CS bis GS angesprochen. Die CPU bildet für den Zugriff auf den Speicher sogenannte Segment-Offset-Paare: Das Segment eines bestimmten Speicherobjekts wird durch das Segmentregister, der Offset innerhalb des so festgelegten Segments dann noch durch das beteiligte Vielzweckregister angegeben. Die 16-Bit-Segmentregister können wie die 16-Bit-Offsetregister des 8086 64k=65536 Segmente adressieren, die jeweils 64kByte groß sind. Der theoretisch mögliche Adreßraum umfaßt daher 64kBytex64kByte=4gByte. Das ist mit dem 20-Bit-Adreßbus des 8086 aber nicht zu realisieren, er ist nur in der Lage, 1mByte anzusprechen. Die Segmente werden daher in einem Abstand von 16 Byte verzahnt. Erhöht sich der Wert des Segmentregisters um eins, so verschiebt sich das Segment lediglich um 16 Bytes, nicht um ein "ganzes" Segment mit 64kByte. Demgegenüber verschiebt eine Erhöhung des Offsetregisters um den Wert eins das Speicherobjekt nur um eine Stelle (d.h. ein Byte). Anderungen der Segmentregister führen also zu einem wesentlich größeren (zum 16-fachen) Versatz als Anderungen der Offsetregister. Die eben beschriebene Festlegung von Segment und Offset ist charakteristisch für den Real Mode. Der 8086 kann nur in eben diesem Modus arbeiten, der i386 (sowie alle weiteren Prozessoren der IA) beginnt nach dem Einschalten im Real Mode, kann aber später in den Protected- oder Virtual-8086-Mode umgeschaltet werden.
Die Adresse eines Objekts wird im Real Mode also durch die einfache Formel:
10h * Segment + Offset
berechnet. Anders ausgedrückt bedeutet das eine Verschiebung des Segmentregisters um vier Bits nach links und eine Addition des Offsets. Die quasi aus dem nichts auftretenden vier Bits bei dieser Segmentregisterverschiebung werden auf Null gesetzt. Die AU führt genau diesen Verschiebungs- und Additionsprozeß aus: Nach einer Verschiebung der Segmentadresse um vier Bits nach links summiert ein Addierer in der AU die verschobene Segmentadresse und den Offset, um die betreffende lineare Adresse zu bilden.
Segment und Offset werden meist hexadezimal in der Schreibweise Segment:Offset angegeben.
Nach der oben angeführten Formel ergibt sich für die lineare Adresse:
1F36h * 10h + 0A5Dh = 7990d * 16d + 2653d = 130493d
Alternativ kann man auch mit einer Verschiebung des Segments um vier Bits (eine Hexadezimalstelle) arbeiten:
1F360h
00A5Dh
1FDBDh=130493d
Zu beachten ist, daß zwei verschiedene Segment-Offset-Paare im Real Mode durchaus dieselbe Speicherstelle bezeichnen können.
1FB1:02AD => 1FB1h * 10h + 02ADh = 130493d (vgl. obiges Beispiel)
Wie bereits erwähnt, ist diese Art der Adreßberechnung charakteristisch für den Real Mode oder eben den 8086. Im Protected Mode, der als wesentliche Neuerung mit dem 80286 eingeführt wurde, sind Segment und Offset vollständig entkoppelt. Die Segmentregister besitzen eine völlig andere Bedeutung, es findet keine Abbildung nach der oben angegebenen einfachen Formel statt. Dadurch ist beim 80286 ein logischer Adreßraum von maximal 1gByte und beim 80386 sogar 64tByte je Task (Programm) möglich. Dieser logische Adreßraum wird jedoch von der Segmentierungslogik beim 80286 auf einen physikalischen Adreßraum von maximal 16mByte entsprechend seinen 24 Adreßleitungen (224=16mByte) abgebildet. Beim i386 stehen bereits 4gByte (232=4gByte) zur Verfügung. Meist liegt der tatsächliche Speicherausbau des Computers jedoch weit unter diesem Wert.
Der im i386 erstmals implementierte Virtual 8086 Mode stellt eine erhebliche Innovation im Hinblick auf die Betreibung im Protected Mode dar. Der i386 führt in diesem Modus die Adreßberechnung nach der oben beschriebenen Formel des Real Mode aus, wobei aber der tatsächliche Zugriff auf den Speicher und die Peripherie durch dieselben Mechanismen überwacht und gegen unerlaubte und fehlerhafte Versuche geschützt wird, wie sie für den Protected Mode charakteristisch sind.
Die Vielzweckregister sind ab dem i386 32 Bit breit, können aber aus Kompatibilitätsgründen mit den 16-Bit Vorgängern auch als 16- oder 8-Bit-Register angesprochen werden. Der i386 weist sieben Vielzweckregister, EAX bis EBP, sechs Segmentregister, CS bis GS, einen Befehlszähler EIP, einen Stack-Zeiger ESP sowie das Flagregister EFlags auf. All diese sind 32 Bit, nur die Segmentregister 16 Bit breit. Durch die maximale Größe der Vielzweckregister von 32 Bit sind beim i386 Offsets mit einer Länge von 32 Bits möglich, solche also von 0 bis 4gByte-1. Segmente können beim i386 demnach wesentlich größer sein, als beim 8086. Bei allen 32-Bit-Registern ist es möglich, auch nur die zwei niederwertigen Bytes anzusprechen. Sie werden als AX bis DI, IP, SP und Flag bezeichnet. Das niederwertige Wort der vier Vielzweckregister AX bis DX kann sogar noch weiter in zwei Registerbytes aufgeteilt werden, nämlich AH und AL, BH und BL, usw. Dadurch kann der i386 auch einzelne Datenbytes bearbeiten.
Der Akkumulator wird am häufigsten zum Speichern von Daten verwendet. Die Sonderstellung des Akkumulators hat historische Gründe, da es in den älteren und einfacheren Mikro-prozessoren nur ein Register (eben den Akkumulator) gab, um beispielsweise Daten zu addieren. Heute ist von dieser Einschränkung übrig geblieben, daß viele Befehle nur für den Akkumulator geschwindigkeitsoptimiert sind und damit bei einer Referenz des Akkumulators schneller arbeiten. Auch sind manche Befehle bei Registerreferenzen nur für den Akkumulator gültig.
OUT 70h, ax ;über Port 70h wird der Wert des Akkumulators ax ausgegeben.
MOV ax,2Dh ;der Akkumulator ax wird mit dem Wert 2Dh geladen.
Das Basisregister kann zur temporären Speicherung von Daten und als Zeiger auf die Basis von Datenobjekten (beispielsweise den Beginn eines Feldes) bei indirekter Adressierung verwendet werden.
MOV ecx, [ebx] ;ecx wird mit dem Wert geladen, der an der Basisadresse ebx gespeichert ist.
Das Zählregister speichert üblicherweise die Zahl der Wiederholungen von Schleifen (LOOP), Zeichenkettenbefehlen (REP) oder Verschiebungen und Rotationen (SHL, ROL, etc.). Bei dieser Verwendung wird der Wert von ECX bei jedem Schleifendurchlauf um eins erniedrigt. ECX kann auch als gewöhnliches Vielzweckregister verwendet werden, um zum Beispiel Daten temporär abzulegen.
MOV ecx, 10h ;ecx mit 10h laden
Marke: ;Label für Rücksprung
OUT 70h, al ;al über Port 70h ausgeben
LOOP Marke ;wiederholen, bis ecx null ist (=> 16 mal)
Das Datenregister wird am häufigsten zur temporären Speicherung von Daten verwendet. Bei der Ein- und Ausgabe enthält EDX die I/O-Adresse des anzusprechenden Ports (zwischen 0 und 65536). Der Weg über das Register EDX ist dabei der einzige Weg, um Ports mit einer I/O-Adresse größer als 255 anzusprechen.
MUL ebx ;Multiplikation von ebx mit eax (implizit), Produkt in edx:eax
OUT dx, ax ;Ausgabe von ax über Port dx
Auch wenn der Basiszeiger zur allgemeinen temporären Speicherung von Daten verwendet werden kann, so liegt seine Stärke dennoch in der Verwendung als Zeiger. In diesem Fall dient er meist als Zeiger auf die Basis eines Stack-Frames und wird zum Ansprechen der Argumente von Prozeduren verwendet.
PUSH sum1 ;sum1 auf den Stack bringen
PUSH sum2 ;sum2 auf den Stack bringen
PUSH sum3 ;sum3 auf den Stack bringen
CALL addition ;Aufruf der Funktion addition
Addition PROC NEAR ;Near call mit vier Byte für alten eip als Rückkehradresse
PUSH ebp ;ebp sichern
MOV ebp, esp ;ebp mit esp (Top of Stack) laden
MOV eax, [ebp+8] ;sum3 nach eax kopieren
ADD eax, [ebp+12] ;sum2 zu eax addieren
ADD eax, [ebp+16] ;sum1 zu eax addieren
POP ebp ;alten ebp wiederherstellen
RET ;Rücksprung, (sum1+sum2+sum3) in eax
Addition ENDP
Ahnlich wie EBP kann der Source-Index ESI als allgemeiner temporärer Datenspeicher und als Zeiger benutzt werden. Meist wird ESI als Index eines Datenobjekts innerhalb eines Feldes oder einer ähnlichen Struktur verwendet, dessen Basis häufig durch das Basisregister EBX angegeben wird. Bei Zeichenkettenbefehlen zeigt ESI auf einzelne Bytes, Worte oder Doppelworte innerhalb der Source-Zeichenkette und wird bei wiederholter Ausführung des Befehls (durch REP) in Abhängigkeit vom Direction-Flag (DF) automatisch erhöht oder erniedrigt.
String DB 'a',1,'b',1,'c',1,'d',1,'e',1,'f',1,'g',1,'h',1,'i',1,'j',1 ;1 ist unterstrichen
MOV eax, @data ;Datensegment von string in eax laden
MOV ds, eax ;ds auf Datensegment von string einstellen
MOV eax, 0B800h ;Segment des Video-Ram nach eax laden
MOV es, eax ;Videosegment nach es kopieren
CLD ;von a nach j (aufsteigend) vorgehen
MOV ecx, 5 ;5 Worte á 4 Bytes (4 Zeichen + 4 Attribute) übertragen
MOV esi, OFFSET string ;Adresse von string nach esi laden
MOV edi, 00h ;Offset 0 im Video-RAM
REP MOVSW ;fünfmal ein Wort übertragen
Durch den Befehl REP MOVSW werden edi und esi nach jedem übertragenen Wort um zwei Byte erhöht und zeigen damit auf das nächste zu übertragende Wort
Der Destination-Index EDI stellt das Pendant zum Source-Index ESI dar und kann als allgemeiner temporärer Datenspeicher und als Zeiger benutzt werden.
Das Codesegment CS enthält die Befehle und Daten, die unmittelbar (immediate) adressiert werden. Die Befehle des Segments werden durch den Befehlszeiger EIP (Extended Instruction Pointer) adressiert. Das Codesegment wird bei einem Far-Call und einem INT automatisch verändert. Im Protected Mode prüft der i386 bei einer Anderung des Inhalts eines Segmentregisters automatisch, ob der aufrufende Task auch eine Zugangsberechtigung für das neue Segment hat.
Das Datensegment DS enthält Daten, die dem gerade laufenden Programm zugeordnet sind. Viele Befehle verwenden das Datensegment implizit, um Daten im Speicher zu adressieren. Erst eine Überschreibung mit einem anderen Segmentregister hebt diese automatische Segmentadressierung auf.
Das Stacksegment SS enthält Daten, auf die mittels Stack-Befehlen wie PUSH, POP, PUSHA, POPA etc. zugegriffen wird. Diese Befehle benützen den Wert von SS automatisch, um Daten im Speicher (auf dem Stack) abzulegen oder aus diesem zu lesen. Auch die sogenannten lokalen Variablen oder lokalen Daten von Prozeduren werden normalerweise auf dem Stack abgelegt. Sie werden dann nach einer Rückkehr von der Prozedur von einer anderen Prozedur wieder überschrieben. Beim Abspeichern von Daten auf dem Stack wird der zugehörige Stapelzeiger ESP (Stack Pointer) automatisch entsprechend dem Umfang der abgelegten Daten vermindert. Damit wächst der Stack von höheren zu niedrigeren Speicheradressen.
Diese Segmentadressen stehen für Zeichenkettenbefehle zur Verfügung. Außerdem können ES, FS und GS dazu benützt werden, das Standarddatensegment DS zu überschreiben, um die Daten einer Speicherzelle anzusprechen, die nicht in DS liegt, ohne den Wert in DS zu verändern.
Die sechs Segmentregister sind in allen Betriebsmodi des i386 für die Organisation und Adressierung des Speichers von erheblicher Bedeutung.
Bedingten Sprüngen geht nahezu immer ein logischer Vergleich zweier Größen (eine Prüfung einer Bedingung) voraus, wie schon das Wort bedingt impliziert. Von großer Bedeutung in diesem Zusammenhang ist das Flag-Register, da bestimmte Flags in Abhängigkeit vom Ergebnis des Vergleichs gesetzt (Flag gleich 1) oder gelöscht (Flag gleich 0) werden. Auch setzen und löschen manche Befehle bestimmte Flags. Bei den 16-Bit Prozessoren waren nur die unteren 16 der folgenden 32 Bit vorhanden, deswegen wird dieses Register auch EFlags genannt.
Im folgenden eine Beschreibung der einzelnen Flags des i386.
[Da bestimmte Flags in dieser Arbeit keine Bedeutung haben, werde ich diese (IP, VIP, VIF, AC,VM) nicht erklären]
Carry wird gesetzt, wenn ein Vorgang einen Übertrag für den Zieloperanden erzeugt. Dies ist beispielsweise der Fall, wenn bei einer 32-Bit Addition die Summe zweier 32-Bit-Zahlen ein Ergebnis größer als 4gByte-1 erzeugt. Carry kann durch den Befehl STC (Set Carry) gesetzt, durch CLC (Clear Carry) gelöscht und durch CMC (Complement Carry) komplementiert werden.
Parity wird gesetzt, falls das Ergebnis der Operation eine gerade Zahl von gesetzten Bits aufweist. Parity wird vom Prozessor gesetzt.
Das Flag wird für Arithmetik mit BCD-Zahlen verwendet und wird gesetzt, wenn eine Operation einen Übertrag oder ein Borrow für die unteren vier Bits (BCD-Zahlen belegen nur die unteren vier Bits eines Byte) eines Operanden erzeugt.
Zero wird vom Prozessor gesetzt, falls das Ergebnis einer Operation Null ergibt. Ein Beispiel liefert die Subtraktion gleich großer Zahlen oder das bitweise logische AND eines Wertes mit Null.
Sign ist gleich dem höchstwertigen Bit des Operationsergebnisses (0=positiv, 1=negativ) und hat damit nur einen Sinn für vorzeichenbehaftete Zahlen. Ist die Differenz zweier Zahlen negativ, so wird Sign vom Prozessor auf eins gesetzt. Damit können beispielsweise zwei Zahlen miteinander verglichen werden.
Ist Trap gesetzt, so erzeugt der Prozessor nach jedem Schritt einen Interrupt 1. Trap gehört also zur Klasse der Exceptions. Viele Debug-Programme setzen Trap und fangen den Interrupt ab, um ein Programm schrittweise auszuführen.
Ist Interrupt Enable gesetzt, so akzeptiert der Prozessor Hardware-Interrupts. Dieses Flag kann explizit mit CLI gelöscht und mit STI gesetzt werden. Interrupts müssen bei Anwendungen gesperrt werden, die keine Unterbrechung erlauben. Eine zu lange Sperrung kann jedoch zu Problemen bei Echtzeitanwendungen führen. Normalerweise sollte nur das Betriebssystem dieses Flag verändern.
Direction bestimmt die Richtung von String-Operationen (z.B. MOVS). Ist Direction gesetzt, so werden die Zeichenketten von hoher zu niedriger Adresse bearbeitet, ansonsten von niedriger zu hoher Adresse. Das Direction-Flag kann mit STD gesetzt und mit CLD gelöscht werden.
Overflow wird vom Prozessor gesetzt, falls das Ergebnis einer Operation für den Zieloperanden zu groß oder zu klein ist. Beispielsweise kann die Addition zweier 16-Bit-Zahlen zu einem Wert führen, der nicht mehr in ein 16-Bit-Register paßt.
Die bisher beschriebenen Register waren bereits auf dem 8086 vorhanden, die folgenden sind mit dem 80286 dazugekommen, um den Protected Mode zu unterstützen und einen Schutz für den I/O-Adreßbereich zu implementieren.
Dieses 2-Bit Flag gibt im Protected Mode die minimal benötigte Schutzebene für Ein- und Ausgabeoperationen für den I/O-Adreßraum an und wird vom Betriebssystem verwaltet. Im Real Mode hat das Flag keine Bedeutung.
Nested Task dient im Protected Mode zur Überwachung der Verkettung unterbrochener und aufgerufener Tasks und wird vom Betriebssystem verwaltet. Ein gesetztes NT-Flag zeigt an, daß mindestens ein Task-Switch aufgetreten ist und sich im Speicher ein inaktives Task-State-Segment befindet.
Beim i386 sind im Flag-Register noch zwei Einträge hinzugekommen, die einerseits die neu implementierte Debug-Unterstützung, andererseits den innovativen Virtual 8086 Mode betreffen (die jedoch beide für diese Arbeit nicht von all zu großer Bedeutung sind. Ich will sie nur der Vollständigkeit halber erwähnen).
Das Resume-Flag steuert den Wiederanlauf eines Tasks nach einer Breakpoint-Unterbrechung über die Debug-Register des i386. Ist Resume gesetzt, so werden die Breakpoints temporär deaktiviert. Das bedeutet, daß die Programmausführung an der Unterbrechungsstelle wiederaufgenommen werden kann, ohne daß eine erneute Debug-Exception auftritt.
Um den i386 in den Virtual 8086 Mode umzuschalten muß das Betriebssystem das VM-Flag setzen. Das ist nur im Protected Mode und hier nur über ein Gate möglich. Ist das VM-Flag gesetzt, so arbeitet der Prozessor im Virtual 8086 Mode. Er führt dann die vom 8086 her bekannte einfache Real-Mode-Adreßberechnung aus und kann dadurch Real-Mode-Anwendungen ausführen. Das alles geschieht aber im Gegensatz zum Real Mode in einer geschützten Umgebung. Ist das VM-Flag dagegen gelöscht, so arbeitet der i386 im gewöhnlichen Protected Mode. Im Real Mode hat das Flag keine Bedeutung.
Nahezu alle Befehle zu bedingten Sprüngen testen die Werte des Flags, das das Ergebnis des vorherigen Vergleichs widerspiegelt. Das macht die besondere Bedeutung des Flag-Registers aus.
CMP eax, 5 ;Register eax mit dem Wert 5 vergleichen
JE Marke1 ;Sprung zu Marke1, wenn der letzte Vergleich Gleichheit ergeben hat
Der i386 führt den Vergleich CMP eax, 5 aus, indem er den Wert 5 von eax subtrahiert und die Flags entsprechend setzt. In unserem Beispiel wird daher 5 vom Wert des Registers eax subtrahiert. Ist eax gleich 5, so wird das Zero-Flag gesetzt. Ist eax größer 5, so setzt der Prozessor die Flags Sign und Zero auf 0. Ist eax kleiner als 5, so wird Zero auf 0 und Sign auf 1 gesetzt.
Manche Befehle (wie zum Beispiel JBE = Jump if Below or Equal) testen mehrere Flags (hier: Sign und Zero) um zu ermitteln, ob die Sprungbedingung erfüllt ist oder nicht.
Der i386 besitzt vier eigentliche Steuerregister sowie vier Speicherverwaltungsregister für den Protected Mode. Die Steuer-Register sind jeweils 32 Bit breit.
Das bereits im 80286 implementierte Maschinenstatuswort (MSW) für die Unterstützung des 16-Bit Protected Mode beim 80286 ist im niederwertigen Wort des Steuerregisters CR0 aufgegangen. Die Bedeutung der Bits TS, EM, MP und PE ist daher dieselbe wie beim 80286. Aus Kompatibilitätsgründen kann das niederwertige MSW-Wort des CR0-Registers weiterhin über die 80286-Befehle LMSW (Load MSW) und SMSW (Store MSW) angesprochen werden.
Wenn dieses Bit gesetzt wird, schaltet der i386 in den Protected Mode um. Beim 80286 war eine Rückkehr zum Real Mode nur über einen Prozessor-Reset oder einen Dreifach-Fehler (Triple Fault) möglich; beim i386 kann das explizit durch einen Befehl vollzogen werden, der das PE-Bit löscht. Das darf aber nur ein Task mit der Privilegstufe 0 (das Betriebssystem), sonst wird eine Exception ausgelöst.
Wenn das MP-Bit gesetzt ist, dann löst der WAIT-Befehl die Exception "kein Coprozessor vorhanden" aus, die zu einem Interrupt 7 führt.
Ein gesetztes EM-Bit teilt dem i386 mit, daß alle Coprozessorfunktionen des i387 durch Software emuliert werden müssen, das heißt keine Datenübertragung zwischen der CPU und dem Coprozessor ausgeführt wird. Statt dessen führt jeder ESC-Befehl für den Coprozessor zu einer Exception 7, deren Handler dann den entsprechenden Befehl mit der Ganzzahlarithmetik des i386 ausführt.
Ist dieses Bit gesetzt, so ist ein Task-Switch aufgetreten; der i386 ist also im Protected Mode mindestens einmal auf ein Task-Gate gestoßen. Das TS-Bit ist wichtig, um zu ermitteln, ob der Coprozessor möglicherweise noch einen Befehl für den alten Task oder schon einen für den neu aktivierten berechnet. Da manche aufwendigen i387-Befehle bis zu 1000 Taktzyklen benötigen, ist es zumindest denkbar, daß die i386 CPU zu einem neuen Task umschaltet, diesen bedient und, bevor der Coprozessor seinen Befehl abgearbeitet hat, zum alten Task zurückschaltet.
Die vier beschriebenen Bits des CR0-Registers sind bereits im 80286-MSW implementiert. Beim i386 sind zwei Bits hinzugekommen, die zur Aktivierung oder Deaktivierung der Paging-Unit und zur Festlegung des Coprozessors als i387 oder 80287 dienen.
Mit dem ET-Bit wird festgelegt, ob der installierte Coprozessor ein i387 (ET=1) oder ein 80287 (ET=0) ist. Der i386 kann mit beiden zusammenarbeiten, weil die Schnittstelle zwischen beiden und das Protokoll für den Datenaustausch identisch sind. Dagegen ist die Breite des Datenbusses für den 80287 mit 16 Bits geringer. Dementsprechend mehr besondere I/O-Zyklen müssen der i386 und 80287 ausführen, um Codes und Daten auszutauschen.
Mit dem PG-Bit wird die Paging Unit (PU) in der Speicherverwaltungseinheit des i386 aktiviert (PG=1) oder deaktiviert (PG=0). Bei deaktivierter PU führt der i386 keine Adreßtransformation aus, die lineare Adresse nach Addition von Offset und Segmentbasis stellt automatisch die physikalische Adresse des Speicherobjekts dar, so wie der i386 über seine Adreßleitungen den Speicher physikalisch adressiert. Bei aktiver Paging Unit führt der i386 dagegen zusätzlich zur (schon aufwendig genug erscheinenden) Segmentierung noch eine weitere Adreßtransformation aus, um aus der linearen eine physikalische Adresse zu bilden. Paging kann nur im Protected Mode benutzt werden (Ich werde jedoch im Rahmen dieser Arbeit nicht näher darauf eingehen).
Wie bereits erwähnt, stehen für den Zugriff auf das MSW zwei besondere Befehle zur Verfügung, nämlich LMSW und SMSW. Wenn man aber auf das PG- oder das ET-Bit zugreifen möchte, so muß das Steuerregister CR0 mit einem MOV-Befehl angesprochen werden.
Möglichkeit: MOV CR0, 0001h ;PE-Bit durch MOV-Befehl mit 32-Operanden setzen
Möglichkeit: LMSW 01h ;PE-Bit durch LMSW-Befehl mit 16-Bit Operanden setzen
Dieses Kapitel behandelt die logische Adressierung des Speichers. Dazu ist eine eingehende Untersuchung der segmentierten Speicherorganisation und der beteiligten Register notwendig. Außerdem unterscheidet sich die Speicheradressierung je nach dem aktiven Betriebsmodus (Real Mode, Protected Mode, Virtual 8086 Mode).
Zur Programmausführung holt der Prozessor Befehle aus dem Speicher (Befehls-Prefetching) und führt diese dann aus. Grundlage für dieses automatische Lesen bilden Codesegment und Befehlszeiger. Das Codesegment gibt dabei das Segment an, aus dem der nächste Befehl gelesen werden soll. Der Befehlszeiger ist der Offset des nächsten zu lesenden Befehls. Das Paar Codesegment:Befehlszeiger bildet somit die Adresse des nächsten auszuführenden Befehls im Speicher. Der Prozessor kann damit diesen Befehl einlesen und ausführen. Am Code des Befehls erkennt der Prozessor, wie viele Bytes er einlesen muß, damit sich der vollständige Befehl im Prozessor befindet. Befehle für den 80x86 sind zwischen einem und 15 Bytes lang.
Ist der Befehl ausgeführt worden, so wird der Befehlszeiger um die Zahl der Bytes inkrementiert, die der gerade ausgeführte Befehl aufwies. Bei einem kurzen 2-Byte-Befehl wird der Befehlszähler also um zwei erhöht, das Codesegment:Befehlszeiger-Paar verweist dann auf den nächsten auszuführenden Befehl. Dieser wird in gleicher Weise eingelesen und ausgeführt. Anschließend inkrementiert der Prozessor den Befehlszeiger erneut. Dieses Einlesen und Inkrementieren führt die CPU dabei völlig selbständig aus, es ist kein Eingriff eines Steuerprogrammes oder gar des Benutzers notwendig. Einmal "angestoßen", fährt die CPU damit ständig fort, Befehle einzulesen und auszuführen.
Dieser gleichmäßige Befehlsstrom kann jedoch durch bedingte und unbedingte Sprünge und Verzweigungen unterbrochen und an anderer Stelle wieder fortgesetzt werden. Hierzu muß nur der Wert des Befehlszeigers und gegebenenfalls des Codesegments verändert werden. Bei einem Near-Call oder -Jump bleibt das Codesegment unverändert, es wird nur der Wert des EIP neu geladen. Demgegenüber wird bei einem Far-Call oder -Jump auch der Wert des Codesegments verändert. Der Prozessor fährt an einer anderen Stelle des im Speicher befindlichen Programmes fort. Sprünge - allgemeiner auch Verzweigungen oder Branches genannt - sind für den logischen Ablauf von Programmen sehr wichtig, weil ein Computer häufig in Abhängigkeit von bestimmten Bedingungen verschiedene Dinge ausführen soll.
Der Wert des Codesegments lautet 24D5, der Wert des Befehlszählers 0108. Der nächste Befehl befindet sich damit bei der Adresse 24D5:0108. Der Code an dieser Adresse lautet 8CC0. Die Steuereinheit CU dekodiert diesen Code und ermittelt den Befehl
MOV eax, es
Es soll also der Wert des Extrasegments es in das 32-Bit-Akkumulatorregister eax übertragen werden. Nach der Ausführung des Befehls wird der Wert des Befehlszählers um zwei erhöht, da <MOV eax, es> ein Zwei-Byte-Befehl war. Der Wert von EIP lautet somit 10a0, der Wert des Codesegments bleibt unverändert.
Eine besondere Bedeutung besitzt das Stacksegment SS sowie der zugehörige Stack-Pointer oder Stapelzeiger ESP. Jedes Programm besitzt normalerweise ein eigenes Stacksegment, auf dem mit PUSH der Wert eines Registers oder eines Speicherwortes abgelegt werden kann. Mit PUSHF können die Flags und ab dem 80186 mit PUSHA alle Vielzweckregister auf dem Stack gespeichert werden. Umgekehrt werden mit POP, POPF bzw. POPA (ab 80186) die entsprechenden Daten vom Stack wieder abgenommen. Dabei wächst der Stack nach unten, d.h. zu kleineren Werten des Stapelzeigers ESP. Werden Daten auf dem Stack abgelegt, so vermindert sich der Wert von ESP um vier, weil immer ein ganzes Doppelwort auf den Stack geschoben wird. Wird der i386 im 16-Bit-Modus betrieben, so werden stets nur zwei Byte auf dem Stack abgelegt und der Wert von SP mit jedem PUSH nur um zwei verringert. Das gilt natürlich auch für die 16-Bit-Vorgänger 8086 und 80286. Ist der Stack leer, dann nimmt der Stapelzeiger ESP seinen größten Wert an. Nach dem Ablegen desWortes zeigt der Stack-Pointer auf das zuletzt abgelegte Wort auf dem Stack. Ein Befehl PUSH dekrementiert also zuerst den Wert des Stapelzeigers ESP, und anschließend wird der Register- oder Speicherwert auf dem Stack abgelegt.
Durch das Wachsen des Stack nach unten kann ein Stapelüberlauf auf einfache Weise erkannt werden: Nimmt ESP den Wert 0 an, so ist der Stack erschöpft und bei entsprechend programmierten Anwen-dungsprogrammen, die den Stack laufend überprüfen, erscheint die Mitteilung Stapelüberlauf oder Stack-Overflow. Programmierer sehen jedoch meist einen ausreichend großen Stack (ausreichend großen Anfangswert für ESP) vor, so daß ein solcher Stack-Overflow nur bei einem Programmfehler oder einer fehlerhaften Programmbedienung auftreten sollte. Nachteilig ist, daß im Real Mode das Anwendungsprogramm vor jedem PUSH explizit prüfen muß, ob noch ausreichend Kapazität auf dem Stack frei ist. Im Protected Mode wird die Prüfung, ob ein Stacküberlauf vorliegt, von der Hardware des Prozessors erledigt. Damit ist eine sehr schnelle Überprüfung möglich, ohne daß zusätzliche Software-Routinen notwendig sind.
Neben dem Code- und Stacksegment hat auch das Datensegmentregister DS eine besondere Bedeutung. Es ist immer dann wichtig, wenn ein Befehl Daten aus dem Speicher liest oder in ihm abspeichert, d.h. wenn Speicheroperanden betroffen sind. Der Offset des Speicheroperanden wird üblicherweise in einem der Vielzweckregister bereitgehalten, und das Paar DS:Offset verweist auf den anzusprechenden Wert. Das Datensegmentregister DS wird standardmäßig als das einem Offset zugeordnete Segmentregister verwendet. Wenn dagegen ein Wert in einem anderen Segment geschrieben oder gelesen werden soll, muß das Segmentregister DS mit dem Wert eines neuen Segments geladen oder ein Segmentpräfix benutzt werden, das das Segment DS durch eines der Extrasegmente ES bis GS ersetzt.
Die Daten des Codesegments sollten dabei nur ausführbar und höchstens noch lesbar, nicht aber überschreibbar sein. Ein Überschreiben von Code führt notwendigerweise zum Absturz eines Programmes. Nur-ausführbare Daten sind nicht in Vielzweck- oder Segmentregister einlesbar. Ein Programm kann sie also nicht im Sinne von Daten verwenden, die verarbeitet werden. Die Verwendung verschiedener Segmente für Code, Stack und Daten gestattet eine Trennung der verschiedenen Abschnitte eines Programmes. Im Protected Mode wird davon intensiv Gebrauch gemacht, um ein versehentliches Überschreiben von Code durch einen Programmfehler (eine häufige Ursache hängender Programme) zu vermeiden. Selbstverständlich können alle Segmentregister denselben Wert aufweisen. In diesem Fall findet keine Trennung von Code, Stack und Daten statt. Die .COM-Programme unter MS-DOS sind in dieser Weise strukturiert. .COM-Programme sind Relikte aus den Zeiten von CP/M, einem Betriebssystem für einfachere 8-Bit-Prozessoren. Sie unterstützen keinen in Segmente aufgeteilten Speicher. Damit ist der Adreßraum für Code und Daten auf zusammen 64kByte (ein Segment) begrenzt.
Befehle, wie MOV, CALL, JNZ werden als mnemonische Codes oder Mnemonics bezeichnet. Sie dienen nur dazu, dem Programmierer eine Gedächtnisstütze zu liefern, da die Mnemonics die Operation des entsprechenden Befehls in verkürzter Form angeben. Ein Assembler versteht diesen mnemonischen Code und führt eine entsprechende Codierung in einen Maschinenbefehl aus. Maschinenbefehle sind - wie könnte es anders sein - eine Folge von Nullen und Einsen mit einem oder mehreren Byte Länge. Wenn nun ein Programm mit TYPE ausgegeben wird, so werden diese Codes als ASCII-Zeichen interpretiert und anscheinend völlig wirres Zeug ausgegeben.
Soll ein Register (hier beispielsweise der Akkumulator eax) über MOV eax,<wert> mit einem Wert geladen werden, dann stehen drei Möglichkeiten zur Verfügung:
Unmittelbarer Operand (Immediate):
MOV eax,6a02h
Das Akkumulatorregister eax wird mit dem Wert 6a02h geladen. Dieser Wert wird
zur Programmierzeit fest vorgegeben und ist Bestandteil des Programmcodes, d.h.
er erscheint als Bestandteil des Befehlsstromes, der aus dem Speicher geladen
wird. Das zugeordnete Segment ist also das Codesegment CS und nicht das
Datensegment DS oder ein Extrasegment.
Registeroperand: MOV
eax, ebx
Das Register eax wird mit dem Wert im Register ebx geladen. Auch im obigen
Beispiel war eax ein Registeroperand (nämlich Ziel- oder Destination-Operand).
Hier ist nun auch der Quellen- oder Source-Operand ein Register (nämlich ebx).
Speicheroperand: MOV
eax, mem32
Anstelle von mem32 muß in diesem Fall die effektive Adresse des symbolischen
Operanden stehen, der in den Akkumulator übertragen werden soll. Ist die
effektive Adresse fest, d.h. bereits zur Assemblierzeit bekannt, wird sie
bereits vom Assembler berechnet. mem32 ist in diesem Fall ein direkter
Speicheroperand. Weist die effektive Adresse einen veränderlichen Anteil
(meist ein Register) auf, so berechnet die CPU zur Laufzeit die effektive
Adresse. In diesem Fall stellt mem32 einen indirekten Speicheroperanden
dar.
Als effektive Adresse bezeichnet man den Offset des Operanden innerhalb des ausgewählten Segments (hier: DS). Die effektive Adresse setzt sich aus bis zu vier Elementen zusammen.
Displacement: MOV
eax, array[0]
Im assemblierten Programm steht anstelle der symbolischen Adresse array[0]
eine Zahl. Befindet sich array[0] beispielsweise an der Stelle 0f2h, so lautet
der Befehl in diesem Fall MOV eax,[0f2h]. Diese Art der Adressierung darf nicht
mit einem unmittelbaren Operanden verwechselt werden: Bei einem Displacement
wird der Wert an der angegebenen Adresse und nicht der Wert selbst
angesprochen. In diesem Beispiel also der Wert am Offset 0f2h im Segment DS und
nicht der Wert 0f2h selbst.
Basisregister: MOV
eax, [ebx]
Der Befehl lädt den Operanden im Segment DS bei dem Offset, den der Wert im
Register ebx angibt, in den Akkumulator. Weist ebx beispielsweise den Wert 0f2h
auf, so ist dieser Befehl äquivalent zu MOV eax,[0f2h]. Der Unterschied besteht
nur darin, daß der Wert in ebx zur Laufzeit dynamisch verändert werden kann,
array[0] jedoch einen während des Programmablaufs stets festen und konstanten
Wert darstellt.
Indexregister: MOV
eax,[esi]
In dieser Grundform ist die Verwendung eines Indexregisters identisch mit
der Benutzung des Basisregisters. Zusätzlich besteht aber noch die Möglichkeit,
einen Skalierungsfaktor anzugeben. Als Indexregister sind esi und edi gültig.
Skalierungsfaktor:
MOV eax, [esi*4]
Um die effektive Adresse zu berechnen, wird beim angegebenen Beispiel der
Wert des Indexregisters mit 4 multipliziert. Hierdurch können Felder
angesprochen werden, deren Inhalt vier Byte lang ist. Für den Skalierungsfaktor
können die Faktoren 1, 2, 4 oder 8 verwendet werden, die Skalierung
(Multiplikation) selbst erfolgt ohne Zeitverlust.
Displacement, Basisregister, Indexregister und Skalierungsfaktor können in beliebiger Kombination angewandt werden. Damit können sehr ausgeklügelte und mehrdimensionale Felder definiert werden.
Gegeben sei ein Feld mit 10 Elementen, das 10 verschiedene Körper bezeichnet. Jedes Element besitzt den Aufbau Höhe:Breite:Tiefe:Querschnitt. Die Teilelemente Höhe, etc. umfassen jeweils 1 Byte. Das Feld beginnt bei 0c224h. Durch die folgenden Zeilen kann die Tiefe der Elemente in den Akkumulator eax geladen werden.
MOV ebx, 0c224h ;Laden der Basisadresse in ebx
MOV esi, <nr> ;Laden der Elementnummer in esi
MOV eax, [ebx+esi*4+2] ;Tiefe (Displacement 2 gegenüber dem Beginn des Elements) des Elements <nr> ;(Elementgröße = 4 Byte, daher Skalierung 4) des Feldes (beginnend bei der Basisadresse in ;ebx) in den Akkumulator eax laden.
Daß der Prozessor unentwegt Befehle ausführt ist wohl unschwer zu erkennen. Auch, wenn er scheinbar darauf wartet, daß ein Befehl (z.B. dir) eingegeben wird, bedeutet das nicht, daß er wirklich angehalten wird. Vielmehr läuft im Hintergrund eine Routine ab, die überprüft, ob bereits Zeichen eingegeben worden sind. Nur bei Computern mit Power-Management wird die CPU wirklich stillgelegt, um beim ersten Tastendruck sofort wieder aktiviert zu werden.
Um den Prozessor bei der kontinuierlichen Abarbeitung dieser Befehle gezielt zu unterbrechen, wird ein sogenannter Interrupt ausgelöst. Beispielsweise wird ein periodischer Interrupt, der Timer-Interrupt, dazu benutzt, regelmäßig das Hintergrundprogramm PRINT für eine kurze Zeit zu aktivieren. Für den i386 stehen insgesamt 256 verschiedene Interrupts (0 bis 255) zur Verfügung. INTEL hat die ersten 32 Interrupts für eine Verwendung durch den Prozessor reserviert, was IBM allerdings nicht daran gehindert hat, alle Hardware-Interrupts und die Interrupts des PC-BIOS in genau diesen Bereich zu legen. Zwingende Gründe dafür kennt wohl kein Mensch.
In Abhängigkeit von der Quelle einer Unterbrechung (dt. für Interrupt) unterscheidet man drei Kategorien von Interrupts:
Software-Interrupts
Hardware-Interrupts
Exceptions (Ausnahmen)
Ein Software-Interrupt wird gezielt durch einen INT-Befehl ausgelöst, z.B. ruft der Befehl INT 10h den Interrupt mit der Nummer 10h auf.
Im Real-Mode-Adreßraum des i386 sind die ersten 1024(1k) Byte für die Interrupt-Vector-Table reserviert. Diese Tabelle weist für jeden der 256 möglichen Interrupts einen sogenannten Interrupt-Vektor auf. Beim 8086 war die Lage dieser Tabelle fest von der Hardware vorgegeben. Der i386 verwaltet sie selbst im Real Mode etwas flexibler. Von allen Speicherverwaltungsregistern besitzt das Interrupt-Descriptor-Table-Register IDTR bereits im Real Mode eine Bedeutung. Es speichert nämlich die Basisadresse und das Limit der Real-Mode-Deskriptortabelle. Nach einem Prozessor-Reset wird das IDTR standardmäßig mit den Werten 0h für die Basis, 03ffh für das Limit geladen. Das entspricht genau einer 1-kByte-Tabelle im Segment 0000h, Offset 0000h. Durch die beiden i386-Befehle LIDT (Load IDT) und SIDT (Store IDT) können die beiden Werte aber verändert und die Tabelle dadurch im Real-Mode-Adreßraum verschoben und deren Größe verändert werden. Es ist aber darauf zu achten, daß die Tabelle auch alle Vektoren für die möglicherweise auftretenden Interrupts aufnehmen kann. Sonst ist eine Exception 8 die Folge.
Jeder Interrupt-Vektor ist vier Bytes lang und gibt im INTEL-Format die Adresse Segment:Offset des Interrupt-Handlers an, der den Interrupt bedient. Da ein Interrupt meist eine bestimmte Ursache hat, wie die Anforderung einer Betriebssystemfunktion, der Empfang eines Zeichens über die serielle Schnittstelle etc., behandelt der Interrupt-Handler diese Ursache in geeigneter Weise. Er führt also beispielsweise die Betriebssystemfunktion aus oder nimmt das empfangene Zeichen entgegen. Durch Ersetzen des Handlers kann einem Interrupt auf einfache Weise eine andere Funktion zugewiesen werden. Die Zuordnung von Interrupt und Interrupt-Vektor verläuft auf einer 1:1 Basis, d.h. dem Interrupt 0 wird der Interrupt-Vektor 0 (an der Adresse 0000:0000), dem Interrupt 1 der Vektor 1 (an der Adresse 0000:0004) usw. zugeordnet. Damit muß nur die Nummer des Interrupts mit 4 multipliziert werden, um den Offset des Interrupt-Vektors im Segment 0000h zu erhalten. Der Prozessor tut genau dies. Ein Überschreiben der Interrupt-Vector-Table mit ungültigen Werten hat katastrophale Folgen. Beim nächsten Interrupt stürzt der Rechner ab.
Beim Aufruf eines Interrupts läuft nun folgende Prozedur ab, die der i386 automatisch und ohne weiteres Eingreifen eines Programmes ausführt:
Der i386 schiebt die EFlags, CS und EIP - in dieser Reihenfolge - auf den Stack (im 16-Bit Modus natürlich nur Flags, CS und IP),
das Interrupt- und das Trap-Flag werden gelöscht,
der i386 adressiert den Interrupt-Vektor in der Interrupt-Vector-Table entsprechend der Nummer des Interrupts und lädt EIP (oder IP im 16-Bit Modus) sowie CS aus der Tabelle.
Der Prozessor schiebt die aktuellen Flags, CS und IP auf den Stack, löscht das Interrupt- und Trap-Flag und liest den Interrupt-Vektor an der Adresse 0000:0040h. Die zwei Byte bei 0000:0040 werden in IP, die beiden Byte bei 0000:0042 in CS geladen.
Für alle Interruptbefehle ist nur eine unmittelbare Adressierung möglich, d.h. die Nummer des Interrupt-Vektors ist Teil des Befehlsstromes. Damit kann die Nummer des Interrupts nicht in einem Register oder Speicheroperanden bereitgehalten werden. Software-Interrupts treten synchron zur Befehlsausführung auf, d.h. jedesmal, wenn das Programm den Punkt mit dem INT-Befehl erreicht, wird ein Interrupt ausgelöst. Das unterscheidet sie wesentlich von den Hardware-Interrupts und Exceptions.
Wie schon der Name sagt, werden diese Unterbrechungen durch einen Hardware-Baustein (beim Timer-Interrupt z.B. durch den Timer-Baustein) oder ein Peripheriegerät wie beispielsweise die Festplatte ausgelöst. Man unterscheidet zwei grundsätzlich verschiedene Arten von Hardware-Interrupts: den nicht-maskierbaren Interrupt NMI sowie die (maskierbaren) Interruptanforderungen IRQ (Interrupt Request). Für die Bedienung eines solchen IRQ spielt der Interruptcontroller 8259 eine große Rolle. Er verwaltet mehrere Interrupt-Anforderungen und gibt sie geordnet nach ihrer Priorität gezielt an den Prozessor weiter.
Löst der Computer einen NMI aus, so arbeitet der i386 den gerade ausgeführten Befehl ab und führt unmittelbar anschließend in gleicher Weise wie oben einen Interrupt 2 aus. Beim PC wird ein NMI ausgelöst, wenn beim Lesen von Daten aus dem Speicher ein Paritätsfehler oder ein anderes ernstes Hardware-Problem wie z.B. eine fehlerhafte Busarbitrierung, auftritt. Der Computer meldet sich bei einem Paritätsfehler mit der folgenden Nachricht:
Paritätsfehler bei xxxx:xxxx
xxxx:xxxx gibt das Byte mit dem Paritätsfehler an. Die Besonderheit des NMI ist, daß er (wie bereits der Name sagt) nicht unterdrückt werden kann. Ein NMI drängelt sich immer nach vorne. Da er normalerweise nur bei einer wirklich schwerwiegenden Fehlfunktion der Hardware ausgelöst wird, ist dies aber verständlich und auch richtig: Ein PC mit unzuverlässigem Speicherinhalt muß am Datenselbstmord gehindert werden.
Demgegenüber können die Interrupt-Anforderungen IRQ maskiert werden, indem man mit CLI (Clear Interrupt Flag) das Interrupt Flag IE löscht. Alle Interrupt-Anforderungen über den Anschluß INTR des i386 werden dann ignoriert. Erst durch den umgekehrten Befehl STI (Set Interrupt Flag) werden solche Interrupts wieder zugelassen. Zu beachten ist, daß der Befehl INT xx implizit ein CLI ausführt. Nach einem INT müssen also Interrupt-Anforderungen mit einem STI explizit wieder zugelassen werden, sonst wird der Computer taub. IRQs werden üblicherweise durch ein Peripheriegerät ausgelöst, beispielsweise die serielle Schnittstelle oder den Drucker.
Hardware-Interrupts (NMI und IRQ) sind im Gegensatz zu Software-Interrupts völlig asynchron. Es tritt ja z.B. nicht immer an der gleichen Programmstelle ein Speicherparitätsfehler auf. Abgesehen davon benötigt die Festplatte in Abhängigkeit von der Ausgangsstellung der Festplattenköpfe eine unterschiedliche Zeitspanne, bis die Daten zum Programm übertragen werden können. Dies macht die Erfassung von Programmfehlern sehr schwierig, wenn sie nur im Zusammenhang mit Hardware-Interrupts auftreten.
Neben den beiden obigen bildet der Prozessor selbst eine Quelle für Interrupts. Solche vom Prozessor erzeugten Ausnahmen nennt man Exceptions. Die Auswirkungen einer Exception entsprechen dabei einem Software-Interrupt, d.h. es wird ein Interrupt aufgerufen, dessen Nummer in diesem Fall vom Prozessor selbst angegeben wird. Ursache für eine Exception ist im allgemeinen eine prozessorinterne Fehlerbedingung, die den Eingriff von System-Software erfordert und vom i386 nicht mehr alleine behandelt werden kann.
Exceptions werden in drei Klassen eingeteilt: Faults, Traps und Aborts. Im folgenden kurz die Kennzeichen der drei Klassen:
Fault: Ein Fault löst eine Exception aus, bevor der Befehl vollständig abgearbeitet wird. Der gesicherte EIP-Wert zeigt damit auf den Befehl, der die Exception ausgelöst hat. Durch laden des gesicherten EIP-Wertes kann der i386 den Befehl nochmals ausführen, hoffentlich ohne erneut eine Exception auszulösen. Ein Beispiel für einen Fault wäre die Exception "Segment nicht vorhanden". Der i386 hat dann die Möglichkeit, das Segment in den Speicher zu laden und einen erneuten Zugriffsversuch zu wagen.
Trap: Ein Trap löst dagegen eine Exception aus, nachdem der Befehl ausgeführt worden ist. Der gesicherte EIP-Wert zeigt also auf den Befehl unmittelbar nach dem, der die Exception ausgelöst hat. Der Befehl wird also nicht nochmals ausgeführt. Traps sind günstig, wenn der Befehl zwar korrekt ausgeführt worden ist, aber das Programm trotzdem unterbrochen werden soll. Das gilt z.B. für die Haltepunkte eines Debuggers; ein nochmaliges Ausführen des entsprechenden Befehles würde zu einem fehlerhaften Befehlsfluß führen.
Abort: Ein Abort liefert im Gegensatz zu Faults und Traps nicht immer die Adresse des Fehlers. Dadurch kann nach einem Abort die Programmausführung manchmal nicht wieder aufgenommen werden. Aborts werden nur dazu benutzt, äußerst schwerwiegende Fehlfunktionen anzuzeigen, wie z.B. Hardware-Ausfälle oder ungültige Systemtabellen.
Ein großer Teil der Exceptions ist für den Betrieb des i386 im Protected Mode vorgesehen. Im Real Mode sind nur folgende von Bedeutung:
Division durch 0 (Exception 0): Ist bei einem Befehl DIV oder IDIV der Divisor gleich Null, dann ist das Ergebnis der Operation nicht einmal mathematisch definiert, geschweige denn im i386. Die ALU würde für die Berechnung eines solchen Quotienten unendlich lange brauchen. Ist eine Division nicht nach einer bestimmten Zahl von Taktzyklen beendet, ermittelt die Steuereinheit eine Division durch Null und löst eine Exception 0 aus.
Einzelschritt (Exception 1): Ist das Trap-Flag gesetzt worden, so löst der i386 nach jedem einzelnen ausgeführten Befehl eine Exception 1 aus. Da das Trap-Flag beim Aufruf eines Interrupts automatisch gelöscht wird, kann der Prozessor die Interrupt-Routine ohne Unterbrechung durchführen. Häufig setzen Debugger das Trap-Flag und fangen den Interrupt ab, um ein Programm schrittweise auszuführen.
Breakpoint (Exception 3): Unter bestimmten Umständen (in den Debug-Registern festlegbar) löst der i386 bei Schreib- oder Leseversuchen auf bestimmte Speicherstellen eine Exception 3 aus.
Überlauferfassung mit INTO (Exception 4): Ist das Overflow-Flag gesetzt und wird der Befehl INTO ausgeführt, so erzeugt der Prozessor eine Exception 4.
BOUND (Exception 5): Liegt der mit dem Befehl BOUND geprüfte Index in ein Feld außerhalb der Grenzen des Feldes, so erzeugt der i386 eine Exception 5.
Ungültiger Opcode (Exception 6): Stößt die Befehlseinheit auf einen Opcode, dem kein Befehl zugeordnet ist, so löst der Prozessor eine Exception 6 aus. Die Ursache dafür ist meist ein fehlerhafter Assembler oder Compiler, oder ein Programmfehler, der zu einem Sprung an eine Stelle führt, an der eigentlich kein Opcode, sondern z.B. ein Datenbyte steht.
Kein Coprozessor vorhanden (Exception 7): Erfaßt die Befehlseinheit einen Opcode, der einen Befehl für den Coprozessor angibt, und ist gar kein Coprozessor installiert, so löst der i386 eine Exception 7 aus.
IDT-Limit zu klein (Exception 8): Ist das Limit der IDT zu klein, um den Vektor des ausgelösten Interrupts aufzunehmen, dann führt das zu einer Exception 8. Das ist meist der Fall, wenn ein Programm die IDT in unkorrekter Weise verändert hat.
Stack-Exception (Exception 12): Der Stapelzeiger ESP liefert einen größeren Wert als 0ffffh für den Stack-Zugriff. Das kann vorkommen, weil ESP ein 32-Bit-Register darstellt und sein Wert nicht auf unter 1mByte beschränkt ist. Eine Exception 12 ist die Folge, um die Erzeugung von linearen Adressen über 10ffefh im Real Mode zu unterbinden.
Allgemeiner Protection-Fehler (Exception 13): Ein 32-Bit-Offset liefert eine größere Adresse als 0ffffh für einen Speicherzugriff. Wie für den Stapelzeiger wird dadurch die Erzeugung von linearen Adressen größer als 10ffefh im Real Mode unterbunden.
Coprozessor-Fehler (Exception 16): Im Coprozessor ist ein Fehler aufgetreten.
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 i386-Hardware den Zugriff auf insgesamt vier Schutzebenen. Damit sind Daten und Code geschützt und ein kompletter Systemabsturz ist normalerweise - außer bei unsauberer Programmierung - nicht möglich.
Die Zugriffsprüfungen im Protected Mode dienen vor allem zur Hardwareunterstützung eines Multitasking-Betriebssystems; typische Vertreter sind Linux, Windows NT oder UNIX. Unter einem Multitasking-Betriebssystem laufen mehrere Programme (Tasks) scheinbar parallel ab. Doch genau genommen werden die einzelnen Tasks vom Betriebssystem in kurzen Zeitabständen aktiviert, laufen eine kurze Zeit lang ab und werden dann vom Betriebssystem wieder unterbrochen, damit der nächste Task aktiviert werden kann. Im Gegensatz zu TSR(Terminate and Stay Residental)-Programmen unter MS-DOS werden die Programme also gezielt vom Betriebssystem aufgerufen. TSR-Programme hingegen 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, d.h. es wird ein Task-Switch ausgeführt, so daß der Benutzer den Eindruck hat, die Programme würden parallel arbeiten. Durch die Hardware-Unterstützung des i386 im Protected Mode können diese Task-Switches schnell und effektiv ausgeführt werden.
Ein weiterer wesentlicher Unterschied des Protected Mode gegenüber dem Real Mode ist die völlig andersartige Berechnung der linearen Adresse eines Speicherobjekts.
Wie schon beschrieben, multipliziert die Adressierungseinheit des i386 im Real Mode den Wert des Segmentregisters einfach mit 16, um die lineare Basisadresse des betreffenden Segments zu ermitteln. Die Adresse innerhalb eines solchen 64kByte großen Segments wird dann durch den Offset in einem Vielzweckregister angegeben. Im Protected Mode werden die Werte der Segmentregister völlig anders interpretiert, sie stellen keine Basisadressen, sondern sogenannte Selektoren dar.
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 (RPL: Requestor Privilege Level) an, ab der ein Programm auf das Segment zugreifen darf. Der Wert des Feldes RPL im Segmentselektor des Code-Segments CS wird als gegenwärtige Privilegierungsstufe (CPL: Current Privilege Level) bezeichnet, da dem gegenwärtig aktiven Programmcode diese Privilegierungsstufe zugewiesen ist. Das aktive Programm kann also auf Datensegmente zugreifen, deren Privilegierungsstufe gleich oder größer als CPL ist. Dabei kann der Wert von RPL größer als CPL sein, d.h. 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 i386 kennt insgesamt vier Privilegierungsstufen (PL) 0 bis 3 - 0 kennzeichnet die höchste, 3 die niedrigste Stufe. Ein größerer Wert gibt also eine niedrigere, ein kleinerer eine höhere Privilegierungsstufe an. 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öherer CPL) dürfen nur in Ausnahmefällen auf Segmente mit höherer Stufe (kleinerer CPL) zugreifen. Hierzu dienen sogenannte Gates (Tore). Damit wird der Schutz innerhalb eines Tasks durch die verschiedenen Privilegierungsstufen vermittelt.
Die höchste Privilegierungsstufe 0 besitzt üblicherweise der kritische Teil oder Kernel des Betriebssystems. Dieser umfaßt zumeist die Routinen zur Speicherverwaltung, zum Ausführen von Task-Switches, zu Behandlung kritischer Fehler, usw. Viele Befehle, die den Status des Prozessors oder Computers direkt betreffen, wie beispielsweise LGDT (Load Global Descriptor Table) oder LMSW (Load Machine Status Word) können im Protected Mode nur von einem Programm ausgeführt werden, das die Privilegierungsstufe 0 aufweist. Damit soll verhindert werden, daß Anwendungsprogramme durch einen Programmierfehler das Betriebssystem zerstören oder Hacker unkontrolliert Zugriff auf Daten bekommen.
Betriebssysteme verwalten aber den Computer nicht nur, sondern stellen auch bestimmte Funktionen zur Datenverwaltung, Zeichenausgabe etc. 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 arbeiten häufig auf dieser Stufe. Weniger kritische Betriebssystemfunktionen, z.B. Unterstützungen für eine graphische Benutzeroberfläche (API), können dagegen Stufe 2 aufweisen.
Die niedrigste Privilegierungsstufe aller Tasks besitzen Anwendungsprogramme, da diese den Computer nur benutzen, aber nicht steuern oder kontrollieren sollen. Durch die niedrige Stufe (hohe 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 dem Abbruch des fehlerhaften Programms, alle anderen Tasks laufen unbeschädigt und unbeeinflußt weiter. Damit können Bugs, d.h. Programmfehler, besser entdeckt werden. Dies alles setzt natürlich voraus, daß das Betriebssystem fehlerfrei ist, was wegen der Komplexität von Multitasking-Betriebssystemen leider keine Selbstverständlichkeit darstellt.
Der i386 sieht für jede Privilegierungsstufe PL=0 bis PL=3 eines Tasks einen eigenen Stack vor. Beispielsweise wäre bei der Ausführung von Word ein Stack für das Programm Word (PL=3), einer für die Funktionen der Benutzeroberfläche (PL=2), ein weiterer für die Betriebssystemfunktionen zur Dateiverwaltung (PL=1) und ein letzter für 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 z.B. ein Anwendungsprogramm Betriebssystemfunktionen in Anspruch nimmt, um Dateien zu eröffnen oder zu schließen - es also auf fremde Funktionen zugreift -, garantieren die Gates, daß der Zugriff fehlerfrei lä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 gibt als sogenannter 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 i386 verwendet im Protected Mode die Segmentselektoren in den Segmentregistern nämlich als Index in die globale oder lokale Deskriptortabelle.
Die globale Deskriptortabelle ist eine Liste im Speicher, die in Form von Segmentdeskriptoren Größe und Adresse von Segmenten im Speicher beschreibt. Jeder Deskriptor umfaßt acht Byte. Diese Deskriptortabelle wird als global bezeichnet, weil sie Segmente beschreibt, die üblicherweise allen Tasks zur Verfügung stehen (wenn deren Privilegierungsstufe oder entsprechende Gates den Zugriff gestatten). Auch die lokale Deskriptortabelle ist eine Liste mit gleichartig aufgebauten Segmentdeskriptoren. Im Gegensatz zur globalen Deskriptortabelle steht die lokale aber normalerweise nur dem gerade aktiven Task zur Verfügung, d.h. bei einem Task-Switch wird auch die lokale Deskriptortabelle gewechselt.
Die 32 Basisbits des Segmentdeskriptors geben die Startadresse des beschriebenen Segments im Speicher an. Diese 32 Bits der Basisadresse entsprechen den 32 Adreßleitungen des i386. Damit können im Protected Mode 232 Byte oder 4gByte adressiert werden. Der Basiseintrag eines Segmentdeskriptors gibt die Startadresse des betreffenden Segments in diesem doch recht großen Adreßraum an.
Im Real Mode ist jedes Segment 64kByte groß, selbst, wenn nur wenige Daten in einem Segment gespeichert werden. Durch die festgelegte Verzahnung der Segmente bleiben aber höchstens 15 Byte unbenutzt, da die Segmente mit 16 Byte Abstand aufeinander folgen. Die völlige Entkoppelung 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 auf, um die tatsächliche Größe des Segments in Byte zu definieren. Bei gelöschtem G-Bit (Page-Granularität [=Körnigkeit]) sind durch die 20 Bits des Limiteintrags im Protected Mode also Segmente mit einer maximalen Größe von 220*1Byte (=1mByte) möglich, bei gesetztem G-Bit hingegen Segmente mit einer Größe von bis zu 220*4kByte (=4gByte).
Das tatsächliche Segmentlimit (oder gleichbedeutend die Größe) des vom Deskriptor beschriebenen Segments hängt also neben dem 20-Bit-Limit noch vom Granularity-Bit G ab. Zu beachten ist, daß beim i386 eine Page 4kByte (=4,096 Byte) groß ist.
Limit=1000, G=0: Segmentlimit 1000 Byte
Limit=1000, G=1: Segmentlimit 1000*4kByte=4096000 Byte
Der volle Adressierungsbereich der 32-Bit-Offset-Register kommt daher erst bei Page-Körnigkeit zum Tragen. In diesem Fall ist die kleinste Zuweisungseinheit für ein Segment aber bereits 4kByte groß, d.h. der Speicher wird in Portionen zu 4kByte in Segmente eingeteilt. Das kommt nur für größere Systeme in Betracht, für PC-Anwender sind Segmente mit einer Größe von 1mByte meist ausreichen, der Speicherplatz wird dann besser ausgenutzt. OS/2 und auch Windows NT verwenden aber ein sogenanntes flaches Speichermodell (flat memory), bei dem ein einziges 4gByte großes Segment alle Tasks aufnimmt, d.h. die Segmentierung spielt hier fast keine Rolle mehr. Statt dessen wird eine Speicheraufteilung in Pages vorgenommen, für die weitere Schutzmechanismen existieren. [im Rahmen dieser Arbeit werden die Paging-Mechanismen jedoch nicht dargestellt]
Das Bit S im Segmentdeskriptor gibt an, um welchen Deskriptortyp es sich handelt. Ist S gleich 0, so beschreibt der Deskriptor ein Systemsegment, ansonsten ein Applikations-segment. Das Feld Type im Segmentdeskriptor gibt die Art und DPL (Descriptor PL) die Privilegierungsstufe (von 0 bis 3) des Segments an. Schließlich ist das Bit P (Present) ein Indikator dafür, ob sich das Segment tatsächlich im Speicher befindet (oder auf Festplatte ausgelagert ist etc.).
Das Bit AVL im Segmentdeskriptor kann der Anwender oder das Betriebssystem frei benutzen, der i386 verwendet es nicht, und Intel hat es auch nicht für künftige Verwendung reserviert.
Das Bit DB (Default/Big) gibt an, ob der i386 für das vom Deskriptor beschriebene Segment standardmäßig 16- oder 32-Bit-Operanden im Fall eines Datensegments bzw. 16- oder 32-Bit-Adressen für ein Codesegment verwenden soll. Ist DB=0, so benutzt der i386 16-Bit-Operanden oder -Adressen. Dieses Bit dient vor allem der Emulation von 16-Bit-Programmen, die für den 80286 geschrieben worden sind, auf einem i386.
Für die Verwaltung der lokalen und globalen Deskriptortabelle sowie der weiter unten beschriebenen Interrupt-Deskriptortabelle und der Tasks implementiert der i386 fünf Register. Das sind das Steuerregister CR0 und die vier Speicherverwaltungs- oder Taskregister (TR), sowie die Register für die lokale Deskriptortabelle (LDTR), die Interrupt-Deskriptortabelle (IDTR) und die globale Deskriptortabelle(GDTR).
Der Index, d. h. die höherwertigen 13 Bits des Segmentselektors geben nun die Nummer des Segmentdeskriptors in der Deskriptortabelle an, der das zugehörige Segment beschreibt. Mit 13 Bits sind maximal 8192 verschiedene Indices möglich, so daß die globale und lokale Deskriptortabelle jeweils maximal 8192 Einträge zu 8 Byte oder insgesamt 64kByte 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 i386 auf einen Eintrag in der GDT oder LDT zugreifen, multipliziert er den Indexwert des Selektors mit 8 (Anzahl der Byte pro Deskriptor) und addiert das Ergebnis zur Basisadresse der entsprechenden Deskriptortabelle.
Das niederwertige Wort des Steuerregisters CR0 ist bereits beim 80286 als Maschinenstatuswort (MSW) vorhanden und kann beim i386 aus Kompatibilitätsgründen auch so adressiert werden.
Von besonderer Bedeutung für den Protected Mode ist das Bit PE (Protection Enable). Wenn es auf 1 gesetzt wird, schaltet der i386 sofort in den Protected Mode um. Gelöscht wird es entweder explizit durch den Befehl MOV CR0,xxx.xxx0b, einen Prozessor-Reset oder einen Triple-Fault des i386.
Die Basisadresse der globalen und lokalen Deskriptortabelle ist im GDT- bzw. LDT-Register abgelegt. Diese Register werden von der Laderoutine des Betriebssystems mit den entsprechenden Werten geladen. Im Gegensatz zur LDT darf beim Aufbau der GDT der nullte Eintrag nicht benutzt werden. Ein Bezug auf den nullten Eintrag führt sofort zu der Exception "allgemeiner Protection-Fehler". Dadurch wird verhindert, daß ein noch nicht initialisiertes GDTR benutzt wird.
Wesentlich an der GDT ist, daß die gesamte Segment- und damit Speicherverwaltung aus ihr aufgebaut wird. Im GDTR 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 i386 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. 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 die Betriebssystemlader den i386 in den Protected Mode umschalten. 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 Selektor gibt den Eintrag in der globalen Deskriptortabelle an, der den Deskriptor für die lokale Deskriptortabelle enthält. Die konsistente Verwaltung der Deskriptortabellen ist alleinige Aufgabe des Betriebssystems, der Anwendungsprogrammierer hat keinerlei Einflußmöglichkeit auf diesen Vorgang. Die Befehle zum Laden der Deskriptortabellenregister LDTR und GDTR müssen im Protected Mode von einem Task mit der Privilegierungsstufe 0 ausgeführt werden, üblicherweise vom Kernel.
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 benutzte Segmente (Programmcode und -daten) 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 á 8192 Einträge oder 16384 Segmente zur Verfügung, wobei der nullte Eintrag in der GDT nicht benutzt wird. Jedes Segment kann bei gelöschtem G-Bit bis zu 1mByte, bei gesetztem G-Bit bis zu 4gByte umfassen. Damit erhält man einen maximalen logischen oder virtuellen Adreßraum von 16gByte bzw. 64tByte pro Task. Dieser Wert ist natürlich wesentlich größer als der physikalische Adreßraum des i386 mit 4gByte.
Schon bei Page-Granularität mit einem virtuellen Adreßraum von vergleichsweise bescheidenen 16gByte können nicht mehr alle Segmente im Speicher vorhanden sein. Wird ein Segment beispielsweise vom Betriebssystem auf 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 die 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 unter Umständen ein anderes Segment auslagert. Der i386 unterstützt dieses Swapping, indem es die Exception auslöst. Das eigentliche Laden oder Auslagern der Segmente muß aber das Betriebssystem erledigen.
Soll der i386 in den Protected Mode umgeschaltet werden, muß 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 ROM-BIOS über den Befehl MOV cr0, <wert> das Bit PE im Steuerregister CR0 auf 1. Der i386 arbeitet nun im Protected Mode. Beim PC kann der i386 auch über die Funktion 89h des Interrupts INT 15h in den Protected Mode umgeschaltet werden. Aus Kompatibilitätsgründen mit dem 80286 kann der i386 zudem über den 80286-Befehl LMSW in den Protected Mode versetzt werden.
Der i386 kann den Protected Mode auf einfache Weise wieder verlassen, indem ein Task mit PL=0 das PE-Bit durch den Befehl MOV cr0, <wert> wieder löscht. Der i386 schaltet dann sofort in den Real Mode zurück. Beim 80286 ist das leider nicht möglich. Einmal im Protected Mode, konnte beim 80286 das PE-Bit nicht wieder gelöscht werden. Der Grund dafür war wohl, daß INTEL sich nicht vorstellen konnte, daß jemand (nämlich MS-DOS) den neuen Protected Mode einfach ignorieren könnte. Der Real Mode sollte beim 80286 nur dazu dienen, alle notwendigen Vorbereitungen zum Sprung in den Protected Mode zu treffen. Eine Rückkehr in den Real Mode schien absurd (ist sie auch, wenn man z.B. den physikalischen Real-Mode-Adreßraum von 1mByte mit dem des Protected Mode mit 4gByte vergleicht).Erst die Maxime der Kompatibilität und die enorme Marktbedeutung von MS-DOS zwangen INTEL, eine einfache Rückkehr in den Real Mode zu ermöglichen.
Im Real Mode war die Ermittlung einer linearen Speicheradresse ganz einfach: Der Wert des entsprechenden Segments wurde mit 16 multipliziert zum Offset addiert. Im Protected Mode ist das ganze erheblich umfangreicher. Es laufen folgende Schritte ab:
Anhand des Segmentselektors im entsprechenden Segmentregister wird ermittelt, ob die globale oder die lokale Deskriptortabelle benutzt werden soll;
Mit Hilfe des Speicherverwaltungsregisters GDTR oder LDTR wird die Basisadresse der globalen bzw. lokalen Deskriptortabelle ermittelt;
Der Index des Segmentselektors wird mit 8 multipliziert, das Ergebnis zur Basisadresse der Deskriptortabelle addiert und es wird ermittelt, ob der so erhaltene Wert das Limit nicht übersteigt; tut er das, so löst der Prozessor die Exception 0dh "allgemeiner Protection-Fehler" aus.
Anhand des Segmentdeskriptors in der Deskriptortabelle werden Basisadresse und Limit des Segments ermittelt;
Im Adreßaddierer in der Adressierungseinheit werden diese Basisadresse und der Offset addiert und es wird ermittelt, ob der erhaltene Wert das Limit des betreffenden Segments nicht übersteigt; ist dies der Fall, so löst der Prozessor die Exception 0dh "allgemeiner Protection-Fehler" aus.
Die so ermittelte Adresse wird als lineare (ohne Paging als physikalische) Adresse ausgegeben.
Das Bit S sowie die vier Typbits des Segmentdeskriptor geben Art und Möglichkeit für einen Zugriff auf das beschriebene Segment an. Ist S (Descriptor-Type) gleich 1, so handelt es sich um ein Applikationssegment, d.h. das Segment enthält Programmcode oder Programmdaten. Zu diesen Programmen gehören auch Systemroutinen, die zur Verwaltung des Computers bis auf Kernel-Ebene des Betriebssystems gehören. Demgegenüber beschreiben Systemsegmente und Gates (S=0) vordefinierte Datenstrukturen, die zur Steuerung und Überwachung von mehreren parallel ablaufenden Tasks im Computer oder zum Aufruf von Prozeduren und zum Sprung zu Segmenten mit unterschiedlicher Privilegierungsstufe dienen.
Ist S=1, handelt es sich also um ein Applikationssegment, so hat das Feld Typ im Segmentdeskriptor folgenden Aufbau:
In der umseitigen Tabelle finden sich alle möglichen Kombinationen der Bits EXE, E/C, W/R und A sowie deren Bedeutung.
Legt man den Begriff Daten möglichst weit aus, so besagt er soviel wie Information. In diesem Sinne sind auch Programme Daten, da sie (logischerweise) eine Menge von Information (eine Folge von Bits) darstellen (nämlich was der Computer machen soll). Daß uns diese Art der Information nicht immer unmittelbar einsichtig erscheint, ist dabei ohne Relevanz.
Im engeren Sinne trifft man aber doch eine Unterscheidung zwischen Daten und Programmen: Daten sind Information, die be- oder verarbeitet werden soll, Programme stellen die Werkzeuge dar, mit denen die Daten be- und verarbeitet werden. Wesentlich unterscheiden sich diese beiden Kategorien vor allem darin, daß die Datenmenge "Programm" während ihrer Ausführung im allgemeinen nicht verändert wird, Daten im engeren Sinne aber gerade bearbeitet und dadurch verändert werden. Diese Differenzierung des allgemeinen Begriffes "Daten" spiegelt sich auch bei den Segmentdeskriptoren wider: Es gibt ausführbare, d.h. Programmsegmente, sowie nicht ausführbare, d.h. Datensegmente. Markiert wird diese Unterscheidung durch das Bit EXE (Execute): ist EXE gleich 1, so handelt es sich um ein Programmsegment, ansonsten um ein Datensegment.
Datensegmente sind implizit stets als lesbar gekennzeichnet. Sie können jedoch erst dann beschrieben werden, wenn das Bit W (Write) gesetzt ist. Ein Schreibversuch auf ein Segment mit gelöschtem Write-Bit (W=0) löst die Exception "allgemeiner Protection-Fehler" und damit den Interrupt 0dh aus. Datensegmente können sich darin unterscheiden, in welche Richtung sie jeweils "wachsen". Die Richtung wird dabei durch das Bit E (Expand-down) definiert. Es verändert die Bedeutung des Limits: Bei einem sich nach oben erstreckenden Segment (E=0) muß der Offset eines Objekts stets kleiner oder darf höchstens gleich dem Limit sein. Demgegenüber muß bei einem sich nach unten erstreckenden Segment (E=1) der Offset immer einen größeren Wert als das Limit aufweisen. Bei gesetztem E-Bit wächst das Segment also nach unten, ansonsten nach oben. Ein Beispiel für ein nach unten wachsendes Segment wäre das Stack-Segment, da der Stack bei einer Vergrößerung durch den dekrementierten Stack-Zeiger ESP nach unten wächst.
Greift der Prozessor auf Daten oder Code in einem Segment zu, so setzt er automatisch das Bit A (Accessed) im entsprechenden Deskriptor. Das Betriebssystem ist dadurch in der Lage festzustellen, welche Segmente häufig angesprochen werden. Das ist wichtig, wenn ein Segment auf Festplatte ausgelagert werden soll, um Platz für ein neues zu schaffen.
Programmsegmente sind im Gegensatz zu Datensegmenten implizit stets als nicht-beschreibbar gekennzeichnet, der Prozessor kann daher niemals einen Schreibzugriff auf ein solches Codesegment ausführen. Diese Vorkehrung dient zum Schutz gegen Programmfehler und ist Bestandteil der Schutzphilosophie des Protected Mode. Unter MS-DOS führen Bugs häufig zum Überschreiben von Programmcode - der PC stürzt ab. Das wird durch den impliziten Schreibschutz von Codesegmenten verhindert. Manchmal ist es aber dennoch notwendig, Code oder Immediate-Operanden (die ja Bestandteil des Befehlsstromes und dadurch des Codesegments sind) während der Laufzeit zu überschreiben. Zu diesem Zweck sieht die Implementierung des Protected Mode vor, ein Code- und ein Datensegment zu überlagern, beispielsweise indem ein Segment als Codesegment gekennzeichnet wird, während man ein zweites Segment mit gleicher Basisadresse und gleichem Limit als Datensegment definiert. In diesem Fall kann der i386 über den Umweg eines sogenannten Alias mit Hilfe des Datensegments Code überschreiben. Das ist natürlich nicht ganz ungefährlich, weil nun im Prinzip die gleichen, oben angeführten, Fehler auftreten können.
Ist das Bit R (Read) im Segmentdeskriptor eines Codesegments gesetzt (R=1), kann das Segment nicht nur ausgeführt, sondern auch über einen MOV-Befehl direkt in ein Vielzweckregister übertragen werden. Das ist beispielsweise notwendig, wenn im Programmcode Daten eingebettet sind (also Immediate-Operanden oder Tabellen). Im ROM-BIOS finden sich häufig Tabellen für Festplattentypen, Basisadressen des Bildschirmspeichers etc. Wenn die Tabellen nur gelesen werden sollen, kann man sich über das R-Bit ein aufwendiges und fehleranfälliges Alias sparen.
Um auch ein möglichst effektives Auslagern von Codesegmenten auf einen Massenspeicher zu ermöglichen, weist der Segmentdeskriptor für ein Codesegment ebenfalls ein Accessed-Bit auf. Statt des Expand-Down-Bits (Bit E) der Datensegment-Deskriptoren besitzt ein Codesegment-Deskriptor das Bit C (Conforming). Programmsegmente, die als Conforming gekennzeichnet sind, können auch von weniger privilegierten Codesegmenten direkt angesprochen werden, ohne daß der Umweg über ein Gate notwendig ist. Der Code des Segments wird dann mit der Privilegierungsstufe des aufrufenden Segments ausgeführt. Typischerweise befinden sich Systemfunktionen, die keine geschützten Teile des Systems benützen, in Conforming-Segmenten. Damit können Anwendungsprogramme auf weniger kritische Funktionen zugreifen, ohne den langwierigen Umweg über ein Call-Gate machen zu müssen.
Der i386 kennt insgesamt drei grundlegende Systemsegmenttypen, nämlich Task-State-Segment (TSS), lokale Deskriptortabelle (LDT) und Gate. Sie dienen zur Speicherverwaltung (LDT) sowie zum Aufruf von Tasks und Prozeduren. Ein Segmentdeskriptor beschreibt ein Systemsegment, wenn das S-Bit gelöscht ist (S=0). Der i386 interpretiert dann den Wert in dem Feld Typ des Deskriptors entsprechend der folgenden Tabelle. Beispielsweise wird eine lokale Deskriptortabelle durch einen Systemsegment-Deskriptor beschrieben, der im Typfeld den Eintrag 2 (0010b) aufweist. Neben den Systemsegmenten existieren auch Gates.
Die Tabelle zeigt deutlich, daß sich die meisten Gates und die Task-State-Segmente für den 80286 und den i386 unterscheiden. Ursache ist die 32-Bit-Architektur des i386, die im Gegensatz zum 80286 z.B. 32-Bit-Offsets und 32-Bit-Stackwerte erlaubt. Aus Kompatibilitätsgründen kann der i386 die 80286-Systemsegmente problemlos verarbeiten. Umgekehrt gilt das natürlich nicht. Ein TSS definiert den Zustand eines aktiven (busy) oder unterbrochenen (verfügbaren) Tasks, es ist wesentlich für die Hardwareunterstützung von Multitasking-Betriebssystemen durch den i386. Zu beachten ist, daß TSS- und LDT-Deskriptoren nur in der GDT als Einträge zulässig sind, in der LDT können sie nicht auftreten. [Im Rahmen dieser Arbeit werde ich nur die i386-Versionen der verschiedenen Systemsegmenttypen erläutern, da nur diese aufgrund des Veraltens des 80286 noch von Bedeutung sind.]
Im Protected Mode verwendet der i386 sogenannte Gates für einen kontrollierten Zugriff von Tasks auf Daten und Code in Segmenten höherer Privilegierungsstufe. Die Gates implementieren einen erheblichen Schutz gegen unberechtigte oder fehlerhafte Zugriffe auf fremde Daten und Programme. Nimmt ein Anwendungsprogramm Betriebssystemfunktionen in Anspruch, um z.B. Dateien zu öffnen oder zu schließen, dann garantieren die Gates, daß der Zugriff fehlerfrei erfolgt, oder eine Exception ausgelöst wird. Das Anwendungsprogramm kann sich nicht an den Schutzmechanismen vorbeimogeln, ohne daß das Betriebssystem sofort davon informiert wird. Im Real Mode ist DOS dagegen völlig blind. Ein fehlerhafter Zugriff muß nicht sofort zu einer offensichtlichen Störung des Computers führen, häufig fällt er erst auf, wenn das System bereits abgestürzt ist. Das macht das Debuggen von Real-Mode-Programmen manchmal recht kompliziert. Versucht ein Anwendungsprogramm, die Funktion mit einer falschen Einsprungadresse aufzurufen, verhält sich der Computer unvorhersehbar. Die Gates definieren im Protected Mode daher quasi "Tore", durch die das Anwendungsprogramm Zugriff auf fremde Routinen hat.
Bei einem Near-Call wird die Steuerung an eine Prozedur oder einen Sprungpunkt übergeben, der bzw. die 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 i386 prüft lediglich, ob EIP das Limit des Segments übersteigt. Ist der Offset gültig, so wird der Aufruf bzw. Sprung ausgeführt, ansonsten löst der i386 die Exception "allgemeiner Protection-Fehler" aus.
Tasks bestehen aber nur 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-Jump oder einem Interrupt statt. In allen drei Fällen wird ein neuer Selektor für das Codesegment 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 des Codesegments eine umfangreiche Prüfprozedur.
Für einen Far-Call oder Far-Jump stehen drei Möglichkeiten zur Verfügung:
Besitzt das Zielsegment dieselbe Privilegierungsstufe (PL) wie das Ausgangssegment, so kann der Far-Call durch das Laden des Zielsegmentselektors in das Codesegment CS direkt ausgeführt werden. Der i386 prüft in diesem Fall lediglich, ob der neue Wert des Befehlszählers EIP das Limit des Zielsegments nicht übersteigt und ob der Typ des Zielsegments (EXE=0 oder 1) mit dem Aufruf konsistent ist.
Ist das Zielsegment als Conforming gekennzeichnet, und ist seine Privilegierungsstufe größer (der Wert von PL kleiner) als die des Ausgangssegments, so wird der Far-Aufruf in gleicher Weise wie oben ausgeführt. Der Code des Conforming-Segments wird dann allerdings mit einer Privilegierungsstufe CPL ausgeführt, die der weniger privilegierten Ebene des aufrufenden Programmes und nicht der höher privilegierten Stufe des Conforming-Segments entspricht. Das verhindert, daß sich weniger privilegierte Programme über die Hintertür eines Conforming-Segments eine höhere Privilegstufe beschaffen und dadurch Zugriff auf geschützte Systembereiche erlangen.
Besitzt das Zielsegment eine andere Privilegstufe als das Ausgangssegment, und ist es nicht als Conforming gekennzeichnet, so bleibt nur der Weg über ein Call-Gate.
In den ersten beiden Fällen lädt der i386 einfach den Zielselektor 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üfung) 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 sogenanntes 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 Anderung der Privilegierungsstufe (z.B. wenn ein Anwenderprogramm (PL=3) durch einen Interrupt-Handler im Kernel (PL=0) unterbrochen wird). Der Interrupt muß daher ein Interrupt- oder Trap-Gate benutzen, um den Interrupt-Handler zu aktivieren. Die Bedeutung der Task-Gates wird weiter unten 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 S=0 im Segmentdeskriptor und einen Wert des Typfeldes von 4 bis 7 und 12 bis 15 definiert. Sie sind also Teil der Systemsegmente.
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 und globalen Deskriptortabelle, nicht aber in der Interrupt-Deskriptortabelle auftreten. Dort sind nur Interrupt-, Trap- und Task-Gates erlaubt.
Wie die Abbildung bereits zeigt, unterscheidet sich der Aufbau eines Gate-Deskriptors ganz erheblich von einem "normalen" Segmentdeskriptor: Es fehlt z.B. die Basisadresse des Segments. Statt dessen ist das 5-Bit-Feld Param-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 Segmentdeskriptor-referenzen ausgeführt: die erste, um den Gate-Deskriptor zu ermitteln, und die zweite, um die Basisadresse des betreffenden Segments zu ermitteln. Das Gate enthält ja wiederum nur einen Zielsegment-selektor, nicht dessen lineare Adresse. Die Adressierungseinheit des i386 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 i386 erkennt am Eintrag im Typfeld, ob der Zielsegmentselektor für das CS-Register bei einem Far-Call oder Far-Sprung direkt ein Codesegment oder einen Gate-Deskriptor darstellt. Im ersten Fall prüft der Prozessor, ob der direkte Aufruf erlaubt ist (ob z.B. das Zielsegment als Conforming gekennzeichnet ist), und führt ihn unabhängig davon auf 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 Einsprungpunkt vorgegeben, so daß das aufrufende Programm nicht versehentlich einen falschen Einsprungpunkt vorgeben kann. Das ist besonders wichtig, wenn Funktionen des Betriebssystems aufgerufen werden: Ein falscher Einsprungpunkt in diesen Routinen führt gewöhnlich zu einem totalen Systemabsturz, die Angabe des falschen Gates dagegen höchstens zum Abbruch des Tasks und der Ausgabe einer Fehlermeldung.
Wie bereits erwähnt, besitzt jeder Task für jede der vier verschiedenen Privilegierungsstufen jeweils einen eigenen Stack. 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 Param-Count die Anzahl der zu kopierenden Doppelworte (á vier Byte) ein. Der i386 überträgt dann diese Doppelworte 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, d.h. 124 Byte übergeben. Ein Programmierer, der mehr Byte für die Parameterübergabe benötigt, ist selbst schuld, doch bietet die Übergabe eines Far-Zeigers auf eine Datenstruktur mit den gewünschten Parametern einen einfachen und schnellen Ausweg.
Selbstverständlich führt der i386 auch bei einem Aufruf über Gates eine Prüfung der Zugriffsberechtigung aus. In diese Prüfung gehen die folgenden Privilegierungsstufen ein:
CPL;
RPL des Segmentselektors für das Call-Gate;
DPL des Gate-Deskriptors;
DPL des Segmentdeskriptors für das Zielsegment des Aufrufs oder Sprungs.
Der DPL-Eintrag des Gate-Deskriptors legt fest, von welchen Privilegierungsstufen aus das Gate benutzt werden kann.
Gates werden z.B. benutzt, um die Steuerung an privilegierte Ebenen (z.B. das Betriebssystem) oder Code gleicher Stufe zu übergeben. Für letzteren Fall ist zwar kein Gate nötig, aber dieses Vorgehen ist auch möglich (und sicherer). Wichtig ist, daß nur CALL-Befehle Gates dazu verwenden können, Routinen niedriger Privilegierungsstufe (mit größerer 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:
Die effektive Privilegierungsstufe EPL (gleich dem Maximum von CPL und RPL) muß kleiner oder gleich dem DPL des Gate-Deskriptors sein;
Der DPL des Zielsegment-Deskriptors muß gleich dem CPL des aufrufenden Programmes sein.
Für den CALL-Aufruf oder einen Sprung-Befehl zu einem Conforming-Segment müssen dagegen die folgenden zwei Bedingungen erfüllt sein:
Die effektive Privilegierungsstufe EPL (gleich dem Maximum von CPL und RPL) muß kleiner oder gleich dem DPL des Gate-Deskriptors sein;
Der DPL des Zielsegment-Deskriptors muß kleiner oder gleich dem CPL des aufrufenden Programmes sein.
Bei einem Aufruf einer Prozedur höherer Privilegierungsstufe über ein Call-Gate führt der i386 noch folgende Vorgänge aus:
Der CPL-Wert wird so geändert, daß er die neue Privilegierungsstufe widerspiegelt;
Der i386 übergibt die Steuerung an die aufgerufene Prozedur oder den angesprungenen Code;
Statt des bisherigen Stack wird nun der Stack der neuen Privilegierungsstufe benutzt.
Die Stacks aller Privilegierungsstufen werden dabei durch das Task-State-Segment des jeweiligen Tasks definiert.
Neben den Registern für die globale und lokale Deskriptortabelle sowie das Task-Register besitzt der 80286 noch ein neues Register für die Interrupt-Deskriptortabelle (IDT). Im Real Mode waren die 1024 (1k) niederwertigen Byte des Adreßraums für die 256 Einträge (entsprechend den 256 Interrupts des i386) der Interrupt-Vektortabelle reserviert. Jeder Eintrag enthält im Format Segment:Offset die Einsprungadresse 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 Bytes lang. Durch den Eintrag Limit im IDTR kann die Größe der Interrupt-Deskriptortabelle jedoch den tatsächlichen Erfordernissen angepaßt werden. Benötigt ein System beispielsweise nur die Interrupts 0 bis 63, so genügt eine IDT mit 64 Einträgen zu acht Bytes, d.h. insgesamt 512 Bytes. Wird ein Interrupt ausgelöst, für den kein Eintrag in der IDT mehr existiert (im angeführten Fall z.B. ein INT 68), so tritt der i386 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.
Bevor der i386 in den Protected Mode umgeschalten wird, muß 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 neue Fehler-Exception auslöst, die nicht behandelt werden kann. Beim Einschalten oder einem Prozessor-Reset lädt der i386 von sich aus das IDTR mit dem 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, 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 bei einem Call-Gate auf den Segmentdeskriptor in der LDT oder GDT, der die Basisadresse des betreffenden Segments enthält. Der Unterschied zwischen Interrupt- und Trap-Gate besteht darin, daß ein Interrupt-Aufruf über ein Interrupt-Gate die Flags IE (Interrupt Enable) und T (Trap) löscht, das Trap-Gate hingegen nicht.
Die Besonderheiten, die gelten, wenn der Prozessor bei einem Interrupt oder einem CALL- bzw. JMP-Befehl auf ein Task-Gate trifft, werden im folgenden Abschnitt erläutert.
Die gesamten Protection-Funktionen des i386 dienen in erster Linie einem Ziel: Multitasking. Bei einem leistungsfähigen 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 nur für kurze Zeit hintereinander ausgeführt werden, um dann unterbrochen und nach kurzer Zeit an der gleichen Stelle wieder gestartet zu werden. Um das zu erreichen, muß der Zustand eines Tasks zum Zeitpunkt der Unterbrechung vollständig gesichert werden, weil der Task ja sonst nicht an derselben Stelle unter den Bedingungen, die zum Zeitpunkt der Unterbrechung herrschten, 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 den Stack gesichert, der Interrupt bedient und alle Register vom Stack wieder mit den alten Werten geladen. Wichtig ist, daß das Registerpaar CS:EIP gesichert wird, da es die Stelle im Programm angibt, an der es unterbrochen worden ist.
Doch im Protected Mode ist es aufgrund der umfangreichen Schutzfunktionen des i386 nicht damit getan, einfach 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 es den Zustand eines Tasks vollständig. Es stellt ein ganzes Segment dar, das ausschließlich zum Speichern des Task-Zustandes dient.
Im TSS sind neben den gewöhnlichen Offset- und Segment-Registern beispielsweise die Zeiger ESP und die Segmente SS für die Stacks der verschiedenen Pivilegierungsstufen, 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 hier das CR3-Register abgelegt, das die Basisadresse des Page-Directory für den beschriebenen Task angibt. [Ich erwähne das nur der Vollständigkeit halber, in dieser Arbeit wird nicht näher auf die Paging-Mechanismen eingegangen.]
Der Eintrag I/O-Map-Basis gibt die Adresse einer I/O-Map an, die neben dem IOPL-Flag zum Schutz des I/O-Adreßbereichs im Protected Mode dient. 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 EFlags-Register gesetzt ist. Wenn das T-Bit (Trap) gesetzt ist, erzeugt der i386 bei einem Task-Switch (d.h. beim Laden des TSS) eine Debug-Exception 01h.
Weist der zugehörige TSS-Deskriptor in der LDT oder GDT im Typfeld den Wert 1 (80286-kompatibles TSS) oder 9 (i386-TSS) auf, so ist das durch den Deskriptor beschriebene TSS verfügbar. Dies bedeutet, daß der von diesem TSS beschriebene Task gestartet werden kann. Ist im Typfeld hingegen ein Eintrag 3 (80286-kompatibles TSS [busy]) oder 11 (i386-TSS [busy]) vorhanden, so ist das TSS als aktiv (busy) gekennzeichnet. Der von einem solchen TSS beschriebene Task ist aktiv und muß nicht eigens aktiviert werden. Im übrigen 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 z.B. einen anderen Task zu aktivieren, sichert der i386 alle aktuellen Werte des aktiven Task im zugehörigen TSS und lädt die Werte des zu startenden Task aus dessen TSS in die Segment-, Offset- und Steuerregister. Das geschieht völlig automatisch und ohne einen weiteren Eingriff von Software. Woher "weiß" der Prozessor aber, wann er einen Task und welchen neuen er aktivieren soll, d.h. was bildet den Trigger für einen Task-Switch. Der Schlüssel liegt in den Task-Gates.
Der TSS-Segmentselektor im Task-Gate verweist auf den Segmentdeskriptor, der das TSS des neu zu aktivierenden Tasks definiert. Trifft der i386 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 definiert ist und dem Typfeld des zugehörigen TSS-Deskriptors den Wert 1 (80286-kompatibles TSS) oder 9 (i386-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 Basisadresse, Limit und Zugriffsrechte des Task-Gate-Deskriptors. Um den Task-Switch zu vollenden, kennzeichnet der Prozessor nun den zugehörigen Deskriptor im Typfeld als busy, d.h. er schreibt den Wert 3 (80286-kompatibles TSS [busy]) oder 11 (i386-TSS [busy]) 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.
Erstmals aktivierte Tasks - also Tasks, die neu geladen werden - aktiviert der i386 in gleicher Weise. Nur zeigt das Registerpaar CS:EIP hier nicht auf den Befehl an der Unterbrechungsstelle, sondern den Startbefehl des Programmes.
Der aktive Task sei das Textverarbeitungsprogramm Word, das gerade damit beschäftigt ist, einen Seitenumbruch durchzuführen. Nun tritt ein Timer-Interrupt auf. Im Interrupt-Handler trifft der i386 auf ein Task-Gate, das auf dBase 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 dBase und startet diesen bereits früher unterbrochenen Task. Nach kurzer Zeit tritt erneut ein Timer-Interrupt auf, nur wird diesmal dBase angehalten und dafür 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. Ein Multitasking-Betriebssystem muß also sehr komplexe Operationen rasend schnell ausführen. Es wird jedoch vom i386 in sehr effektiver Weise unterstützt: Um einen Task-Switch auszuführen, muß das Betriebssystem "nur" ein Task-Gate, 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 selbständig und automatisch aus. Es sind keine Software-Anweisungen des Betriebssystems notwendig, d.h. der i386 sichert bei einem Task-Switch die 104 Bytes des alten TSS und lädt die 104 des neuen TSS völlig selbständig.
Zu betonen ist noch, daß es alleinige Aufgabe des Betriebssystems ist, den einzelnen Programmen einen entsprechend großen Anteil an Prozessorzeit zuzuweisen. Die Steuerung der Task-Switches ist alleinige Aufgabe des Betriebssystem, die Programme selbst haben bei einem richtigen Multitasking-Betriebssystem keine Einflußmöglichkeit darauf.
Nun ein mittlerer Hammer: Das ehemals am meisten verbreitete und auch heute noch oft benützte MS-DOS (genauso wie DR-DOS oder PC-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 dem unteren 1mByte des Speichers und dem Extended Memory zu verschieben. Task-Switches und die umfangreichen und sehr nützlichen Zugriffsüberprüfungen werden in keinster Weise ausgenützt. Es gibt ja schließlich auch Leute, die stellen sich eine MIG in den Vorgarten
Neben den bereits im Ansatz geschilderten Prüfungen und Besonderheiten beim Aufruf von Prozeduren oder dem Umschalten zwischen verschiedenen Tasks muß 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 den i386 voll ausnutzt. In den nächsten beiden Abschnitten werden noch die Schutzvorkehrungen für den zweiten Adreßraum des i386 erläutert, nämlich die Zugriffsprüfungen für den I/O-Adreßraum.
Über die I/O-Ports werden im allgemeinen Register von Hardware-Komponenten des PC wie beispielsweise der Festplattencontroller oder die Steuerregister des DMA-Chips angesprochen. Da die Steuerung und Überwachung eine originäre Aufgabe des Betriebssystems ist und hierzu meist Treiber mit PL=1 benutzt werden, fällt auch der I/O-Adreßbereich unter den Zugriffsschutz. Ports werden aber nicht mit Hilfe eines Segmentregisters angesprochen, also steht diese Art des Zugriffsschutzes nicht zur Verfügung.
Der Schutz des I/O-Adreßbereiches erfolgt beim i386 über zwei völlig unterschiedliche Strategien: Einmal das IOPL-Flag im Flag-Register und zusätzlich die I/O-Permission-Bit-Map im Task-State-Segment. Zunächst aber zum IOPL-Flag.
Der Wert dieses Flags gibt die Privilegierungsstufe an, die ein Code-Segment mindestens aufweisen muß, um auf den I/O-Adreßraum zugreifen zu können, d.h. es muß gelten CPL IOPL. Ist der CPL des aktuellen Task größer (niedrigere Privilegierungsstufe), so führen die I/O-Befehle IN, OUT, INS und OUTS zu der bereits hinlänglich bekannten Exception "allgemeiner Protection-Fehler". Vernünftige Anwendungsprogramme unter einem vernünftigen Betriebssystem führen solche Zugriffe ausschließlich über das Betriebssystem aus. Weniger vernünftige Anwendungsprogramme versuchen das direkt, um 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 (Clear Interrupt Flag) und STI (Set Interrupt Flag) vom IOPL-Flag abhängig. Diese sechs Befehle werden als IOPL-sensitive Befehle bezeichnet, da der Wert des IOPL-Flag Einfluß auf ihre Ausführung hat.
Sinn und Zweck dieser Einschränkung werden sofort einsichtig, betrachtet man den Fall, daß 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 Festplattencontroller "dazwischenfunkt". In welchen Zustand sich die unterbrochene Systemroutine nach einem erneuten Task-Switch 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 Anderung des IOPL-Flags steht kein expliziter Befehl zur Verfügung (wie z.B. CLI oder STI für das Interrupt-Flag). Die beiden genannten Befehle sind jedoch privilegiert, d.h. sie können nur von einem Codesegment 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 die Exception "allgemeiner Protection-Fehler" aus. Da die Flags jedoch Teil des TSS sind und sich somit von Task zu Task unterscheiden, kann durchaus ein Task Zugriff auf den I/O-Adreßraum besitzen, ein anderer dagegen nicht.
Diese Strategie der globalen Absicherung des I/O-Adreßbereichs über das IOPL-Flag ist bereits im 80286 implementiert. Der i386 kann die Ports zusätzlich individuell schützen. Diese Schutzstrategie für die Ports ist besonders im Hinblick auf den Virtual 8086 Mode implementiert worden. [Wie auch die Paging-Mechanismen, werde ich auf den Virtual 8086 Mode nicht näher eingehen, er sei nur der Vollständigkeit wegen erwähnt.]
Neben dem globalen Schutz durch das IOPL-Flag kennt der i386 einen weiteren Schutzmechanismus für Zugriffe auf den I/O-Adreßbereich, nämlich die sogenannte I/O-Permission-Bit-Map. Sie ist im TSS des jeweiligen Task 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. Die I/O-Permission-Bit-Map muß 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. Zu beachten ist, daß das höchstwertige Byte der Map, d.h. das Byte unmittelbar vor dem Ende des TSS den Wert 11111111b (=0ffh) besitzen muß. Für die I/O-Permission-Bit-Map kann nur ein i386-TSS verwendet werden, da das 80286-TSS 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 i386 alle Prüfungen im Zusammenhang mit der I/O-Permission-Bit-Map, der Zugriffsschutz für den I/O-Adreßbereich erfolgt allein durch das IOPL-Flag.
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-Adreßbereich gestatten, so untersucht der i386 anschließend zusätzlich noch die I/O-Permission-Bit-Map, um zu ermitteln, ob der gewünschte Port auch tatsächlich angesprochen werden darf. Das geschieht auf einer eins-zu-eins Zuordnung von I/O-Adressen und dem entsprechenden Bit in der Map. Dem Port mit der Adresse 0 ist das Bit mit dem Offset 0 innerhalb der Map zugeordnet, dem Port 1 das Bit 1 usw. Ist das einem Port entsprechende Bit in der Map gesetzt, also gleich 1, so löst der i386 beim Zugriff auf den zugehörigen Port die 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, daß die I/O-Permission-Bit-Map alle I/O-Adressen abdeckt. Allen von der Map nicht erfaßten I/O-Ports wird automatisch ein gesetztes Bit zugeordnet, d.h. ein Zugriff auf die außerhalb der Map liegenden Ports führt automatisch zu einer Exception. In einem ISA-PC reicht es z.B. aus, die 3ffh niederwertigsten Ports durch eine Map abzudecken. Ein Zugriff auf Ports mit höheren Adressen löst eine Exception aus. Das ermöglicht neben der Schutzwirkung von Programmen und dem System eine wesentlich genauere Lokalisierung von Bugs.
Zu beachten ist, daß 16-Bit-Ports zwei und 32-Bit-Ports vier aufeinanderfolgende Bits zugeordnet sind. Nur wenn beide bzw. alle vier zugeordneten Bits gleichzeitig gelöscht sind, kann der i386 die I/O-Operation fortsetzen. Ist auch nur eines der Bits gleich 1, so löst der Prozessor eine Exception aus.
Die Bit-Map lautet: [11001101 00110000 11010100]
[I/O-Permission-Bit-Map]
Fall: 8-Bit-Ports
Geschützt sind die Ports 2, 4, 6, 7, 12, 13, 16, 18, 19, 22, 23
Ungeschützt sind die Ports 0, 1, 3, 5, 8, 9, 10, 11, 14, 15, 17, 20, 21
Fall: 16-Bit-Ports
Geschützt sind die Ports 2, 4, 6, 12, 16, 18, 22
Ungeschützt sind die Ports 0, 8, 10, 14, 20
Fall: 32-Bit-Ports
Geschützt sind die Ports 0, 4, 12, 16, 20
Ungeschützt ist der Port 8
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 Mode liegt.
Im folgenden eine Auflistung der neuen Exceptions:
Zweifacher Fehler (Exception 8) Treten zwei Exceptions hintereinander auf und ist es dem i386 nicht möglich, beide Exceptions hintereinander auszuführen, dann löst er den Interrupt 8 aus. Der i386 kann die beiden Exceptions immer dann nicht sequentiell behandeln, wenn es sich um zwei der folgenden handelt: 0 (Division durch 0), 9 (Coprozessor-Segmentüberlauf), 10 (ungültiges Task-State-Segment), 11 (Segment nicht vorhanden), 12 (Stack-Exception) oder 13 (Allgemeiner Protection-Fehler). Z.B. führt also das Auftreten der Exceptions 11 und 13 zu einer Exception 8.
Coprozessor-Segmentüberlauf (Exception 9): Ist ein Teil des Coprozessoroperanden geschützt oder nicht vorhanden, dann löst der i386 einen Interrupt 9 aus.
Ungültiges Task-State-Segment (Exception 10): Jeder Task-Switch mit einem ungültigen TSS löst einen Interrupt 10 (0ah) aus. Die Ursache liegt in einer inneren Inkonsistenz des TSS (z.B. ist das durch CS bezeichnete Segment nicht ausführbar, oder ein Selektor übersteigt das zugehörige Tabellenlimit).
Segment nicht vorhanden (Exception 11): Wenn der i386 versucht, auf ein Segment zuzugreifen, das aus dem Speicher ausgelagert ist, d.h. bei dem das P-Flag gelöscht ist, dann ist ein Interrupt 11 (0bh) die Folge.
Stack-Exception (Exception 12): Versucht ein Befehl, das Stack-Segmentlimit zu überschreiten oder ist das durch SS bezeichnete Segment z.B. nach einem Task-Switch im Speicher nicht vorhanden, dann löst der i386 einen Interrupt 12 (0ch) aus.
Allgemeiner Protection-Fehler (Exception 13): Erfaßt der i386 eine Verletzung der Schutzregeln des Protected Mode, wobei sich die Ursache nicht einer der Exceptions 8-12 zuordnen läßt, dann ist ein Interrupt 13 (0dh) die Folge.
Coprozessorfehler (Exception 16): Werden die Coprozessorfunktionen nicht vom i386 durch eine Software-Bibliothek emuliert und erfaßt der i386 ein Fehlersignal vom Coprozessor, dann löst er einen Interrupt 16 (10h) aus. Dieses Fehlersignal zeigt eine Fehlerbedingung im Coprozessor (z.B. einen Über- oder Unterlauf) an.
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, daß eine fehlerhafte oder inadäquate Anweisung die CPU aufhängt oder blockiert (wie z.B. der HLT-Befehl) bzw. Daten- und Codesegmente in unsauberer Weise benutzt werden und dadurch die Systemintegrität zerstört wird. Zu diesem Zweck sind drei Gruppen von Schutzmechanismen vorgesehen.
Beschränkte Nutzung von Segmenten: Beispielsweise können Codesegmente prinzipiell nicht und Datensegmente nur bei gesetztem Schreibbit (W) beschrieben werden. Alle ansprechbaren Segmente sind durch die GDT oder LDT beschrieben, die anderen Segmente sind nicht erreichbar.
Beschränkter Zugriff auf Segmente: Durch die verschiedenen Privilegierungsstufen und die Verwendung von CPL, DPL, EPL und RPL ist der Zugriff von Programmen einer bestimmten Privilegierungsstufe (CPL, RPL, EPL) auf Daten und Code anderer Segmente (DPL) beschränkt. Ausnahmen sind nur durch zuverlässige Aufrufmechanismen (Call-Gates etc.) zulässig.
Privilegierte Befehle: Befehle, die den Zustand der CPU unmittelbar beeinflussen (wie LGDT oder LLDT), oder die Anderung der Deskriptortabellen sowie I/O-Operationen können nur von Programmen ausgeführt werden, deren CPL oder IOPL eine hohe Privilegierungsstufe angeben.
Verletzt im Protected Mode ein Vorgang einen dieser Schutzmechanismen, dann löst der i386 sofort eine Fehler-Exception aus.
Es ist also klar erkenntlich, daß der Protected Mode dem Real Mode bei weitem überlegen ist. Das ergibt sich alleine schon aus folgenden Vorteilen:
Der gesamte Speicher steht zur Verfügung
Die Programme können vor einander geschützt werden, wie auch das Betriebssystem vor den Programmen
Der Zugriff auf externe Ressourcen (I/O-Adreßraum) wird vom Betriebssystem alleine überwacht
Multitasking wird vom Prozessor aktiv unterstützt
Obwohl die gesamte Materie ziemlich komplex ist, glaube ich doch, daß es mir gelungen ist, sie verhältnismäßig einfach wiederzugeben. Mit der Kenntnis all dieser Mechanismen war es mir möglich, das beiliegende Programm zu erstellen. Es soll nur einen Einblick in die Möglichkeiten der Programmierung zur Nützung dieser brachliegenden Ressourcen zeigen. Ein genaueres Eingehen auf diese Mechanismen würde ein um vieles komplexeres (sowie längeres) Programm erfordern. Vor allem ist es nicht möglich, diese Aufgabe ohne reichlichen Einsatz von Assembler zu erfüllen, was nicht mein Ziel war.
Durch die ohnehin schon ordentliche Größe der Arbeit habe ich mich entschlossen, den Virtual-8086-Mode nur zu erwähnen, da seine eingehende Behandlung wiederum einige Dutzend Seiten benötigt, was den Rahmen dieser Arbeit sprengen würde. Ebenso habe ich vielfach den SystemManagementMode verschwiegen, der jedoch ohnehin kaum verwendet wird und nur sehr geringe und eingeschränkte Bedeutung besitzt.
Das Programm ist etwas kürzer geworden, als eigentlich vorgesehen, doch war es, wie oben erwähnt, niemals der Sinn der Arbeit, ein Betriebssystem oder eine Ansammlung von Assemblerroutinen zu verfassen (man möge mir die Anwendung einiger Assembler-Befehle im Programm verzeihen, doch für diese existieren keine C++-Funktionen, es gibt also keine andere Möglichkeit). Ich glaube, daß das Programm die wesentlichen Punkte ganz gut illustriert.
Als Referenzmaterial standen mir die Intel-Dokumente über den Pentium PRO zur Verfügung, denen ich sämtliche Daten entnahm. Näher an der Quelle zu sitzen ist wohl kaum möglich. Die Bilder entstammen ebenfalls allesamt diesen Dokumenten. Nähere Hinweise dazu im Quellenverzeichnis.
Ich möchte abschließend noch meinen Eltern für die Unterstützung daheim beim Arbeiten und Herrn Professor Mühlegger für seine Hilfestellung danken.
Assembler: Ein Programm, das Mnemonics in Maschinensprache umwandelt; eine Programmiersprache, deren Elemente eben die Mnemonics sind.
Bug: Ein Programmfehler, der oft zum kompletten Absturz führt
Call: Der Aufruf einer Prozedur.
Compiler: Ein Programm, das in höheren Programmiersprachen geschriebenen Source-Code in Assembler oder Maschinensprache umschreibt.
Debug(ging): Das Überprüfen eines Programmes auf Fehler und die anschließende Lokalisation der Fehlerursachen sowie deren Beseitigung.
Deskriptor: Ein 128-Bit-Segmentdeskriptor beschreibt das Segment genauer (Lokation, Zugriffsbestimmungen etc.). Deskriptoren befinden sich in den Deskriptortabellen.
Far: Außerhalb des selben Segments.
Gate: Erlaubt die Ausführung von Prozeduren, die normalerweise nicht zugänglich sind. Ein Gate definiert einen genauen Einstiegspunkt in eine Routine, deshalb ist diese Art der Befehlsübergabe sicherer als ein einfacher Far-Jump.
Höherwertig: Die oberste Teil eines Speicherobjekts (z.B. 0x12345678 => das höherwertige Byte ist 12, Wort ist 1234)
Intel-Architecture: Sammelname für die Prozessoren der 80x86 Familie, nämlich den 8086, 8088, 80186, 80286, i386, i486, Pentium, Pentium Pro und Pentium II.
Interrupt-Handler: Das Programm, das aufgerufen wird, sobald ein Interrupt ausgelöst wird, und dessen Aufgabe es ist, die Ursache des Interrupts zu behandeln.
Interrupt-Vektor: Ein Zeiger auf den Interrupt-Handler des jeweiligen Interrupts.
Jump: Ein Sprung von einer Stelle des Programmes an eine andere.
Kernel: Der kritische Teil eines Betriebssystems, meist die Speicher- und Task-Verwaltung.
Linearer Speicher: Durch Paging wird der gesamte Adreßraum des Computers abgebildet, obwohl viel weniger RAM eingebaut ist. Linear meint, daß der gesamte Speicher angesprochen werden kann. Dabei müssen jedoch aufeinanderfolgende Bytes nicht physikalisch aufeinanderfolgen.
Logischer Speicher: Der durch Segmente beschriebene Speicher. Zumeist nur RAM, er unterscheidet sich ohne Paging nicht vom linearen Speicher.
Mnemonic: Der uns verständliche Name eines Maschinensprache-Befehls.
Multitasking: Das gleichzeitige Ablaufen mehrerer Tasks (z.B. Word, die CD-Wiedergabe und Excel).
Near: Innerhalb des selben Segments.
Niederwertig: Der unterste Teil eines Speicherobjekts (z.B. 0x12345678 => das niederwertige Byte ist 78, Wort ist 5678)
Offset: Der Offset gibt an, wie weit ein Speicherobjekt vom Beginn des aktiven Segments entfernt ist.
Overflow (Überlauf): Ein Ergebnis wird so groß, daß es nicht mehr in das dafür vorgesehene Register paßt.
Page: Eine Speicherseite (in der Intel-Architecture 4kByte groß).
Paging: Das zerteilen des Speichers in gleich große Teile (Pages), die leichter auszulagern sind.
Physikalischer Speicher: Der wirklich eingebaute Speicher, RAM sowie ROM. Physikalische Adressen entsprechen selten logischen oder linearen.
Pointer: Ein Zeiger, der eine bestimmte Speicherstelle angibt.
Segment: Der Speicher ist in Segmente zerlegt. Ein Segment beginnt an einer bestimmten Stelle und hat eine gewisse Länge. Im Real Mode sind alle Segmente gleich lang, im Protected Mode hängt dies vom Betriebssystem ab.
Segmentpräfix: Der Prozessor codiert die Art des verwendeten Segments als Präfix des Befehls.
Selektor: Ein 16-Bit-Segmentselektor gibt den Offset des Deskriptors in der jeweiligen Deskriptortabelle an. Nur durch Selektoren im Segmentregister kann auf die verschiedenen Segmente zugegriffen werden.
Stack: Der einem Programm als Zwischenspeicher zur Verfügung gestellte Speicher. Dort finden sich die lokalen Variablen sowie die Aufrufsparameter der verschiedenen Prozeduren. Über die Mnemonics PUSH und POP kann auf den Stack zugegriffen werden.
Stack-Frame: Ein Stack-Frame dient der aufgerufenen Prozedur dazu, die übergebenen Parameter auf dem Stack anzusprechen
Stack-Overflow (-Überlauf): Der dem Stack zur Verfügung stehende Speicher wurde vollständig verbraucht. Es ist kein Platz für weitere Variablen.
Swapping: Das Auslagern von Speicher(-segmenten) auf einen Massenspeicher (Festplatte etc.).
Task: Ein Programm, mit allen von ihm beanspruchten Ressourcen (z.B. Word mit allen geöffneten Fenstern).
Task-Switching: Das Umschalten von einem Task zum nächsten.
Underflow (Unterlauf): Ein Ergebnis wird so klein, daß es nicht mehr in das dafür vorgesehene Register paßt.
Virtual Memory: Virtueller Speicher ist die Menge an Speicher, die ein Computer zur Verfügung stellt, der physikalischen Speicher auslagert, d.h. die Daten aus dem Speicher auf Festplatte sichert. Dadurch kann der zur Verfügung gestellte Speicher nahezu unbegrenzt erweitert werden.
ADD: Addiert den ersten und den zweiten Operanden
Byte: 8 Bit
CALL: Ruft die angegebene Prozedur auf
CLD: Löscht das D-Flag
CLI: Löscht das IE-Flag
CMP: Vergleicht die beiden Operanden
Double-Word (Doppelwort): 32 Bit (4 Byte; 2 Words)
IN(S): Einlesen aus dem I/O-Adreßraum
JE: Jump if Equal
JNE: Jump if not Equal
LGDT: Lädt das GDTR mit dem angegebenen Operanden
LIDT: Lädt das IDTR mit dem angegebenen Operanden
LLDT: Lädt das LDTR mit dem angegebenen Operanden
LMSW: Lädt das MSW mit dem angegebenen Operanden
LOOP: Zur Programmierung von Schleifen
mem32: ein Speicheroperand mit 32 Bit Größe
mem64: ein Speicheroperand mit 64 Bit Größe
MOV: Kopiert den zweiten Operanden in den ersten
MUL: Multipliziert den Operanden mit AX
OUT(S): Ausgabe an den I/O-Adreßraum
POP: Nimmt den Operanden vom Stack
POPA: Nimmt alle Vielzweckregister vom Stack
POPF: Nimmt das Flag-Register vom Stack
PUSH: Bringt den Operanden auf den Stack
PUSHA: Bringt alle Vielzweckregister auf den Stack
PUSHF: Bringt das Flag-Register auf den Stack
Quad-Word (Vierfachwort): 64 Bit (8 Byte; 4 Words; 2 Double-Words)
reg16: Ein Registeroperand mit 16 Bit Größe
reg32: Ein Registeroperand mit 32 Bit Größe
RET: Kehrt aus einer Prozedur zurück
SGDT: Speichert das GDTR im angegebenen Operanden
SIDT: Speichert das IDTR im angegebenen Operanden
SLDT: Speichert das LDTR im angegebenen Operanden
SMSW: Speichert das MSW in den angegebenen Operanden
STD: Setzt das D-Flag
STI: Setzt das IE-Flag
Word (Wort): 16 Bit (2 Byte)
Intel Architecture Software Developer's Manual, Volume 1: Basic Architecture, 1997, Number 243190
Intel Architecture Software Developer's Manual, Volume 2: Instruction Set Reference, 1997, Number 243191
Pentium Pro Family Developer's Manual, Volume 3: Operating System Writer's Guide, December 1995, Number 242692
PC Intern 4 - Systemprogrammierung, 1994, ISBN 3-8158-1094-9
15.9.97 Besprechung des Themas "Die Bedeutung des Protected Mode und seine Funktionsweise, sowie die Programmierung einer Bibliothek zur Nutzung desselben in C++".
16.9.97 Konkretisierung des Themas: praktischer - theoretischer Teil
19.9.97 Zeitplan festgelegt; Gliederung sowie Zitierregeln besprochen;
23.9.97 Festsetzen der Gliederung
30.1.98 Abgabe erster Textteile (Kapitel 1)
06.2.98 Besprechung über Anderungen im Programm
24.2.98 Vorlage der fast fertigen Arbeit
25.2.98 Vorlage des Referenzmaterials
27.2.98 Abgabe der entgültigen Version
#define version 1.0 //version number
// ** ** ** ** Headers ** ** *********
#include <iostream.h>
#include <conio.h>
// ** ** ******** Global Constants ** ** ********
#define PIC_MASTER 0x20 //The Master ProgrammableInterruptController //Main I/O-Port
#define PIC_SLAVE 0xa0 //The Slave PIC Main I/O-Port
#define GDT_ENTRIES 2 //The number of entries in the GDT
#define true 1 //for use with the variable type bool
#define false 0 // - ' -
// ** ** ******** Global Type Definitions ** ** ********
typedef unsigned short int bool; //boolean type, false or true
typedef unsigned char byte; //byte type, 8 bit unsigned
typedef unsigned short int word; //word type, 16 bit unsigned
typedef unsigned long int dword; //double-word type, 32 bit unsigned
typedef unsigned char u8; //u8 type, 8 bit unsigned
typedef unsigned short int u16; //u16 type, 16 bit unsigned
typedef unsigned long int u32; //u32 type, 32 bit unsigned
typedef signed char s8; //s8 type, 8 bit signed
typedef signed short int s16; //s16 type, 16 bit signed
typedef signed long int s32; //s32 type, 32 bit signed
//* Enums for the following classes + Txts for use with describing them *******
enum ; //selector::TI()
char *ti[2]=;
enum ; //descriptor::System()
char *s[2]=;
enum ; //descriptor::Present()
char *p[2]=;
enum ; //descriptor::DefaultSize()
char *ds[2]=;
enum ; //descriptor::Granularity()
char *g[2]=;
char *f[2]=;
enum ;
//descriptor::Type() if system==0
char *cdtype[16]=;
enum; //descriptor::Type() if system==1
//************* Classes for Protected-Mode-Memory-Management ** ** ************
class pseudo_descriptor ; //constructor
void Limit(word l) ; //for accessing <limit>
word Limit(void) ;
void Base(dword b) ; //for accessing <base>
dword Base(void) ;
void ShowAll(void);
}; //for status information
class selector ; //constructor
byte RPL(void) ; //for accessing<RPL>
void RPL(byte x) ;
bool TI(void) ; //for accessing <TI>
void TI(bool x) ;
word Index(void) ; //for accessing <index>
void Index(word x) ;
void ShowAll(void) ;
}; //Status information, as usual
class descriptor ;
//constructor
//for accessing the various fields of the segment-descriptor
dword Limit(void) ;
void Limit(dword x) ;
dword Base(void) ;
void Base(dword x) ;
void Type(byte x) ;
byte Type(void) ;
void DPL(byte x) ;
byte DPL(void) ;
void System(bool x) ;
bool System(void) ;
void Present(bool x) ;
bool Present(void) ;
void DefaultSize(bool x) ;
bool DefaultSize(void) ;
void Granularity(bool x) ;
bool Granularity(void) ;
void Free(bool x) ;
bool Free(void) ;
void ShowAll(void) ;
};
//******* Procedures for I/O and for disabling/enabling interrupts ************
//enums to be used with set/get_interrupt_mask()
enum;
u8 inpb(u16 port) //byte input from <port>
};
u16 inpw(u16 port) //word input from <port>
};
void outpb(u16 port,u8 value) //byte output to <port>
};
void outpw(u16 port,u16 value) //word output to <port>
};
u8 low(u16 value) //returns lower byte of word <value>
;
u8 high(u16 value) //returns higher byte of word <value>
;
u16 get_interrupt_mask(void) //returns interrupt_mask_register from PIC
;
void set_interrupt_mask(u16 mask) //sets the interrupt_mask_register in the PIC
;
pseudo_descriptor sgdt(void) //returns limit and base of Global Descriptor Table
;
void lgdt(pseudo_descriptor x) //sets limit and base of Global Descriptor Table
;
void lidt(pseudo_descriptor x) //sets limit and base of Interrupt Descriptor Table
;
pseudo_descriptor sidt(void) //returns limit and base of Interrupt Descriptor Table
;
u32 physical(u32 x) //translates segment:offset (C++ far //pointer) to physical addresses
;
void main(void)
[i] Cf. [BasArch], Kapitel 3.1
[ii] Cf. [BasArch], Kapitel 2
[iii] Cf. [BasArch], Seite 2-1
[iv] Cf. [BasArch], Seite 2-1
[v] Cf. [BasArch], Seite 2-2
[vi] Cf. [BasArch], Seite 2-2
[vii] Cf. [BasArch], Seite 2-3
[viii] Cf. [BasArch], Kapitel 3.6
[ix] Cf. [BasArch], Kapitel 3.6.3
[x] Cf. [OSWG], Kapitel 2.5
[xi] Cf. [OSWG], Kapitel 3; [BasArch], Kapitel 3.3
[xii] Cf. [OSWG],Kapitel 3.4.2; [BasArch], Kapitel 3.7
[xiii] Cf. [BasArch], Kapitel 4.2, Kapitel 3.6
[xiv] Cf. [BasArch], Kapitel 5.3.3.2
[xv] Cf. [BasArch], Kapitel 5; [OSWG], Kapitel 5
[xvi] Cf. [OSWG], Kapitel 3, Kapitel 4
[xvii] Cf. [OSWG], Kapitel 3.4
[xviii] Cf. [OSWG], Kapitel 3.5.1
[xix] Cf. [OSWG], Kapitel 8
[xx] Cf. [OSWG], Kapitel 3.4.3
[xxi] Cf. [OSWG], Kapitel 4.8.4
[xxii] Cf. [OSWG], Kapitel 5.7
[xxiii] Cf. [OSWG], Kapitel 6
[xxiv] Cf. [BasArch], Kapitel 3.6.4
[xxv] Cf. [OSWG], Kapitel 6.2.1
Referate über:
|
Datenschutz |
Copyright ©
2024 - Alle Rechte vorbehalten AZreferate.com |
Verwenden sie diese referate ihre eigene arbeit zu schaffen. Kopieren oder herunterladen nicht einfach diese # Hauptseite # Kontact / Impressum |