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 keyboard
  • ae: Enable keyboard
  • d0: Read output port
  • d1: 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:

  1. Schalte die Tastatur ab
  2. Teile dem Keyboard-Controller mit, dass wir den Output-Port lesen wollen
  3. Lies die bereitgestellten Daten
  4. Teile dem Keyboard-Controller mit, dass wir den Output-Port schreiben wollen
  5. Schreibe das A20-Bit
  6. 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.

Den kompletten Code zum Stand nach diesem Artikel gibts auch bei Github (Commit 4e464a4dab52b685b9b90bd12507eb85209e00bc).