Tworzenie systemu operacyjnego – część 0×03: Przejście do trybu chronionego

Potrzebna wiedza

W końcu nadszedł dzień, w którym nasz system operacyjny będzie działał w trybie chronionym :) Do zrozumienia odcinka 0x03 potrzebna będzie wiedza na temat używania sprzętu poprzez porty, linii A20, segmentacji i jej realizacji w x86 czyli GDT oraz tego, czym jest sam tryb chroniony. Po zdobyciu tych nietrywialnych informacji, możemy przejść do sedna.

Skok

W poprzednim odcinku ładowaliśmy zawartość dysku do pamięci. Wykorzystamy teraz tę umiejętność do załadowania kodu przejścia. Musimy zatem zamienić linijkę w której haltowaliśmy procesor na linijkę:

jmp	0x0000:0x7E00	; jump to setup code

która skoczy pod adres, do którego załadowaliśmy zawartość dysku.

Halo? A20?

Teraz, zgodnie z arkanami sztuki powinniśmy odblokować linię A20.., ale zaraz! Może producent sprzętu zrobił to za nas? Poniższy kod sprawdzi czy linia A20 jest gotowa do użycia:

[ORG 0x7E00]			; here is where our code will be loaded
[BITS 16]

check_a20:			; check if A20 is not enabled, it's all about comparing 0xFFFF:0x7E0E with 0xAA55 - for wrapping memory
	mov	ax, 0xFFFF
	mov	es, ax
	mov	di, 0x7E0E
	mov	al, byte[es:di]
	cmp	al, 0x55	; comparing with 0x55 (first part)
	jne	enter_pmode	; if first byte is not wrapping around then second of course too
	; now we have to check if it not happened by chance
	mov	al, 0x69	; therefore we will set first byte and check results
	mov	byte[es:di], al	; load new value
	mov	bl, byte[0x7DFE]	; load old value
	cmp	bl, 0x69
	jne	enter_pmode

Pamiętamy, że ostatnie 2 bajty bootloadera, załadowane do pamięci to zawsze 0xAA55, wiemy też, że jeśli A20 nie jest aktywna, to po granicy 1MiB pamięć zawija się. Wykorzystajmy więc sprytnie ten fakt – jeśli A20 jest nieaktywna, to nasze bajty bootloadera odnajdziemy pod adresem o 1MiB dalej, niż rzeczywiście są. 0xAA55 znajduje się pod adresem 0x7DFE, a więc odpowiednik powinien być pod adresem FFFF:7E0E.

Nasz kod robi proste porównanie. Jeśli pierwszy bajt nie jest równy temu 1 MiB dalej, to znaczy, że A20 jest aktywna (linia 10). Z tego, że jest równy, nie możemy niestety wnioskować, że A20 jest nieaktywna – bo może być to przypadek. Musimy więc pamięć za granicą 1MiB, odpowiadającą bootloaderowi, ustawić na pewną wartość (linia 13), a następnie odczytać zawartość adresu 0x7DFE (linia 14) i sprawdzić, czy są równe (linia 15). Jeśli są równe, musimy spróbować aktywować linię A20. Może wydawać się to skomplikowane, ale tak naprawdę to dosyć prosty trik.

Aktywacja A20

Linię A20 spróbujemy aktywować na 2 sposoby – przerwaniem BIOS-u oraz klasycznie – kontrolerem klawiatury. Oczywiście nie wyczerpuje to szerokiego repertuaru sposobów i w profesjonalnym systemie operacyjnym musielibyśmy postarać się bardziej. Więcej metod tu.

Spójrzmy na kod:

enable_a20:			; let's enable A20 line first, we'll use only basic methods
	; first, try enabling it via BIOS
	mov	ax, 0x2401
	int	0x15
	jnc	enter_pmode

	; then try conventional way - keyboard controller
	call	wait_a20
	mov	al, 0xD1 	; proper value
	out	0x64, al 	; proper port
	call	wait_a20 	; wait
	mov	al, 0xDF	; proper value
	out	0x60, al	; proper port
	call	wait_a20	; wait
	jmp	enter_pmode	; not so safe assumption that we enabled A20 line...


wait_a20:			; waits for keyboard controller to finish
	in	al, 0x64
	test	al, 2
	jnz	wait_a20
	ret

Linijki 20 – 21 to standardowe wywołanie przerwania. Przerwanie to ustawia flagę carry, jeśli się nie powieidzie, stąd kontrola w linii 22. Więcej o tym przerwaniu można przeczytać tutaj.

Druga metoda wymaga o wiele więcej kodu. Funkcja wait_a20 jest funkcją aktywnego oczekiwania na kontroler klawiatury. Musimy ją wywoływać po każdej próbie zapisu. Sama procedura zapisu do kontrolera klawiatury też jest nietrywialna – najpierw musimy wysłać do portu 0x64 wartość 0xD1 (linia 27), a następnie wysłać do portu 0x60 wartość 0xDF (linia 30). Po wykonaniu tych operacji zakładamy, że linia A20 jest aktywna, choć tak naprawdę, nie możemy być tego pewni.

GDT

Przed przejściem do trybu chronionego musimy ustawić tablicę GDT:

gdt:
gdt_null:
	dq	0		; it's just null..
gdt_code:
	dw 	0xFFFF		; limit (4GB)
	dw	0		; base (0)
	db	0		; base (still 0)
	db	10011010b	; [present][privilege level][privilege level][code segment][code segment][conforming][readable][access]
	db	11001111b	; [granularity][32 bit size bit][reserved][no use][limit][limit][limit][limit]
	db 	0		; base again
gdt_data:
   	dw	0xFFFF		; it's just same as above
   	dw	0		; it's just same as above
	db	0		; it's just same as above
	db	10010010b	; [present][privilege level][privilege level][data segment][data segment][expand direction][writeable][access]
	db 	11001111b	; it's just same as above
	db 	0		; it's just same as above
gdt_end:

gdt_desc:
   	dw 	gdt_end - gdt	; it's size
   	dd	gdt		; and location

Tablica zawiera 3 deskryptory. Pierwszy z nich to null descriptor, który jest zawsze wymagany (niektóre emulatory, np. BOCHS narzekają na jego brak). Drugi deskryptor to deskryptor kodu, który rozciąga się na całą 4GiB przestrzeń adresową. Ostatni deskryptor, to deskryptor danych, który również rozciąga się na całe 4GiB przestrzeni adresowej. W ten sposób otrzymujemy płaski model pamięci, w którym nie widać segmentacji. Ustawień poszczególnych bitów nie będę tłumaczył, gdyż są one opisane w kodzie, a dokładniejsze informacje można znaleźć w moim artykule. Warto zwrócić uwagę na sprytne oznaczenie deskryptorów poprzez etykiety gdt oraz gdt_end, co umożliwia nam eleganckie i poprawne ustawienie rozmiaru (linia 69) oraz adresu (linia 70) w strukturze opisującej GDT.

pmode!!!

Uff, teraz możemy już naprawdę przejść do trybu chronionego.

enter_pmode:
	cli
	lgdt 	[gdt_desc]
	mov 	eax, cr0
	or 	eax, 1
	mov 	cr0, eax
	jmp 	0x8:clear_pipe	; do the far jump, to clear instruction pipe

Aby przejść do trybu chronionego musimy upewnić się, że przerwania są wyłączone (linia 42). Zalecane jest także wyłączenie przerwań niemaskowalnych (NMI), ale nie jest to konieczne. W linii 43 ładujemy strukturę opisującą GDT to rejestru gdtr. Aby aktywować tryb chroniony musimy ustawić najmłodszy bit rejestru CR0 – dzieje się to w liniach 44, 45 oraz 46. Po wykonaniu instrukcji z linii 46 jesteśmy już w trybie chronionym! Pięknie, prawda? :) Niestety, w kolejce instrukcji pozostały śmieci po trybie rzeczywistym. Musimy ją oczyścić robiąc skok długi. Dokonujemy tego w linii 47. Pierwszy człon adresu wskazuje nam, którego deskryptora z GDT chcemy użyć – 0x8 odpowiada pierwszemu indeksowi w tablicy GDT (inne pola selektora zajmują bity 0 – 2, 3 bit, to pierwszy bit indeksu); drugi człon, to po prostu adres w naszym kodzie. Załadowanie CS odpowiednią wartością powoduje faktyczne przejście do trybu chronionego.

No i jesteśmy!

Ostatni fragment kodu to po prostu zatrzymanie się w, mlekiem i miodem płynącej, krainie 32 bitowej:

[BITS 32]
clear_pipe:
	hlt			; in pmode :)
	jmp clear_pipe

Co teraz?

Świat stoi teraz przed nami otworem! Możemy korzystać ze wszystkich dobrodziejstw architektury x86, które opisywałem tutaj. Następnym krokiem powinno być prawdopodobnie ustawienie zarządzania pamięcią i obsługa przerwań, ale wcale nie musimy tego robić. Możemy równie dobrze w tym miejscu napisać kod obliczający kolejne liczby pierwsze i w taki sposób być autorem jedynego systemu operacyjnego na świecie, który takie coś robi ;). Tak więc – do dzieła! Aha, jeśli tu dotarłeś, to należą Ci się gratulacje – większość początkujących osdevowców nie osiąga trybu chronionego.

W następnym odcinku porzucimy trudny i męczący assembler i spróbujemy coś napisać w zgrabnym C!

Pełny kod odcinka można pobrać tak:

git clone git://github.com/luksow/OS.git --branch 0x03

Lub po prostu obejrzeć go tu.

11 przemyśleń nt. „Tworzenie systemu operacyjnego – część 0×03: Przejście do trybu chronionego”

  1. W jaki sposób komunikować się z klawiaturą? Będzie wymagany sterownik? (pytam tylko w temacie ASM)

    1. Oczywiście – będzie wymagany sterownik. W trybie chronionym możesz korzystać z portów… ale lepiej byłoby robić to na przerwaniach. Poszperaj na stronach o osdev, które czasem tu przywołuję – powinieneś tam znaleźć przydatne informacje.

      1. Sporo osób „czyści” segmenty przed przełączeniem w tryb chroniony. Dlaczego u Ciebie tego nie ma?

        mov ax, 0x10
        mov ss, ax
        mov ds, ax
        mov es, ax
        mov fs, ax
        mov gs, ax

        1. Szczerze mówiąc nie spotkałem się z tym wcześniej? Jest to gdzieś opisane w podręcznikach Intela albo podane jakieś uzasadnienie dla takiego działania? Wydawało mi się, że te rejestry ładują się same…

  2. Sądzę, że można to skrócić z:

    check_a20: ; check if A20 is not enabled, it's all about comparing 0xFFFF:0x7E0E with 0xAA55 - for wrapping memory
    mov ax, 0xFFFF
    mov es, ax
    mov di, 0x7E0E
    mov al, byte[es:di]
    cmp al, 0x55 ; comparing with 0x55 (first part)
    jne enter_pmode ; if first byte is not wrapping around then second of course too
    ; now we have to check if it not happened by chance
    mov al, 0x69 ; therefore we will set first byte and check results
    mov byte[es:di], al ; load new value
    mov bl, byte[0x7DFE] ; load old value
    cmp bl, 0x69
    jne enter_pmode

    Na:

    check_a20:
    mov ax, 0xFFFF
    mov es, ax
    mov di, 0x7E0E

    mov al, 0x69
    mov byte[es:di], al
    mov bl, byte[0x7DFE]
    cmp bl, 0x69
    jne enter_pmode

    Krótszy i wynik będzie taki sam.

    1. O, pardon :wink: można się pozbyć jeszcze jednej linii.

      check_a20:
      mov ax, 0xFFFF
      mov es, ax
      mov di, 0x7E0E

      mov byte[es:di], al
      mov bl, byte[0x7DFE]
      cmp bl, 0xFF
      jne enter_pmode

      1. Racja, zaproponowana przez Ciebie poprawka powinna działać :). Moja wersja jest trochę bardziej informatywna co do algorytmu.

        1. A co jeżeli pod adresem 0x7DFE z jakiegos powodu było już 0xFF (0x69)? W oryginalnym kodzie właśnie tą przypadkowość chcemy wykluczyć.

Możliwość komentowania jest wyłączona.