Intel x86 Assembly Language & Microarchitecture
Echte versus beschermde modi
Zoeken…
Echte modus
Toen Intel de originele x86, de 8086 (en 8088-derivaat) ontwierp, omvatte deze segmentering om de 16-bits processor toegang te geven tot meer dan 16 bits aan adres. Ze deden dit door de 16-bit adressen relatief te maken aan een gegeven 16-bit Segment Register, waarvan ze er vier definieerden: Code Segment ( CS ), Data Segment ( DS ), Extra Segment ( ES ) en Stack Segment ( SS ) .
De meeste instructies impliceerden welk segmentregister moest worden gebruikt: instructies werden overgenomen uit het codesegment, PUSH en POP impliceerden het Stack-segment, en eenvoudige gegevensverwijzingen impliceerden het datasegment - hoewel dit zou kunnen worden genegeerd om toegang te krijgen tot geheugen in een van de andere segmenten.
De implementatie was eenvoudig: voor elke geheugentoegang zou de CPU het impliciete (of expliciete) segmentregister nemen, het vier plaatsen naar links verplaatsen en vervolgens het aangegeven adres toevoegen:
+-------------------+---------+
Segment | 16-bit value | 0 0 0 0 |
+-------------------+---------+
PLUS
+---------+-------------------+
Address | 0 0 0 0 | 16-bit value |
+---------+-------------------+
EQUALS
+-----------------------------+
Result | 20-bit memory address |
+-----------------------------+
Dit liet verschillende technieken toe:
- Toestaan dat Code, Data en Stack voor iedereen toegankelijk zijn (
CS,DSenSShadden allemaal dezelfde waarde); - Houd Code, Data en Stack volledig van elkaar gescheiden (
CS,DSenSSalle 4K (of meer) van elkaar gescheiden - onthoud dat het wordt vermenigvuldigd met 16, dus dat is 64K).
Het liet ook bizarre overlappingen en allerlei rare dingen toe!
Toen de 80286 werd uitgevonden, ondersteunde deze deze legacy-modus (nu "Real Mode" genoemd), maar voegde een nieuwe modus toe met de naam "Protected Mode" (qv).
Het belangrijkste om op te merken is dat in de Real-modus:
- Elk geheugenadres was toegankelijk, eenvoudig door de juiste waarde in een segmentregister te plaatsen en toegang te krijgen tot het 16-bits adres;
- De mate van "bescherming" was om de programmeur in staat te stellen verschillende delen van het geheugen te scheiden voor verschillende doeleinden, en het moeilijker maken om per ongeluk naar de verkeerde gegevens te schrijven - terwijl het nog steeds mogelijk was om dit te doen.
Met andere woorden ... helemaal niet erg beschermd!
Beveiligde modus
Invoering
Toen de 80286 werd uitgevonden, ondersteunde deze de oude 8086-segmentering (nu "Real Mode" genoemd) en voegde een nieuwe modus toe met de naam "Protected Mode". Deze modus is sindsdien in elke x86-processor aanwezig, zij het verbeterd met verschillende verbeteringen zoals 32- en 64-bit adressering.
Ontwerp
In de beveiligde modus is de eenvoudige "Adres toevoegen aan waarde voor verschoven segmentregistratie" volledig afgeschaft. Ze hielden de segmentregisters bij, maar in plaats van ze te gebruiken om een adres te berekenen, gebruikten ze ze om in een tabel (eigenlijk één van twee ...) te indexeren die het te benaderen segment definieerde. Deze definitie beschreef niet alleen waar het segment zich in het geheugen bevond (met Base en Limit), maar ook welk type segment het was (Code, Data, Stack of zelfs System) en welke soorten programma's er toegang toe hadden (OS Kernel, normaal programma , Device Driver, etc.).
Segmentregister
Elk 16-bits segmentregister nam de volgende vorm aan:
+------------+-----+------+
| 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
Wereldwijd / lokaal
Het globale / lokale bit definieerde of de toegang tot een globale tabel van descriptoren was (niet verrassend de globale beschrijvende tabel of GDT genoemd) of een lokale beschrijvende tabel (LDT). Het idee voor de LDT was dat elk programma zijn eigen Descriptor-tabel zou kunnen hebben - het besturingssysteem zou een wereldwijde set segmenten definiëren, en elk programma zou zijn eigen set lokale code, gegevens en stapelsegmenten hebben. Het besturingssysteem zou het geheugen tussen de verschillende Descriptor-tabellen beheren.
Descriptortabel
Elke descriptortabel (globaal of lokaal) was een 64K-array van 8.192 descriptoren: elk een record van 8 bytes dat meerdere aspecten definieerde van het segment dat het beschreef. De velden voor de Descriptor Index van de segmentregisters laten 8.192 descriptoren toe: geen toeval!
descriptor
Een Descriptor bevatte de volgende informatie - merk op dat het formaat van de Descriptor veranderde naarmate nieuwe processors werden uitgebracht, maar dezelfde soort informatie werd in elk bewaard:
- Baseren
Dit definieerde het startadres van het geheugensegment. - Limiet
Dit definieerde de grootte van het geheugensegment - soort van. Ze moesten een beslissing nemen: zou een grootte van0x0000een grootte van0betekenen, dus niet toegankelijk? Of maximale grootte?
In plaats daarvan kozen ze een derde optie: het veld Limiet was de laatst adresseerbare locatie binnen het segment. Dat betekende dat een one-bye segment kon worden gedefinieerd; of een maximale grootte voor de adresgrootte. - Type
Er waren meerdere soorten segmenten: de traditionele code, gegevens en stapel (zie hieronder), maar er werden ook andere systeemsegmenten gedefinieerd:- Local Descriptor Table Segments definieerde hoeveel Local Descriptors konden worden benaderd;
- Taakstatussegmenten kunnen worden gebruikt voor door hardware beheerde contextomschakeling;
- Gecontroleerde "Call Gates" waarmee programma's het besturingssysteem kunnen inschakelen - maar alleen via zorgvuldig beheerde toegangspunten.
- attributen
Bepaalde attributen van het Segment werden ook behouden, waar relevant:- Alleen-lezen versus lezen-schrijven;
- Of het segment nu aanwezig is of niet - geheugenbeheer op aanvraag mogelijk;
- Welk codeniveau (OS versus stuurprogramma versus programma) heeft toegang tot dit segment.
Eindelijk echte bescherming!
Als het besturingssysteem de descriptortabellen in segmenten zou houden die niet door alleen programma's konden worden geopend, dan kon het strak beheren welke segmenten werden gedefinieerd, en welk geheugen aan elk daarvan was toegewezen en toegankelijk was. Een programma kan produceren wat segmentregister waarde die het leuk vond! - maar als het de vermetelheid had om daadwerkelijk te laden in een segmentregister ... de CPU hardware zou erkennen dat de voorgestelde Descriptor waarde brak één van een groot aantal regels, en in plaats van het verzoek te voltooien, zou het een uitzondering op het besturingssysteem genereren om het foutieve programma te kunnen verwerken.
Deze uitzondering was meestal # 13, de algemene bescherminguitzondering - wereldberoemd gemaakt door Microsoft Windows ... (Heeft iemand een Intel-ingenieur bijgelovig?)
fouten
De soorten fouten die kunnen optreden, zijn onder meer:
Als de voorgestelde Descriptor-index groter was dan de grootte van de tabel;
Als de voorgestelde descriptor een systeemdescriptor was in plaats van code, gegevens of stapel;
Als de voorgestelde Descriptor meer bevoorrecht was dan het aanvragende programma;
Als de voorgestelde Descriptor was gemarkeerd als Niet leesbaar (zoals een codesegment), maar er werd geprobeerd het te lezen in plaats van uit te voeren;
Als de voorgestelde Descriptor is gemarkeerd als Niet aanwezig.
Merk op dat het laatste misschien geen fataal probleem is voor het programma: het besturingssysteem kan de vlag noteren, het segment herstellen, het markeren als nu aanwezig en vervolgens toestaan dat de foutinstructie succesvol verloopt.
Of misschien werd de Descriptor met succes in een segmentregister geladen, maar toen brak een toekomstige toegang daarmee een van een aantal regels:
- Het segmentregister is geladen met de
0x0000Descriptor Index voor de GDT. Dit werd door de hardware gereserveerd alsNULL; - Als de geladen Descriptor is gemarkeerd als Alleen-lezen, maar er is geprobeerd te schrijven.
- Als een deel van de toegang (1, 2, 4 of meer bytes) buiten de limiet van het segment viel.
Overschakelen naar de beveiligde modus
Overschakelen naar de beveiligde modus is eenvoudig: u hoeft alleen maar een bit in te stellen in een besturingsregister. Maar het verblijf in de beveiligde modus, zonder dat de CPU het gooien van zijn handen en zich steeds opnieuw instellen te wijten aan niet te weten wat te doen, vergt veel voorbereiding.
Kort gezegd zijn de vereiste stappen als volgt:
Er moet een geheugengebied voor de Global Descriptor Table worden ingesteld om minimaal drie Descriptors te definiëren:
- De zeroeth,
NULLDescriptor; - Nog een beschrijving voor een codesegment;
- Nog een beschrijving voor een gegevenssegment.
Dit kan worden gebruikt voor zowel Data als Stack.
- De zeroeth,
Het Global Descriptor Table Register (
GDTR) moet worden geïnitialiseerd om naar dit gedefinieerde geheugengebied te wijzen;GDT_Ptr dw SIZE GDT dd OFFSET GDT ... lgdt [GDT_Ptr]De
PMbit inCR0moet worden ingesteld:mov eax, cr0 ; Get CR0 into register or eax, 0x01 ; Set the Protected Mode bit mov cr0, eax ; We're now in Protected Mode!De segmentregisters moeten worden geladen vanuit de GDT om de huidige Real Mode-waarden te verwijderen:
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!
Merk op dat dit het absolute minimum is, alleen om de CPU in de beveiligde modus te krijgen. Om het hele systeem daadwerkelijk gereed te maken, zijn mogelijk veel meer stappen nodig. Bijvoorbeeld:
- De bovenste geheugengebieden moeten mogelijk worden ingeschakeld - de
A20poort uitschakelen; - De onderbrekingen moeten absoluut worden uitgeschakeld - maar misschien kunnen de verschillende foutafhandelaars worden ingesteld voordat de beveiligde modus wordt geactiveerd, om fouten in het begin van de verwerking mogelijk te maken.
De oorspronkelijke auteur van deze sectie heeft een volledige handleiding geschreven over het openen van de beveiligde modus en ermee werken.
Onwerkelijke modus
De onwerkelijke modus maakt gebruik van twee feiten over hoe zowel Intel- als AMD-processors de informatie laden en opslaan om een segment te beschrijven.
De processor slaat de beschrijvingsinformatie op die is opgehaald tijdens een verplaatsing in een selectorregister in beveiligde modus.
Deze informatie wordt opgeslagen in een architectonisch onzichtbaar deel van het selectorregister zelf.In de echte modus worden de selectorregisters segmentregisters genoemd, maar anders duiden ze dezelfde set registers aan en hebben ze als zodanig ook een onzichtbaar deel. Deze delen zijn gevuld met vaste waarden, maar voor de basis die is afgeleid van de zojuist geladen waarde.
In een dergelijke weergave is de echte modus slechts een speciaal geval van de beveiligde modus: waarbij de informatie van een segment, zoals de basis en limiet, wordt opgehaald zonder een GDT / LDT, maar nog steeds wordt gelezen uit het verborgen gedeelte van het segmentregister.
Door in beschermde modus te schakelen en een GDT te maken, is het mogelijk om een segment te maken met de gewenste attributen, bijvoorbeeld een basis van 0 en een limiet van 4GiB.
Door een opeenvolgend laden van een selectorregister worden dergelijke attributen in de cache opgeslagen, is het dan mogelijk om terug te schakelen in de echte modus en een segmentregister te hebben waardoor toegang tot de gehele 32-bit adresruimte.
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
overwegingen
- Zodra een segmentregister opnieuw wordt geladen, laadt de processor, zelfs met dezelfde waarde, de verborgen attributen opnieuw volgens de huidige modus. Dit is de reden waarom de bovenstaande code
fsengsgebruikt om de "uitgebreide" segmenten te bevatten: dergelijke registers worden minder waarschijnlijk gebruikt / opgeslagen / hersteld door de verschillende 16-bits services. - De
lgdtinstructie laadt geen verre aanwijzer naar de GDT, maar laadt in plaats daarvan een 24 bit (kan worden overschreven naar 32 bit) lineair adres . Dit is geen adres in de buurt , het is het fysieke adres (paging moet zijn uitgeschakeld). Dat is de reden vanGDT+7c00h. - Het bovenstaande programma is een bootloader (voor MBR, het heeft geen BPB) die
cs/ds/sstp 7c00h instelt en de locatieteller start vanaf 0. Dus een byte op offset X in het bestand is op offset X in het segment 7c00h en op het lineaire adres 7c00h + X. - Onderbrekingen moeten worden uitgeschakeld omdat een IDT niet is ingesteld voor de korte rondreis in beveiligde modus.
- De code gebruikt een hack om 6 bytes code op te slaan. De structuur geladen door
lgdtwordt opgeslagen in de ... GDT zelf, in delgdt(de eerste descriptor).
Zie hoofdstuk 3.4.3 van Intel Manual Volume 3A voor een beschrijving van de GDT-descriptors.