Protected Mode — Änderungen 3742 → 4387

Zeile 394: Zeile 394:
  
 
==Beispielcode==
 
==Beispielcode==
−
 
+
Eine knappe Variante in den Protected Mode zu schalten wird [[Umschalten in Protected Mode|hier]] erklärt.
−
So um nicht nur reine Theorie hier zum lesen zu geben folgt natürlich auch ein bisschen Code. Allerdings beschränkt sich das erstmal nur auf einen Codeteil, der in den Protected Mode schaltet und eine Funktion in einem C-Kernel aufruft.
 
−
 
 
−
 
 
−
Der Code wird ist so ausgelegt, das dieser von einem Bootloader (ich benutze meinen) an die Lineare Adresse 0x10000 (in Realmode: 0x1000:0000) geladen wird.
 
−
 
 
−
An dieser Adresse befindet sich dann der Kernel, der aus zwei Teilen besteht. Der eine Teil ist in 16-Bit Code geschrieben. Dieser Teil ist dafür verantwortlich in den Protected Mode zu schalten. Dazu enthält dieser Teil am Ende noch etwas 32-Bit Code um das A20 Gate zu aktivieren und in den C-Kernel zu springen.
 
−
 
 
−
 
 
−
So wir erstellen zuersteinmal den Kernelteil, der in den Protected Mode springt. Dazu erstellen wir eine neue ASM-Datei. Dort tragen wir als aller Erstes die NASM-Direktive [Bits 16] ein. Das signalisiert NASM das er erstmal 16 Bit Code erstellen soll. Dies ist nötig weil wir uns hier NOCH im Real-Mode befinden.
 
−
 
 
−
Dann kommt als allererstes ein JMP-Befehl. Dieser Jump soll zu einer Sprungmarke führen, die wir direkt unter den JMP-Befehl schreiben.
 
−
 
 
−
Als nächstes definieren wir die Deskriptoren. Dabei müssen alle Deskriptoren ZWISCHEN den JMP-Befehl und der Sprungmarke eingefügt werden. Dadurch erreichen wir, das der Prozessor unsere Deskriptoren überspringt und nicht versucht diese als Code auszuführen, was zu unschönen Ergebnissen führen sollte.
 
−
 
 
−
 
 
−
Nun benötigen wir aber erstmal die 3 Deskriptoren, die für den Switch in den Protected Mode unerlässlich sind. Der erste, der Null-Deskriptor, ist schnell erzeugt. Dazu werden einfach 8 Bytes mit 0 gefüllt:
 
−
<asm>NULL_Desc:
 
−
dd 0 ;Ein DWORD mit dem Wert 0
 
−
dd 0 ;Ein DWORD mit dem Wert 0</asm>Als nächstes brauchen wir einen Deskriptor für ein Code Segment. Diesen erstellen wir ganz einfach mit dem Programm (CalcDesc.exe) das ich als Download zur Verfügung stelle. Von diesem Programm werden ein paar Fragen gestellt. Diese beantworten wir für den Code Deskriptor wie folgt:
 
−
 
 
−
Als erstes Wählen wir ein Code Segment (Execute-Read). Damit sagen wir das wir ein Codesegment haben möchten, das sowohl ausführbaren Code enthalten darf und es uns gestattet ist daraus zu lesen.
 
−
 
 
−
Als nächstes Sagen wir, das das Segment Present sein soll.
 
−
 
 
−
Als Privilegstufe kommt nur 0 in Frage. Wir benötigen für den Kernel schließlich alle Rechte.
 
−
 
 
−
Als Segment-Size-Unit nehmen wir "4096 Bytes per Unit". Nur so können wir ein Segment erzeugen das größer als 1 MB ist.
 
−
 
 
−
Da wir ein Segment für den 386 (oder höher) haben möchten wählen wir 32-Bit.
 
−
 
 
−
So nun müssen wir die Segmentbasisadresse angaben. Diese Angabe muss in Hexadezimalform angegeben werden. Da unser Segment sich über den kompletten 4 GB Adressraum erstrecken soll, müssen wir hier 0 angeben.
 
−
 
 
−
Die Segmentgröße müssen wir mit 0xFFFFF angeben (4 GB).
 
−
 
 
−
Nun erhalten wir ein Ergebnis, welches wir direkt übernehmen können:<asm>CODE_Desc:
 
−
dw 0xFFFF
 
−
dw 0
 
−
db 0
 
−
db 0x9A
 
−
db 0xCF
 
−
db 0</asm>So nun brauchen wir auch noch ein Daten Segment. Dazu im schnelldurchgang die Angaben für CalcDesc:
 
−
 
 
−
1. Datensegment Read-Write
 
−
 
 
−
2. Segment present
 
−
 
 
−
3. Privilegstufe 0
 
−
 
 
−
4. 4096 Byte per Unit
 
−
 
 
−
5. 32-Bit
 
−
 
 
−
6. Basisadresse: 0
 
−
 
 
−
7. Segmentgröße: 0xFFFFF
 
−
 
 
−
Ergebnis:<asm>DATA_Desc:
 
−
dw 0xFFFF
 
−
dw 0
 
−
db 0
 
−
db 0x92
 
−
db 0xCF
 
−
db 0</asm>So direkt hinter den Deskriptoren, ABER IMMER NOCH VOR DER SPRUNGMARKE, definieren wir folgendes:<asm>gdt:
 
−
Limit dw 0
 
−
Base dd 0</asm>Diese Variablen werden wir gleich füllen. Das sind dann die Angaben mit denen wir das GDT Register füttern werden.
 
−
 
 
−
Jetzt werden wir erstmal die Interrupts deaktivieren und das DS Register so laden, das es auf das selbe Segment wie CS zeigt.
 
−
 
 
−
Das erreichen wir mit folgendem Code:<asm>cli
 
−
mov eax, cs
 
−
mov ds, ax</asm>Etwas verwirrend mag erscheinen, das wir das EAX Register mit dem Wert von CS füllen. Auch wenn wir im Real-Mode sind, können wir trotzdem 32 Bit Register benutzen. Das benutzen des EAX Register hat auch eine zweck der im folgenden Codeabschnitt deutlich wird.
 
−
 
 
−
Wir haben nämlich ein kleines Problem. Kurz nachdem wir (später) in den Protected Mode geschaltet haben, müssen wir einen FAR JUMP ausführen. Mit diesem erreichen wir, das der Deskriptor für das Code Segment den wir erstellt haben geladen wird. Zusätzlich wird die Prefetch Queue des Prozessors gelöscht. In dieser Warteschlange (Queue) warten nämlich noch 16-Bit Befehle, die der Prozessor schonmal im Vorraus teilweise dekodiert hat. Da wir aber in den Protected Mode springen und wir dort nur mit 32-Bit Befehlen arbeiten, können wir diese 16-Bit Befehle in der Queue nicht gebrauchen.
 
−
 
 
−
Das Dumme ist nur, das dieser FAR JUMP eine gewisse Einschränkung hat. Wir können da nämlich nur den Selektor für das Code Segment und eine 16-Bit Offset Adresse angeben. Und genau bei diesen 16 Bit liegt unser Problem. Die meisten Leute (so wie ich) laden ihren Kernel an die Lineare Adresse 0x10000. Diese Adresse ist aber mit einem 16 Bit Wert nicht mehr erreichbar. Das heisst wir können nicht in das Code Segment springen, das ja bei der Linearen Adresse 0 beginnt und gleichzeitig zu dem Offset springen das genau hinter unserem FAR JUMP Befehl liegt. Daher müssen wir uns abhelfen. Um das Problem zu lösen, werden wir zur Laufzeit den Deskriptor für das Code Segment so umschreiben, das seine Lineare Basisadresse nicht mehr bei 0 liegt, sondern bei der Linearen Adresse an welche wir unseren Kernel geladen haben. Nämlich 0x10000. Wir machen das zur Laufzeit, damit wir später auch die Möglichkeit haben unseren Kernel auch an eine andere Speicherstelle laden zu können, ohne den Deskriptor jedesmal von Hand ändern zu müssen.
 
−
 
 
−
Um nun den Deskriptor zu ändern, finden wir erstmal heraus in welchem Real-Mode Segment wir uns befinden. Das erreichen wir indem wir das EAX Register mit dem Wert des CS Register laden. Dann verschieben wir das EAX Register um 4 Bits nach links. Damit erhalten wir die Lineare Adresse des Segmentes. Diese tragen wir dann in den Deskriptor ein. Da wie wir schon gesehen haben der Deskriptor die Angabe der Basisadresse etwas in den 8 Bytes verteilt hinterlegt, müssen wir die Angabe etwas splitten. Das wird aber im folgenden Code ersichtlich:<asm>shl eax, 4 ;EAX beinhaltet immer noch den Wert von CS (siehe letzten Codeabschnitt)
 
−
mov [CODE_Desc+2], ax ;Den ersten Teil der Linearen Adresse eintragen
 
−
mov [DATA_Desc+2], ax
 
−
shr eax, 16
 
−
mov [CODE_Desc+4], al ;Den zweiten Teil der Linearen Adresse eintragen
 
−
mov [DATA_Desc+4], al</asm>So nun müssen wir noch die Basisadresse und das Limit für die GDT eintragen. Dazu müssen wir zuerst wieder errechnen wo sich die GDT befindet. Dazu holen wir uns wieder den Wert des CS Registers nach EAX und schieben dieses wieder 4 Bits nach links. Dann addieren wir das Offset NULL_Desc, da dieses das Startoffset der GDT darstellt.
 
−
 
 
−
Das Limit errechnen wir, indem wir das Startoffset der GDT (NULL_Desc) vom Offset gdt subtrahieren. Das Offset gdt ist direkt hinter den Deskriptoren und kann somit als Ende der GDT angesehen werden. Wenn die Berechnung korrekt gelaufen ist, sollten wir als Wert 24 (3*8) erhalten. Jedoch haben wir (siehe GDT Abschnitt) gesagt das immer eins von dem Wert abgezogen werden muss. Also erhalten wir als endgültiges Limit den Wert 23, den wir dann auch eintragen.
 
−
 
 
−
Das ganze geschieht in folgendem Code:<asm>mov eax, cs
 
−
shl eax, 4
 
−
add eax, NULL_Desc
 
−
mov [Base], eax
 
−
mov [Limit], WORD gdt - NULL_Desc - 1 ;Das passte leider nicht alles hintereinander</asm>So nun können wir das GDT Register laden.<asm>lgdt [gdt]</asm>So nun kommt endlich das was wir eigentlich erreichen wollen. Das schalten in den Protected Mode. So banal der folgende Code auch erscheinen mag. Er hat eine große Auswirkung und bedarf daher auch dieser ganzen Vorarbeit.<asm>mov eax, cr0 ;Das CR0 Register in EAX laden
 
−
or eax, 1 ;Das erste Bit (PE Bit) auf 1 setzen
 
−
mov cr0, eax ;Das CR0 Register mit dem neuen Wert laden</asm>Nun befinden wir uns schon im Protected Mode. War doch ganz einfach oder? Jetzt müssen wir nur noch das CS Register laden und die Prefetch leeren. Das errichen wir mit dem folgenden Code. Es sieht zwar so aus als würden wir Variablen deklarieren, jedoch sind die Werte die wir in die Variablen schreiben das Bitmuster das ein FAR JUMP Befehl hat.<asm>db 0xea ;Bitmuster für einen FAR JUMP
 
−
dw PMODE ;Angabe des Offsets zu dem gesprungen werden soll
 
−
dw 0x8 ;Angabe des Selektors für das Segment zu dem gesprungen werden soll.</asm>So nun fehlt nur noch die Deklaration des Labels PMODE. Und da wir von da an mit 32 Bit Code arbeiten teilen wir das NASM noch mit der Direktive [Bits 32] mit. Und da wir noch nix weiter an Code haben, lassen wir eine Endlosschleife laufen.<asm>[Bits 32]
 
−
PMODE:
 
−
jmp PMODE</asm>Damit wir nun auch ohne Gefahren arbeiten können, müssen wir noch schnell die anderen benötigten Segmentregister laden.
 
−
 
 
−
 
 
−
Bevor wir das tun, helfen wir uns mit einem kleinen Trick aus einer gewissen "schlechten Lage". Wie oben schon erklärt konnten wir die Startadresse des Codesegmentes nicht auf 0 legen. Folglich haben wir im weiteren ein blödes Problem. Wir haben zum einen 64 KB Speicher auf die wir nicht zugreifen können, da wir diese im Segemten "übersprungen" haben und alle späteren Angaben (z.b. Videospeicheradresse) verschiebt sich ebenfalls ein Stück, da wir ja immer das Offset zum Segmentbeginn angeben. Und wenn unser Segment nunmal NICHT bei 0 beginnt, haben wir etwas kuddel muddel. Wir mussten das so machen, da wir beim FAR JUMP in den Protected Mode nur ein 16 Bit Offset angeben konnten.
 
−
 
 
−
Nun befinden wir uns aber im Protected Mode und auch im 32 Bit Bereich. Folglich setzen wir die Segmantbasisadresse der Deskriptoren einfach wieder auf 0 und springen nun mit Hilfe der Angabe eines 32 Bit Offsets an die richtige Stelle. Somit haben wir dann den vollen 4 GB Adressraum zur Verfügung und müssen uns auch nicht mehr um das umrechnen von Adressen kümmern. Um das ganze zu realisieren machen wir folgendes:<asm>mov WORD [CODE_Desc+2], 0 ;Code Segmentstartaddresse auf 0 setzen
 
−
mov WORD [DATA_Desc+2], 0 ;Daten Segmentstartadresse auf 0 setzen
 
−
mov BYTE [CODE_Desc+4], 0 ;Code Segmentstartaddresse auf 0 setzen
 
−
mov BYTE [DATA_Desc+4], 0 ;Daten Segmentstartadresse auf 0 setzen</asm>Nun können wir die Segmente einfach laden.
 
−
 
 
−
Dabei gehen wir vorsichtig vor. Wir schreiben zuerst den Wert 2 in ax. Wir wollen den Eintrag der GDT mit dem Index 2 (also der dritte Eintrag), welches das Daten Segment ist, haben. Dann verschieben wir das ganze um 3 Bits nach links.
 
−
 
 
−
Man erinnere dazu nochmal schnell an den Aufbau eines Selektors.
 
−
 
 
−
Jetzt laden wir das DS, SS und ES Register mit diesem Selektor. Die Register FS und GS laden wir erstmal mit dem Null Deskriptor, da wir für diese vorerst noch keine Verwendung haben. Zusätzlich laden wir noch das ESP Register mit dem Wert 0x1FFFFF um den Stack ausreichend weit von unserem Kernel zu entfernen. Dieser Wert entspricht der 2 MB Grenze. Soviel Speicher sollte jeder Rechner besitzen, weshalb wir somit auch nicht in Gefahr kommen das unser Stack außerhalb des Speichers liegt.
 
−
 
 
−
 
 
−
Dazu dient folgender Code:<asm>mov eax, 2 ;Index 2 der GDT auswählen
 
−
shl eax, 3 ;Im Selektor beginnt der Index erst ab Bit 3
 
−
mov ds, ax
 
−
mov es, ax
 
−
mov ss, ax
 
−
mov eax, 0
 
−
mov fs, ax ;FS und GS auf NULL Deskriptor zeigen lassen
 
−
mov gs, ax
 
−
mov esp, 0x1FFFFF ;ESP auf 2 MB setzen</asm>So nun müssen wir NOCH einen FAR JUMP in das Codesegment machen. Aus dem einfachen Grund, da wir den Deskriptor ja verändert haben und diesen durch den FAR JUMP neu laden müssen.
 
−
 
 
−
 
 
−
Dazu nutzen wir folgenden Code:<asm>jmp 0x8:0x10000 + PMODE2 ;Sprung in das "neue" Codesegment
 
−
PMODE2:</asm>Hier springen wir einfach an eine neue Sprungmarke und müssen zu dem Offset natürlich noch die lineare Adresse addieren, an welche wir unseren Kernel vom Bootloader haben laden lassen.
 
−
 
 
−
 
 
−
So das wäre es soweit. Von hier aus kann man dann in einen C-Kernel springen oder in ASM weitermachen. Natürlich muss man den Code etwas modifizieren, wenn man die GDT zur Laufzeit erweitern möchte. In diesem Falle wäre es angebracht hinter GDT alle restlichen der 65536 Bytes die die GDT umfassen kann, hier aber nicht genutzt wurden, mit Nullen zu füllen.
 
−
 
 
−
 
 
−
Hier nun auch noch der Beispielcode und das Programm CalcDesc zum runterladen. Das Programm wird in nächster Zeit noch aktualisiert, so das auch System Segment Deskriptoren damit erstellt werden können.
 
−
 
 
−
[http://www.jay-code.de/files/pmode.zip Download]
 
−
 
 
−
 
 
−
 
 
−
 
 
−
'''Anmerkung 1:''' Da der Code davon ausgeht, dass er am Anfang eines Segmentes steht, sollte man ihn mit einem geeigeneten Bootloader laden, der dies beachtet. Ein schlankes Beispiel wäre:
 
−
 
 
−
<asm>mov ax,cs
 
−
mov ds,ax
 
−
mov es,ax
 
−
 
 
−
cli
 
−
mov ss,ax
 
−
mov sp,0x20000
 
−
sti
 
−
 
 
−
mov ax,0x9000
 
−
mov es,ax
 
−
mov bx,0x0
 
−
mov ah,02h
 
−
mov al,1
 
−
mov cl,2
 
−
mov ch,0
 
−
mov dl,0
 
−
mov dh,0
 
−
int 13h
 
−
 
 
−
jmp 0x9000:0x0
 
−
 
 
−
times 510-($-$$) db 0
 
−
dw 0aa55h</asm>© by PMTheQuick
 
−
 
 
−
Die beiden 0x9000 können natürlich durch irgendeine andere Adresse ersetzt werden, jenachdem wohin man seinen Kernel laden will. Dann kann man die Dateien assemblieren + zusammenkopieren um ein funktionierendes Image zu erhalten:
 
−
<pre>nasm -f bin -o kernel16.bin kernel16.asm
 
−
nasm -f bin -o bootsec.bin bootsec.asm
 
−
copy /B bootsec.bin + /B kernel16.bin PM_OS.img</pre>
 
−
 
 
−
'''Anmerkung 2:''' Eine weitere, etwas weniger komplizierte Variante in den Protected Mode zu schalten wird [[Umschalten in Protected Mode|hier]] erklärt.
 
  
 
[[Category:Tutorials]]
 
[[Category:Tutorials]]