Intel x86 Assembly Language & Microarchitecture
Echt gegen geschützte Modi
Suche…
Realer Modus
Bei der Entwicklung des ursprünglichen x86, des 8086 (und des 8088-Derivats) von Intel wurde die Segmentierung verwendet, damit der 16-Bit-Prozessor auf Adressen mit mehr als 16 Bit zugreifen kann. Sie taten dies, indem sie die 16-Bit-Adressen relativ zu einem bestimmten 16-Bit-Segmentregister machten, von dem sie vier definierten: Codesegment ( CS ), Datensegment ( DS ), Extra-Segment ( ES ) und Stapelsegment ( SS ). .
Die meisten Anweisungen implizierten, welches Segmentregister verwendet werden sollte: Anweisungen wurden aus dem Codesegment übernommen, PUSH und POP implizierten das Stapelsegment, und einfache Datenreferenzen implizierten das Datensegment - obwohl dies für den Zugriff auf den Speicher in einem der anderen Segmente überschrieben werden konnte.
Die Implementierung war einfach: Für jeden Speicherzugriff würde die CPU das implizierte (oder explizite) Segmentregister übernehmen, um vier Stellen nach links verschieben und dann die angegebene Adresse hinzufügen:
+-------------------+---------+
Segment | 16-bit value | 0 0 0 0 |
+-------------------+---------+
PLUS
+---------+-------------------+
Address | 0 0 0 0 | 16-bit value |
+---------+-------------------+
EQUALS
+-----------------------------+
Result | 20-bit memory address |
+-----------------------------+
Dies ermöglichte verschiedene Techniken:
- Zulassen, dass Code, Daten und Stack gegenseitig zugänglich sind (
CS,DSundSSalle denselben Wert); - Wenn Code, Daten und Stack vollständig voneinander getrennt sind (
CS,DSundSSalle 4K (oder mehr) voneinander getrennt - denken Sie daran, dass sie mit 16 multipliziert werden, also 64K).
Es erlaubte auch bizarre Überlappungen und allerlei seltsame Dinge!
Als der 80286 erfunden wurde, unterstützte er diesen Legacy-Modus (jetzt "Real Mode"), fügte jedoch einen neuen Modus hinzu, der als "Protected Mode" (qv) bezeichnet wird.
Das Wichtigste, was zu beachten ist, ist das im Real-Modus:
- Auf jede Speicheradresse konnte zugegriffen werden, indem einfach der korrekte Wert in ein Segmentregister eingegeben und auf die 16-Bit-Adresse zugegriffen wurde.
- Das Ausmaß des "Schutzes" sollte es dem Programmierer ermöglichen, verschiedene Speicherbereiche für verschiedene Zwecke zu trennen, und es schwieriger zu machen, versehentlich auf die falschen Daten zu schreiben - und dies dennoch zu ermöglichen.
Mit anderen Worten ... überhaupt nicht sehr geschützt!
Sicherheitsmodus
Einführung
Als der 80286 erfunden wurde, unterstützte er die alte 8086-Segmentierung (jetzt "Real Mode") und fügte einen neuen Modus hinzu, der als "Protected Mode" bezeichnet wird. Dieser Modus wurde seitdem in jedem x86-Prozessor verwendet, jedoch mit verschiedenen Verbesserungen, wie z. B. 32- und 64-Bit-Adressierung, verbessert.
Design
Im geschützten Modus wurde auf das einfache "Hinzufügen der Adresse zum Registerwert des verschobenen Segmentes" vollständig verzichtet. Sie behielten die Segmentregister, aber anstatt sie zur Berechnung einer Adresse zu verwenden, verwendeten sie sie zum Indexieren in eine Tabelle (eigentlich eine von zwei ...), die das Segment definiert, auf das zugegriffen werden soll. In dieser Definition wurde nicht nur beschrieben, wo sich das Segment im Speicher befand (mithilfe von Basis und Limit), sondern auch, um welche Art von Segment es sich handelt (Code, Daten, Stack oder sogar System) und auf welche Arten von Programmen (OS-Kernel, normales Programm) , Gerätetreiber usw.).
Segmentregister
Jedes 16-Bit-Segmentregister hat folgende Form:
+------------+-----+------+
| Desc Index | G/L | Priv |
+------------+-----+------+
Desc Index = 13-bit index into a Descriptor Table (described below)
G/L = 1-bit flag for which Descriptor Table to Index: Global or Local
Priv = 2-bit field defining the Privilege level for access
Global / lokal
Das Global / Local-Bit definiert, ob der Zugriff auf eine Globale Tabelle von Deskriptoren (nicht überraschend als globale Deskriptortabelle oder GDT bezeichnet) oder eine lokale Deskriptortabelle (LDT) erfolgt. Die Idee für die LDT war, dass jedes Programm eine eigene Deskriptortabelle haben könnte - das Betriebssystem würde einen globalen Satz von Segmenten definieren, und jedes Programm hätte seinen eigenen Satz von lokalem Code, Daten- und Stapelsegmenten. Das Betriebssystem würde den Speicher zwischen den verschiedenen Deskriptortabellen verwalten.
Deskriptortabelle
Jede Deskriptortabelle (global oder lokal) bestand aus einem 64-KByte-Array mit 8.192 Deskriptoren: jeweils einem 8-Byte-Datensatz, der mehrere Aspekte des von ihm beschriebenen Segments definierte. Die Deskriptor-Indexfelder der Segmentregister erlaubten 8.192 Deskriptoren: kein Zufall!
Deskriptor
Ein Deskriptor enthielt die folgenden Informationen. Beachten Sie, dass sich das Format des Deskriptors mit der Veröffentlichung neuer Prozessoren geändert hat. In beiden Fällen wurde jedoch dieselbe Art von Information beibehalten:
- Base
Dies definiert die Startadresse des Speichersegments. - Grenze
Dies definierte die Größe des Speichersegments - irgendwie. Sie mussten eine Entscheidung treffen: Würde eine Größe von0x0000eine Größe von0bedeuten, also nicht erreichbar? Oder maximale größe?
Stattdessen wählten sie eine dritte Option: Das Limit-Feld war der letzte adressierbare Ort innerhalb des Segments. Das bedeutete, dass ein One-Bye-Segment definiert werden konnte; oder eine maximale Größe für die Adressgröße. - Art
Es gab verschiedene Arten von Segmenten: den herkömmlichen Code, Daten und Stack (siehe unten), aber auch andere Systemsegmente wurden definiert:- Lokale Deskriptortabellensegmente definiert, auf wie viele lokale Deskriptoren zugegriffen werden kann;
- Taskstatus-Segmente können für den durch Hardware verwalteten Kontextwechsel verwendet werden.
- Kontrollierte "Call Gates", mit denen Programme das Betriebssystem aufrufen können - jedoch nur durch sorgfältig verwaltete Einstiegspunkte.
- Attribute
Bestimmte Attribute des Segments wurden gegebenenfalls ebenfalls gepflegt:- Schreibgeschützt vs. Lese- / Schreibzugriff;
- Ob das Segment aktuell vorhanden ist oder nicht - ermöglicht eine bedarfsgerechte Speicherverwaltung;
- Welche Codeebene (Betriebssystem vs. Treiber vs. Programm) kann auf dieses Segment zugreifen.
Endlich wahrer Schutz!
Wenn das Betriebssystem die Deskriptortabellen in Segmenten aufbewahrte, auf die nur Programme zugreifen konnten, konnte es genau festlegen, welche Segmente definiert wurden und welcher Speicher für jeden zugewiesen und zugänglich war. Ein Programm könnte jeden beliebigen Segment-Register-Wert herstellen - aber wenn es die Kühnheit hätte , es tatsächlich in ein Segment-Register zu laden ! ... würde die CPU-Hardware erkennen, dass der vorgeschlagene Descriptor-Wert eine der vielen Regeln brach, und Anstatt die Anforderung abzuschließen, wird eine Ausnahme an das Betriebssystem ausgegeben, damit das fehlerhafte Programm verarbeitet werden kann.
Diese Ausnahme war normalerweise die Nummer 13, die allgemeine Schutzausnahme, die durch Microsoft Windows weltbekannt wurde ... (Wer glaubt, dass ein Intel-Ingenieur Aberglaube war?)
Fehler
Zu den möglichen Fehlern gehören:
Wenn der vorgeschlagene Deskriptorindex größer als die Größe der Tabelle war;
Wenn der vorgeschlagene Deskriptor ein Systemdeskriptor und nicht Code, Daten oder Stack war.
Wenn der vorgeschlagene Deskriptor privilegierter war als das anfordernde Programm;
Wenn der vorgeschlagene Deskriptor als nicht lesbar markiert wurde (z. B. ein Codesegment), wurde jedoch versucht, gelesen und nicht ausgeführt zu werden.
Wenn der vorgeschlagene Deskriptor als Nicht vorhanden markiert wurde.
Beachten Sie, dass das letzte nicht ein schwerwiegendes Problem für das Programm sein kann: Das Betriebssystem könnte das Flag merken, das Segment wiederherstellen, es als jetzt Vorhanden markieren und dann den fehlerhaften Befehl erfolgreich ablaufen lassen.
Oder der Deskriptor wurde möglicherweise erfolgreich in ein Segmentregister geladen, aber dann löste ein zukünftiger Zugriff damit eine der folgenden Regeln:
- Das Segmentregister wurde mit dem
0x0000Descriptor Index für die GDT geladen. Dies wurde von der Hardware alsNULLreserviert; - Wenn der geladene Deskriptor als schreibgeschützt markiert wurde, wurde jedoch ein Schreibvorgang ausgeführt.
- Wenn sich ein Teil des Zugriffs (1, 2, 4 oder mehr Bytes) außerhalb der Begrenzung des Segments befand.
In den geschützten Modus wechseln
Das Umschalten in den geschützten Modus ist einfach: Sie müssen nur ein einzelnes Bit in einem Steuerregister setzen. Um im geschützten Modus zu bleiben , ohne dass die CPU ihre Hände hochwirft und sich selbst zurückstellt, weil sie nicht weiß, was als nächstes zu tun ist, ist viel Vorbereitung erforderlich.
Kurz gesagt, die folgenden Schritte sind erforderlich:
Ein Speicherbereich für die globale Deskriptortabelle muss eingerichtet werden, um mindestens drei Deskriptoren zu definieren:
- Der nullte,
NULLDeskriptor; - Ein weiterer Deskriptor für ein Codesegment;
- Ein weiterer Deskriptor für ein Datensegment.
Dies kann sowohl für Daten als auch für Stapel verwendet werden.
- Der nullte,
Das Global Descriptor Table Register (
GDTR) muss initialisiert werden, um auf diesen definierten Speicherbereich zu zeigen.GDT_Ptr dw SIZE GDT dd OFFSET GDT ... lgdt [GDT_Ptr]Das
PMBit inCR0muss gesetzt sein:mov eax, cr0 ; Get CR0 into register or eax, 0x01 ; Set the Protected Mode bit mov cr0, eax ; We're now in Protected Mode!Die Segmentregister müssen aus dem GDT geladen werden, um die aktuellen Werte des Real-Modus zu entfernen:
jmp 0x0008:NowInPM ; This is a FAR Jump. 0x0008 is the Code Descriptor NowInPM: mov ax, 0x0010 ; This is the Data Descriptor mov ds, ax mov es, ax mov ss, ax mov sp, 0x0000 ; Top of stack!
Beachten Sie, dass dies das absolute Minimum ist, um die CPU in den geschützten Modus zu bringen. Um das gesamte System tatsächlich vorzubereiten, sind möglicherweise viele weitere Schritte erforderlich. Zum Beispiel:
- Möglicherweise müssen die oberen Speicherbereiche aktiviert werden. Schalten Sie das
A20Gate aus. - Die Interrupts sollten auf jeden Fall deaktiviert werden. Möglicherweise können jedoch die verschiedenen Fehlerhandler eingerichtet werden, bevor der geschützte Modus aktiviert wird, um Fehler in der Verarbeitung frühzeitig zu berücksichtigen.
Der ursprüngliche Autor dieses Abschnitts hat ein komplettes Lernprogramm geschrieben, in dem Sie den geschützten Modus aufrufen und damit arbeiten.
Unreal-Modus
Der Unreal-Modus nutzt zwei Fakten, wie sowohl Intel- als auch AMD-Prozessoren die Informationen laden und speichern, um ein Segment zu beschreiben.
Der Prozessor speichert die während einer Verschiebung abgerufenen Deskriptorinformationen in einem Auswahlregister im geschützten Modus.
Diese Informationen werden in einem architektonisch unsichtbaren Teil des Auswahlregisters selbst gespeichert.Im Real-Modus werden die Selektorregister als Segmentregister bezeichnet, ansonsten bezeichnen sie jedoch dieselbe Gruppe von Registern und haben daher auch einen unsichtbaren Teil. Diese Teile werden mit festen Werten gefüllt, jedoch für die Basis, die sich aus dem gerade geladenen Wert ergibt.
In einer solchen Ansicht ist der Realmodus nur ein Sonderfall des Protected Mode: Hier werden die Informationen eines Segments wie Basis und Limit ohne GDT / LDT abgerufen, aber immer noch aus dem verborgenen Teil des Segmentregisters gelesen.
Durch Umschalten in den geschützten Modus und Herstellen eines GDT ist es möglich, ein Segment mit den gewünschten Attributen zu erstellen, z. B. einer Basis von 0 und einem Limit von 4 GB.
Durch ein sukzessives Laden eines Auswahlregisters werden solche Attribute zwischengespeichert, es ist dann möglich, in den Realmodus zurückzuschalten und ein Segmentregister zu haben, durch das der gesamte 32-Bit-Adressraum zugänglich ist.
BITS 16
jmp 7c0h:__START__
__START__:
push cs
pop ds
push ds
pop ss
xor sp, sp
lgdt [GDT] ;Set the GDTR register
cli ;We don't have an IDT set, we can't handle interrupts
;Entering protected mode
mov eax, cr0
or ax, 01h ;Set bit PE (bit 0) of CR0
mov cr0, eax ;Apply
;We are now in Protected mode
mov bx, 08h ;Selector to use, RPL = 0, Table = 0 (GDT), Index = 1
mov fs, bx ;Load FS with descriptor 1 info
mov gs, bx ;Load GS with descriptor 1 info
;Exit protected mode
and ax, 0fffeh ;Clear bit PE (bit0) of CR0
mov cr0, eax ;Apply
sti
;Back to real mode
;Do nothing
cli
hlt
GDT:
;First entry, number 0
;Null descriptor
;Used to store a m16&32 object that tells the GDT start and size
dw 0fh ;Size in byte -1 of the GDT (2 descriptors = 16 bytes)
dd GDT + 7c00h ;Linear address of GDT start (24 bits)
dw 00h ;Pad
dd 0000ffffh ;Base[15:00] = 0, Limit[15:00] = 0ffffh
dd 00cf9200h ;Base[31:24] = 0, G = 1, B = 1, Limit[19:16] = 0fh,
;P = 1, DPL = 0, E = 0, W = 1, A = 0, Base[23:16] = 00h
TIMES 510-($-$$) db 00h
dw 0aa55h
Überlegungen
- Sobald ein Segmentregister selbst mit demselben Wert erneut geladen wird, lädt der Prozessor die verborgenen Attribute gemäß dem aktuellen Modus neu. Aus diesem Grund verwendet der obige Code
fsundgs, um die "erweiterten" Segmente zu speichern: Solche Register werden weniger wahrscheinlich von den verschiedenen 16-Bit-Diensten verwendet / gespeichert / wiederhergestellt. - Der
lgdtkeinenlgdtauf den GDT, sondern lädt eine lineare Adresse mit 24 Bit (kann auf 32 Bit überschrieben werden). Dies ist keine nahe Adresse , sondern die physische Adresse (da Paging deaktiviert werden muss). Das ist der Grund vonGDT+7c00h. - Das obige Programm ist ein Bootloader (für MBR hat es keine BPB), der
cs/ds/sstp 7c00h setzt und den Positionszähler von 0 aus startet. Ein Byte am Offset X in der Datei ist also am Offset X im Segment 7c00h und an der linearen Adresse 7c00h + X. - Die Interrupts müssen deaktiviert werden, da im geschützten Modus keine IDT für die kurze Rundfahrt eingestellt ist.
- Der Code verwendet einen Hack, um 6 Byte Code zu speichern. Die von
lgdtgeladenelgdtwird in der ... GDT selbst imlgdt(dem ersten Deskriptor) gespeichert.
Eine Beschreibung der GDT-Deskriptoren finden Sie in Kapitel 3.4.3 von Intel Manual Volume 3A .