In English

Übersetzung von Philipp Strebl (viele danke!)

# Was ist protected mode?

Die CPU des 8088 im ursprünglichen IBM PC war nicht besonders erweiterungsfähig. Vor allem gab es keinen einfachen Weg, auf mehr als ein MB physischen Speicher zuzugreifen. Um dieses Problem zu beseitigen und trotzdem Abwärtskompatibilität zu wahren, entwickelte Intel die 80286 CPU mit zwei Ausführungsmodi: dem real mode, in dem der 80286 sich wie ein schneller 8088 verhält, und den protected mode (heute als 16-bit protected mode bezeichnet). Protected mode erlaubt Programmen den Zugriff auf mehr als ein MB physischen Speicher und schützt gleichzeitig vor falscher Verwendung von Speicher (zum Beispiel der Ausführung eines data segment als code, oder dem Schreiben in ein code segment). Eine verbesserte Version, 32-bit protected mode, wurde mit der 80386 CPU präsentiert.

# Was unterscheidet protected mode von real mode?

Unterschiede zwischen protected mode und real mode:

| real mode | 16-bit protected mode | 32-bit protected mode
Segmentbasisaddresse | 20-bit (1 MB range) = 16 * Segmentregister | 24-bit (16 MB range), im descriptor | 32-bit (4 GB range), im descriptor
Segmentgröße (höchstens) | 16-bit, 64 KB (fix) | 16-bit, 1 B - 64 KB | 20-bit, 1 B - 1 MB oder 4 KB - 4 GB
Segmentschutz | nein | ja | ja
Segmentregister | segment base adr / 16 | selector | selector

# Ich dachte protected mode kommt ohne segmentierten Speicher aus...

Die Segmente sind immer noch da, aber im 32-bit protected mode ist es möglich, das segment limit, d.h. die Grenze für die Größe eines einzelnen Segments, die im real mode fix bei 64 KB liegt, auf 4 GB zu setzen. Das ist das Maximum an physischem Speicher, das von einer CPU mit 32-bit address bus überhaupt addressiert werden kann. Dadurch "verschwinden" die Segmente, weil es nur ein (sehr großes) Segment mehr gibt (oder geben kann). Trotzdem bleiben andere Schutzmechanismen wirkungsvoll. Einzig und allein diese Tatsache machte 32-bit protected mode beliebt.

# Was ist ein descriptor?

Im real mode gibt es wenig, was man über die Segmente wissen muss. Jedes ist 64 KB groß und man kann damit machen, was man will: Daten darin speichern, den Stack hineinplatzieren oder code ausführen, der im jeweiligen Segment gespeichert ist. Die base address des Segments ist einfach das 16-fache des Werts in einem der Segmentregister. Im protected mode brauchen wir neben der base address eines Segments auch seine maximale Größe und einige flags, die seinen Verwendungszweck angeben. Diese Information kommt in einen 8 bytes großen record mit dem klingenden Namen descriptor:

Der Aufbau eines code/data segment descriptor:

Niedrigstes byte | byte 1 | byte 2 | byte 3 | byte 4 | byte 5 | byte 6 | Höchstes byte
--- | --- | --- | --- | --- | --- | --- | ---
limit 7:0 | limit 15:8 | base 7:0 | base 15:8 | base 23:16 | access | flags, limit 19:16 | base 31:24

Das ist ein 32-bit (80386) descriptor. Bei 16-bit (80286) descriptors sind die beiden höchsten bytes (limit 19:16 und base 31:24) auf 0 gesetzt. Das access-byte gibt die Verwendung des Segments an (data segment, stack segment, code segment etc.).

Der Aufbau eines access byte:

Höchstes bit | present | present bit: Muss auf 1 gesetzt sein, um den Zugriff auf das Segment zu gestatten
bits 6, 5 | privilege | privilege bits: 0 ist das höchste Niveau an privilege (Ring 0), 3 ist das niedrigste (Ring 3)
bit 4 | 1 | für code und data/stack segments. Wenn nicht gesetzt, handelt es sich um ein system segment.
bit 3 | executable | executable bit: Wenn dieses bit als Wert 1 hat, ist dieses Segment code segment, sonst handelt es sich um ein stack/data segment
bit 2 | expansion direction/conforming | expansion direction (stack/data segment): Wenn dieses bit den Wert 1 hat, wächst das Segment (den Adressen nach) nach unten und offsets müssen größer als das limit sein. conforming (code segment): steht in Verbindung mit privilege
bit 1 | writable/readable | writable (stack/data segment): Wenn dieses bit den Wert 1 hat, kann in das Segment geschrieben werden readable (code segment): Wenn dieses bit den Wert 1 hat, kann von diesem Segment gelesen werden (in code segments kann nicht geschrieben werden)
Niedrigstes bit | accessed | accessed bit: Dieses bit wird gesetzt wann immer aus dem Segment gelesen oder in das Segment geschrieben wird.

Der Wert des 4-bit flag in byte 6 eines descriptor ist nur für 32-bit Segmente nicht 0:

Highest bit | Bit 6 | Bit 5 | Bit 4
Granularity | Default Size | 0 | 0

Das granularity bit (G) entscheidet, ob das segment limit in Einheiten zu 4 KB Seiten (G=1) oder ob es in Einheiten zu einem byte (G=0) angegeben ist.

Im Falle der Verwendung eines Segments als stack segment wird das default size bit (D) auch als B (Big) bit bezeichnet und legt fest, ob 16- oder 32-bit Werte auf den Stack gelegt werden (mit den Anweisungen push und pop). Wird das Segment als code segment verwendet, gibt das D bit an, ob Anweisungen prinzipiell in 16-bit (D=0) oder 32-bit (D=1) Einheiten verarbeitet werden. Um etwas näher darauf einzugehen: Ist das D bit gesetzt, dann ist das Segment USE32, benannt nach der Assemblerdirektive mit demselben Namen. Die folgende Sequenz von hexadezimalen bytes:

B8 90 90 90 90

wird von der CPU als eine 32-bit Anweisung betrachtet und würde in Assembler folgendermaßen lauten:

mov eax, 90909090h

Ist das D bit nicht gesetzt, ist das Segment USE16, also 16-bit code, und die gleiche Sequenz von bytes würde wie folgt interpretiert:

mov ax, 9090h
nop
nop

Zwei spezielle opcode bytes, nämlich das operand size prefix und das address length prefix kehren die Wirkung des D bit für das Ziel beziehungsweise für die Quelle der unmittelbar folgenden Anweisung um.

Bit 4 des access byte ist gesetzt, wenn ein code oder data/stack segment vorliegt. Wenn das bit den Wert 0 hat, handelt es sich um ein system segment. Davon gibt es wiederum verschiedene Varianten:

task state segment (TSS) | Wird zur Vereinfachung des multitasking verwendet. Die CPU vom 80386 aufwärts kennt vier Untertypen des TSS.
local descriptor table (LDT) | Hier können tasks anstatt im GDT ihre eigenen privaten descriptors speichern.
gate | Kontrolliert Übergänge der CPU von einem privilege-Niveau zu einem anderen. gate descriptors haben einen anderen Aufbau als die übrigen descriptors.

Aufbau eines gate descriptor:

Niedrigstes byte | byte 1 | byte 2 | byte 3 | byte 4 | byte 5 | byte 6 | Höchstes byte
--- | --- | --- | --- | --- | --- | --- | ---
offset 7:0 | offset 15:8 | selector 7:0 | selector 15:8 | word count 4:0 | access | offset 23:16 | offset 31:24

Beachten Sie das selector-Feld. gates arbeiten über Umwege und benötigen einen eigenen code oder ein eigenes TSS, um zu funktionieren.

Der Aufbau des access byte eines system segment descriptor:

Höchstes bit | bits 6, 5 | bit 4 | bits 3, 2, 1, 0
--- | --- | --- | ---
Present | Privilege | 0 | Type

Die unterschiedlichen system segment-Typen:

0 | ungültig | 8 | ungültig
1 | verfügbares '286 TSS | 9 | verfügbares '386 TSS
2 | LDT | 10 | undefiniert, reserviert
3 | belegtes '286 TSS | 11 | belegtes '386 TSS
4 | '286 call gate | 12 | '386 call gate
5 | task gate | 13 | undefiniert, reserviert
6 | '286 interrupt gate | 14 | '386 interrupt gate
7 | '286 trap gate | 15 | '386 trap gate

So. Wenn das Verwirrung stiften sollte: Es reicht vorerst vollauf, wenn Sie sich merken, dass die Haupttypen von system segments TSS (task state segment), LDT (local descriptor table) und gates sind.

# Wo befinden sich die descriptors?

Die descriptors sind in einer Tabelle, dem global descriptor table (GDT), dem interrupt descriptor table (IDT) oder in einem der local descriptor tables (LDT) aufgelistet. Die CPU verfügt über 3 Register, nämlich den GDTR, den IDTR (wenn interrupts benutzt werden) und den LDTR (wenn ein LDT benutzt wird) - das "R" am Ende jeder Bezeichnung steht für "Register". Diese Register müssen jeweils die Addresse des GDT, des IDT beziehungsweise des LDT beinhalten. Jede descriptor-Tabelle kann bis zu 8192 descriptors enthalten (eine Tabelle ist daher 64 KB gro0).

# Was ist ein Selektor?

Im protected mode enthalten die Segmentregister selectors, die auf einen descriptor tables verweisen. Nur die höchsten 13 bits des selector werden für diesen Verweis gebraucht, das nächstniedrigere bit wählt zwischen dem GDT und dem LDT aus. Die beiden niedrigsten bits legen ein privilege-Niveau fest (0-3).

# Wie aktiviere ich den protected mode?

Den protected mode zu aktivieren ist eigentlich recht einfach. Man muss:

- einen gültigen global descriptor table (GDT) erzeugen
- (fakultativ) einen gültigen interrupt descriptor table (IDT) erzeugen
- die interrupts deaktivieren
- die Addresse des GDT im GDTR ablegen
- (fakultativ) die Addresse des IDT im IDTR ablegen
- das PE bit im MSW-Register setzen
- einen far Sprung ausführen (sowohl CS als auch IP/EIP laden), um den protected zu aktivieren (CS mit dem code segment selector laden)
- DS und SS mit dem data/stack segment selector laden
- einen pmode-stack erzeugen
- (fakultativ) die interrupts aktivieren

# Wie komme ich zurück in den real mode?

Dazu muss man am '386:

- die interrupts deaktivieren
- einen far Sprung in ein 16-bit code segment (d.h. kurz in den 16-bit pmode umschalten) ausführen
- SS mit einem selector in ein 16-bit data/stack segment laden
- das PE bit auf 0 setzen
- einen far Sprung an eine real mode-Adresse ausführen
- die DS, ES, FS, GS und SS Register mit real mode-Werten laden
- (fakultativ) den IDTR auf den real mode-Wert setzen (base 0, limit 0FFFFh)
- die interrupts reaktivieren

Vor dem Zurückschalten in den real mode müssen CS und SS selectors enthalten, die auf "real mode-geeignete" descriptors verweisen. real mode-geeignet bedeutet, dass sie ein limit von 64 KB haben, byte-granular sind, d.h. das flags nibble=0, das sie nach oben wachsen, beschreibbar (gilt nur für data/stack segments) und present sind (access byte=1xx1001xb).

Am '286 kann man nicht einfach das PE bit auf 0 setzen, um den protected mode zu verlassen. Die einzige Möglichkeit ist, die CPU neu zu starten. Das kann geschehen indem man dem keyboard controller die Tastenkombination zum Warmstart übergibt oder indem man die CPU triple-faulted (siehe die Website von Robert Collins).

# Welche Fallen gibt es?

- Sie müssen sich hier ungeheuer auf die Details konzentrieren. Ein einziges falsches bit bewirkt, dass nichts funktioniert. Fehler im protected mode führen häufig dazu, dass die CPU triple-faulted wird und haben einen Neustart zur Folge. Seien Sie darauf vorbereitet, das immer und immer wieder zu sehen.
- Die meisten Bibliotheksfunktionen oder -prozeduren werden wahrscheinlich nicht ordnungsgemäß arbeiten. printf() zum Beispiel funktioniert nicht, weil diese Routine eine DOS- oder BIOS-Routine aufruft, um Text am Bildschirm auszugeben. Wenn Sie keinen DOS extender besitzen, sind all diese Routinen unter protected mode nicht verwendbar. Ich hatte Glück, als ich sprintf() verwendete, um formatierten Text in einen Puffer zu schreiben, den ich dann mit meiner eigenen protected mode-Routine am Bildschirm ausgab.
- Bevor Sie das PE bit auf 0 setzen, müssen die Segmentregister auf die descriptors verweisen, die für den real mode geeignet sind. Das bedeutet ein limit von exakt 0FFFFh (andere Bedingungen: siehe oben). In einem meiner Demoprogramme verwies ES auf ein text/video segment. Mit einem limit von 0FFFFh lief alles perfekt. Mit einem limit von 3999 (80 * 25 * 2 - 1) stürzte das System ab, nachdem es den protected mode verlassen hatte und versucht hatte, das ES Register zu benutzen.

  Tatsächlich muss das segment limit in DS, ES, FS und GS 0FFFFh oder größer sein. Wenn Sie dem Segment ein limit von 0FFFFFh verpassen und es als page-granular ausweisen, können sie auch im real mode auf bis zu 4 GB Speicher zugreifen. Das ist scherzhaft als unreal mode bezeichnet worden. Wie auch immer, limits, die nicht den Wert 0FFFFh haben (oder page-granularity) verursachen in CS oder SS im real mode größte Probleme.

- Sie können die LMSW-Anweisung des '286 nicht dazu verwenden, das PE bit auf 0 zu setzen. Schreiben Sie stattdessen MOV CR0, nnn.
- Laden Sie alle Segmentregister mit gültigen Selektoren, nachdem sie den protected mode aktiviert haben. Ich habe einmal darauf vergessen, das mit ES zu machen. Eine protected mode- Routine legte mit der Anweisung push ES am stack ab, lud ES mit einem gültigen Selektor und verwendete diesen. Als die Routine versuchte, den alten, ungültigen (real mode-) selector mit pop wieder vom Stapel nach ES zu speichern, stürzte das gute Programm ab.
- Das IDTR muss ebenfalls auf einen Wert zurückgesetzt werden, der für den real mode geeignet ist, bevor die interrupts reaktiviert werden (siehe oben).
- Nicht alle Anweisungen sind im real mode erlaubt. Wenn Sie versuchen, task state segments für multitasking zu verwenden, beachten Sie dass die Ausführung der LTR-Anweisung im real mode eine ungültige Anweisungsunterbrechung (???) hervorruft.
- descriptor tables im ROM? Abschnitt 10.4.3 der Datei 386intel.txt meint:

  Der GDT (und ebenso LDTs) sollten ihren Platz im RAM haben, weil der Prozessor das accessed bit [im access byte] der descriptors verändert.

  Trotzdem stellt eine meiner Quellen (danke, Vinay) fest, dass jüngere CPUs nicht versuchen, das accessed bit zu setzen, wenn sein Wert schon 1 ist. Dazu lesen Sie bitte die Dokumentation zu der CPU, für die Sie schreiben.
- Der untenstehende, primitive code bringt das System zum Absturz, wenn der PC im Virtual 8086 (V86) mode läuft. Das ist ein vierter Ausführungsmodus der '386 CPU, in dem die Adressierung ähnlich wie im real mode funktioniert aber einige Schutzmechanismen des protected aktiv sind. Sie wissen wahrscheinlich, dass die DOS-Box unter Windows (oder OS/2 oder Linux) im V86 mode läuft, aber was Sie vielleicht nicht bemerkt haben ist dass Speicherverwaltungsprogramme wie EMM386 die CPU ebenfalls in den V86 mode versetzen.

Wenn Sie einfach beginnen wollen, versuchen Sie es mit diesen Ratschlägen:

- Machen Sie sich keine Sorgen wegen dem Zurückwechseln in den real mode. Benutzen Sie einfach den reset-Knopf :)
- Lassen Sie die interrupts deaktiviert.
- Verwenden Sie keine LDTs (local descriptor tables).
- Erzeugen Sie im GDT (global descriptor table) nur 4 desciptors: null, code, stack/data und text/video.
- Setzen Sie die segment bases auf real mode-Werte, d.h. das 16-fache eines real mode Segmentregisterwertes. Das erlaubt es Ihnen, Variablen sowohl im real mode als auch im protected mode auf die gleiche Weise zu addressieren.
- Setzen Sie alle segment limits auf deren Maximum (0FFFFh unter 16-bit protected mode).
- Lassen Sie alle privilege-Werte auf 0 gesetzt (Ring 0, höchstes privilege-Niveau).
- Installieren Sie ein paar simple exception handlers (Programm, dass bei einem Ausnahmefehler aufgerufen wird (???)), die einfach ein Meldung in den Grafikspeicher schreiben und sich dann schließen:

  void unhand(void)
  { static const char Msg[]="U n h a n d l e d I n t e r r u p t ";
  disable();
  movedata(SYS_DATA_SEL, (unsigned)Msg,
  LINEAR_SEL, 0xB8000,
  sizeof(Msg));
  while(1); }

  Die alternating (???) Leerzeichen in der Meldung werden von der Videohardware des PC als Formatierungsangaben behandelt, die den Text in einem auffälligen Schwarz-auf-Grün erscheinen lassen. Setzen Sie ein interrupt gate in den geeigneten (alle? (???)) descriptor im IDT, gleichzeitig speichern Sie einen selector, der auf Ihr code segment verweist im selector-Feld des trap gate und den offset-Teil der Addresse dieser Routine im offset-Feld des trap gate.