Die A20-Line aktivieren
Wie ich im letzten Artikel geschrieben habe, gibt es aus historischen Gründen noch ein Problem mit der Speicheradressierung. Und zwar konnte man in den alten 8086-Prozessoren über 20 Adressleitungen den Speicher adressieren, also exakt 1MB. Das Adressierungsschema des Realmode erlaubt jedoch einen größeren Adressierungsbereich, nämlich wenn man das Segment 0xFFFF
und den Offset 0xFFFF
wählt. Dies liefert dann über die bekannte Formel Adresse = 16 * Segment + Offset
die Adresse: 0x10FFEF. Über die 20 Adressleitungen der 8086-Prozessoren konnte die vordere 1 (auf dem 21. Bit) nicht übertragen werden.
Mit einer neuen Architektur kamen dann mehrere Adressleitungen hinzu. Nun gab es das Problem, dass manch alter Programmcode davon ausging, dass bei Adressen wie der obigen genannten das höchste Bit abgeschnitten wird. Auf den neuen Architekturen war dies nicht der Fall, wodurch alte Programme nicht mehr gelaufen wären. Deshalb hat man ein Flag eingeführt, mit dem man die Adressleitung des 21. Bits (A20) deaktivieren konnte.
Diese Leitung müssen wir nun aktivieren, wenn wir mehr als 20 Bits zur Adressierung verwenden wollen. Leider gibt es hierzu auch wieder mehrere Methoden, die man alle durchprobieren sollte. Wir werden in diesem Artikel alle durchsprechen und implementieren.
BIOS-Interrupt
Beginnen wir mit der einfachsten Möglichkeit, einem BIOS Interrupt. Historisch gesehen ist dies nicht die erste Variante, sondern sie wurde nachträglich in einigen BIOS implementiert. Hierzu muss man lediglich den Code 0x2401
an den Interrupt 0x15
senden. Wurde der Befehl erfolgreich ausgeführt, ist das Carryflag ungesetzt und AX=0, im anderen Fall ist das Carryflag gesetzt und AX ungleich 0.
Wie ein BIOS-Interrupt verwendet wird, haben wir bereits in einem früheren Artikel gelernt. Für Interrupt 0x15
müssen wir den gewünschten Code ins Register ax
schreiben. In unserem Fall führt das also zu den Zeilen:
mov ax, 2401h
int 15h
Da wir bereits wissen, dass noch mehrere weitere Methoden hinzukommen können und der BIOS-Interrupt uns einen Rückgabecode über Erfolg oder Fehler gibt, können wir noch ein Grundgerüst bauen, womit wir zu den richtigen Codezeilen springen können:
a20_bios:
mov ax, 2401h
int 15h
jnc a20_done ; success if CF is cleared
a20_done:
Aus dem letzten Artikel zum Wechsel vom Real in den Protected Mode wird jedoch auch klar, dass wir im Protected Mode keinen BIOS-Interrupt mehr ausführen können (mindestens nach derzeitigem Stand, ich vermute aber nie mehr vom Protected Mode). Deshalb müssen wir diese Routinen im Real Mode ausführen. Das ist jedoch kein Problem oder sowieso sinnvoll, da das Problem historisch ja nur durch inkompatible Programme hervorgerufen wurde. Wir schreiben unsere paar Real-Mode-Programme aber mit dem Wissen, dass wir 21 Adressleitungen haben.
Fast A20 Gate
Kommen wir zur nächstschwierigeren Methode, die ebenfalls erst später entwickelt wurde. Diese Möglichkeit wurde nachträglich hinzugefügt, da die originale Methode ziemlich langsam ist und man Wartezyklen einbauen muss.
Mit diesem sogenannten Fast-A20-Gate kann man in wenigen Befehlen ohne Wartezyklen die A20-Line aktivieren. Dazu muss man ein bestimmtes Bit auf dem Port 0x92
setzen. Die Kommunikation mit Ports erfolgt über die Assemblerbefehle in
und out
zum Lesen respektive Schreiben.
Diese verwenden immer das AX-Register (in verschiedenen Größen entweder AL, AX oder EAX) für den Wert auf CPU-Seite und entweder einen immediate-Wert oder das DX-Register für die Portnummer. Wenn wir also gleich den Befehl in al, 92h
schreiben, so heißt das nicht, dass man statt al
auch andere Register verwenden könnte. Hier können nur al, ax oder eax stehen. bx
beispielsweise ist nicht erlaubt.
Die A20-Line wird vom Bit 1 (bei 0 angefangen zu zählen) gesteuert. Ist es auf 1 gesetzt, ist die A20-Line aktiv. Also lesen wir den bisherigen Status des Ports ein, verodern ihn und schreiben ihn wieder raus.
in al, 0x92
or al, 2
out 0x92, al
Soweit in der Theorie. In der Praxis gibt es da leider noch ein paar Probleme. Manche Benutzer berichten, dass ihr Monitor schwarz wurde, als versucht wurde, auf Port 0x92 zu schreiben. Abhilfe schaffte hier die Prüfung, ob A20 bereits aktiviert ist (entweder schon vom BIOS oder durch eine andere Methode).
Außerdem liegt auf Port 0x92 ebenfalls das Reset-Bit, welches manchmal nur als Write-Only implementiert ist. Es ist normalerweise auf 0 gesetzt und führt bei 1 zu einem Reset. Deshalb sollte sichergestellt werden, dass dieses Bit immer mit 0 geschrieben wird.
Die verbesserte Variante prüft deshalb erst, ob A20 aktiviert ist und setzt außerdem das Reset-Bit (Bit 0) immer auf 0. Außerdem fügen wir gleich wieder unsere Sprungmarken ein:
a20_bios:
; [bisheriger BIOS Code, siehe oben]
a20_fast:
in al, 92h
test al, 2
jnz a20_done ; A20 Fast Gate is already activated
or al, 2
and al, 0feh
out 92h, al
; Later we will add a check if this worked
a20_done:
Aufgrund der Probleme auf einigen Rechnern empfiehlt das OSDev-Wiki diese Variante allerdings erst als letzten Versuch in der Reihe aller Methoden, um A20 zu aktivieren.
Die ursprüngliche Methode
Ursprünglich setze man die A20-Line über den Keyboard-Controller. Um mit diesem kommunizieren können, braucht man zwei Ports: Port 0x60
und Port 0x64
. Wichtig ist, dass man hierbei auf Ready-Bits warten muss, bevor man lesen oder schreiben darf.
Diese Bits kann man vom Port 0x64
lesen. Bit 0 ist das Ausgabepuffer-Bit und gibt an, ob der Ausgabepuffer voll ist. D.h. bei einer 1 kann man lesen. Bit 1 ist das Eingabepuffer-Bit und gibt an, ob der Eingabepuffer voll ist. Nur bei leeren Eingabepuffer, d.h. einer 0 darf man schreiben.
Deshalb können wir zunächst mal diese beiden Funktionen programmieren:
a20_wait_inbuf:
in al, 64h
test al, 2
jnz a20_wait_inbuf
ret
a20_wait_outbuf:
in al, 64h,
test al, 1
jz a20_wait_outbuf
ret
Für unsere Kommunikation mit dem Keyboard-Controller müssen wir außerdem ein paar Befehle kennen, die wir an den Port 0x64
schicken. Wichtig für uns sind:
ad
: Disable keyboardae
: Enable keyboardd0
: Read output portd1
: Write output port
Bei den “output port”-Befehlen muss man gedanklich noch einmal unterscheiden zwischen den Ports, die wir aus Assembler heraus ansprechen und den hiergenannten Ports. Denn hierbei handelt es sich um Ports des Keyboard-Controllers, dieser Output Port wird auch als P2 bezeichnet.
Der Ablauf zum Setzen des A20-Bits wird nun folgendermaßen sein:
- Schalte die Tastatur ab
- Teile dem Keyboard-Controller mit, dass wir den Output-Port lesen wollen
- Lies die bereitgestellten Daten
- Teile dem Keyboard-Controller mit, dass wir den Output-Port schreiben wollen
- Schreibe das A20-Bit
- Schalte die Tastatur wieder an
Vor jedem dieser Schritte müssen wir prüfen, ob der Keyboard-Controller auch bereit ist. Für das genauere Verständnis, warum wir worauf warten, ist es hilfreich zu wissen, dass vom Keyboard-Controller drei Register auf diese beiden Ports gemappt werden:
- Schreiben auf 0x60 oder 0x64 greift auf den Eingabepuffer zu
- Lesen von 0x60 greift auf den Ausgabepuffer zu
- Lesen von 0x64 greift auf das Statusregister zu (hierfür braucht man nicht zu warten)
Port 0x64 wird für Befehle verwendet, Port 0x60 für Daten. Letztlich heißt das: Jedes Mal, wenn wir etwas schreiben wollen (egal auf welchen Port), müssen wir prüfen, ob der Eingabepuffer bereit ist. Wenn wir etwas von 0x60 lesen wollen, müssen wir prüfen, ob der Ausgabepuffer bereit ist.
Unsere obige Ablaufreihenfolge können wir mit den richtigen Wartebefehlen in diesen Assemblercode transferieren:
a20_keyboard:
cli
call a20_wait_inbuf ; disable the keyboard
mov al, 0adh
out 64h, al
call a20_wait_inbuf ; tell the controller we want to read data
mov al, 0d0h
out 64h, al
call a20_wait_outbuf ; read the P2 port provided by the controller
in al, 60h
push ax
call a20_wait_inbuf ; tell the controller we want to write data
mov al, 0d1h
out 64h, al
call a20_wait_inbuf ; write the new P2 port with A20 line active
pop ax
or al, 2
out 60h, al
call a20_wait_inbuf ; re-enable the keyboard
mov al, 0aeh
out 64h, al
call a20_wait_inbuf
sti
; later we will add a test if this method worked
Prüfen, ob A20 aktiv ist
Wie man an den bisherigen Code-Kommentaren schon sieht, fehlen an manchen Stellen noch die Überprüfungen, ob die A20-Line wirklich aktiviert wurde. Außerdem wird empfohlen, dies direkt vor dem Einsatz irgendeiner der obigen Methoden zu prüfen.
Die Idee zum Prüfen von A20 ist es, einmal von einer Adresse zu lesen, die nur mit 21 Bits erreichbar ist, und einmal von der äquivalenten Adresse, wenn man nur 20 Adressbits hat. Als untere Adresse verwendet man hier eine, die auf den Bootloader verweist. Dieser wurde schon abgearbeitet, sodass keine Instruktionen im Ablauf mehr überschrieben werden können.
Der Schutz für den oberen Bereich ist meiner aktuellen Einschätzung nach, dass man den Bereich über 1MB einfach nicht verwenden darf. In der Theorie könnte man sich meines Erachtens durchaus kommende Instruktionen überschreiben. In der Praxis muss man darauf achten, dass der Real-Mode-Teil des Kernels im unteren Speicherbereich liegt und nicht über der 1MB-Grenze. Der spätere Kernel kann durchaus höher liegen, wir setzen den Speicher wieder in den richtigen Zustand zurück. Lediglich die paar Instruktionen zwischen Setzen des Speichers und Rücksetzen des Speichers dürfen eben nicht genau im Bereich des geschriebenen Speichers liegen.
Der Code ist aus dem OSDev-Wiki übernommen mit ein paar Anpassungen und Anmerkungen von mir. Die Funktion gibt im ax
-Register 1 zurück, wenn A20 aktiv ist. Ansonsten 0.
a20_active:
pushf
push ds
push es
push di
push si
cli
xor ax, ax ; ax = 0
mov es, ax
not ax; ax = 0xffff
mov ds, ax
mov di, 0500h
mov si, 0510h
mov al, byte [es:di] ; save the old values from memory
push ax
mov al, byte [ds:si]
push ax
mov byte [es:di], 00h ; write 0x00 to one and 0xff to the other location
mov byte [ds:si], 0ffh
cmp byte [es:di], 0ffh ; check if the address we set to 0x00 was
; set to 0xff later, then we have only 20 bit
; addresses
pop ax ; restore the bytes we set before
mov byte [ds:si], al
pop ax
mov byte [es:di], al
mov ax, 0
je a20_active_end
mov ax, 1
a20_active_end:
sti
pop si
pop di
pop es
pop ds
popf
ret
Wir benötigen nun noch Code, der diese Funktion überprüft und ggfs. zur richtigen Stelle springt. Und solchen, der dies in einer Schleife tut, wie das OSDev-Wiki empfiehlt, da manche Methoden evtl. einige Zeit brauchen.
a20_stop_if_active:
call a20_active
test ax, ax ; check if all bits are 0, then ZF = 1
jnz a20_done
ret
a20_stop_if_active_loop: ; try in a loop if a20 is active for k times
mov bx, 0ffh
a20_stop_if_active_loop_iterator:
dec bx
call a20_stop_if_active
test ax, ax
jnz a20_done
test bx, bx ; check if bx 0
jnz a20_stop_if_active_loop_iterator
ret
Alles zusammen
Packen wir das alles zusammen in eine Datei namens a20.asm
sieht das Monstrum so aus:
a20_enable:
call a20_stop_if_active
a20_bios:
mov ax, 2401h
int 15h
call a20_stop_if_active
a20_keyboard:
call a20_wait_inbuf ; disable the keyboard
mov al, 0adh
out 64h, al
call a20_wait_inbuf ; tell the controller we want to read data
mov al, 0d0h
out 64h, al
call a20_wait_outbuf ; read the P2 port provided by the controller
in al, 60h
push ax
call a20_wait_inbuf ; tell the controller we want to write data
mov al, 0d1h
out 64h, al
call a20_wait_inbuf ; write the new P2 port with A20 line active
pop ax
or al, 2
out 60h, al
call a20_wait_inbuf ; re-enable the keyboard
mov al, 0aeh
out 64h, al
call a20_wait_inbuf
call a20_stop_if_active_loop
a20_fast:
in al, 92h
test al, 2
jnz a20_done ; A20 Fast Gate is already activated
or al, 2
and al, 0feh
out 92h, al
call a20_stop_if_active_loop
jmp a20_done ; give up, no other methods there
; could jump to another location and display an error
a20_wait_inbuf:
in al, 64h
test al, 2
jnz a20_wait_inbuf
ret
a20_wait_outbuf:
in al, 64h,
test al, 1
jz a20_wait_outbuf
ret
a20_active:
pushf
push ds
push es
push di
push si
xor ax, ax ; ax = 0
mov es, ax
not ax; ax = 0xffff
mov ds, ax
mov di, 0500h
mov si, 0510h
mov al, byte [es:di] ; save the old values from memory
push ax
mov al, byte [ds:si]
push ax
mov byte [es:di], 00h ; write 0x00 to one and 0xff to the other location
mov byte [ds:si], 0ffh
cmp byte [es:di], 0ffh ; check if the address we set to 0x00 was
; set to 0xff later, then we have only 20 bit
; addresses
pop ax ; restore the bytes we set before
mov byte [ds:si], al
pop ax
mov byte [es:di], al
mov ax, 0
je a20_active_end
mov ax, 1
a20_active_end:
pop si
pop di
pop es
pop ds
popf
ret
a20_stop_if_active:
call a20_active
test ax, ax ; check if all bits are 0, then ZF = 1
jnz a20_done
ret
a20_stop_if_active_loop: ; try in a loop if a20 is active for k times
mov bx, 0ffh
a20_stop_if_active_loop_iterator:
dec bx
call a20_stop_if_active
test ax, ax
jnz a20_done
test bx, bx ; check if bx 0
jnz a20_stop_if_active_loop_iterator
ret
a20_done:
Auf Stackoverflow habe ich außerdem den Tipp erhalten, die Interrupts gleich am Anfang zu deaktivieren, bis ich mich wirklich im Protected Mode befinde, da man sonst leicht einige Stellen vergisst. Deshalb habe ich alle cli
- und sti
-Befehle aus dem A20-Code entfernt und deaktiviere Interrupts einmal zu beginn in kernel.asm
. Gleich danach inkludiere ich den neuen A20-Code und dann betreten wir wie im letzten Artikel den Protected Mode:
cli
%include "a20.asm"
enter_pmode:
lgdt [gdtr]
mov eax, cr0
or eax, 1
mov cr0, eax
jmp 08h:far_jump
Oder als gesamte Datei kernel.asm
:
[BITS 16]
[ORG 0x8000]
xor ax, ax ; set ax to 0 = first segment
mov ss, ax
mov ds, ax
mov es, ax
cli
%include "a20.asm"
enter_pmode:
lgdt [gdtr]
mov eax, cr0
or eax, 1
mov cr0, eax
jmp 08h:far_jump
[BITS 32]
far_jump:
mov ax, 10h
mov ds, ax
mov ss, ax
mov esp, 090000h
mov byte [ds:0B8000h], 'P'
mov byte [ds:0B8001h], 1Bh
hang:
jmp hang
gdt_null: ; the first element is the so called null descriptor
dd 0 ; it is not referenced by the processor
dd 0
gdt_code: ; usually we want at least one segment descriptor for code
dw 0ffffh ; segment length bits 0-15
dw 0 ; segment base byte 0,1
db 0 ; segment base byte 2
db 10011010b ; access rights
db 11001111b ; bit 7-4: 4 flag bits: granularity, default operation
; size bit, 2 bits available for OS
; bit 3-0: segment length bits 16-19
db 0 ; segment base byte 3
gdt_data: ; usually we want at least one segment descriptor for data
dw 0ffffh
dw 0
db 0
db 10010010b
db 11001111b
db 0
gdt_end:
gdtr:
dw gdt_end - gdt_null - 1 ; 2 bytes are the size of the GDT in bytes - 1
dd gdt_null ; 4 bytes are the address of the GDT start
Eine Idee wäre es jetzt noch, je nachdem, welche Methode zum A20-Aktivieren gewählt wird, eine entsprechende Ausgabe zu setzen. Dann könnten wir sehen, ob der Code auch funktioniert. Aktuell sehen wir nämlich zumindest auf meinem System einfach nichts. Wir wissen nicht, ob A20 aktiviert werden konnte oder ob es schon aktiv war.
Allerdings sieht man auch, dass der Code langsam unübersichtlicher wird, es wäre also auch überlegenswert, demnächst auf C umzusteigen.
I do not maintain a comments section. If you have any questions or comments regarding my posts, please do not hesitate to send me an e-mail to blog@stefan-koch.name.