Tworzenie systemu operacyjnego – część 0×02: Dobrodziejstwa BIOS-u

512 bajtów (tak samo jak 640K) nie wystarczy każdemu

W poprzednim odcinku załadowaliśmy 512 bajtów zawartości dysku do pamięci. W taki sposób uruchomiliśmy nasz bootloader, który niestety nic nie robił. Teraz wypadałoby, aby zmienił on tryb operacyjny, wyświetlił kilka diagnostycznych komunikatów no i załadował jądro za pomocą sterownika ATA… tylko to wszystko nie zmieści się w 512 bajtach! Jeśli postawimy na minimum, tj. załadowanie jądra z dysku – również może być ciężko, sterownik jednak swoje waży. Z pomocą przychodzi nam jednak BIOS!

Funkcje BIOS-a – przerwania

BIOS udostępnia nam bardzo wiele funkcji poprzez przerwania. Wśród oferowanego asortymentu są np.: pisanie na ekran, wczytywanie z dysku, sprawdzanie stanu sprzętu, uzyskiwanie mapy pamięci, aktywowanie dodatkowej pamięci i wiele, wiele innych. My zajmiemy się dla przykładu tylko dwoma. Pełna ich lista znajduje się tutaj i tutaj. Widzicie funkcje opisane jako „???”? Uważajcie na nie! Skoro nikt, nawet twórcy BIOS-ów nie wiedzą do czego służą, to możecie nimi przez przypadek przejąć władzę nad światem lub wyłączyć prąd u sąsiada w domu.. :)

Dla niezaznajomionych z programowaniem systemowym: przerwanie wywołuje się poleceniem int numer_przerwania, natomiast argumenty przekazuje się za pomocą odpowiednich rejestrów.

int 0x10

Na początek postaramy się wypisać coś na ekran. W tym celu skorzystamy z przerwania 0x10, którego dokładny opis jest tutaj. Oto kod:

	mov	ax, 0x0000
	mov	ds, ax		; round way to do that...
	mov	si, msg

	mov	ah, 0x0E	; mode - teletype (advance and scroll)
	mov	bh, 0x00	; page number
	mov	bl, 0x07	; colors

.next_char:
	lodsb
	or	al, al		; letter here
	jz	.continue
	int	0x10		; BIOS video interrupt
	jmp	.next_char

msg:
	db	'Hello from bootloader!', 13, 10, 0

Idąc od początku, warto zwrócić uwagę na linie 5 i 6 i na dziwną, okrężną drogę ładowania rejestru DS (data segment). Niestety – takie są ograniczenia architektury – nie można bezpośrednio ładować rejestrów segmentowych. Do rejestru SI ładujemy adres pamięci pierwszej litery (czyli mówiąc najprościej – napisu) i tak uzyskana para (DS:SI) wskazuje nam jednoznacznie, gdzie jest napis. Pora na kolejne ustawienia. W rejestrze AH, zgodnie z opisem musi znaleźć się wartość 0xE, która definiuje tryb wyświetlania liter – jest to przewijanie i przeskakiwanie na następną pozycję (w dokumentacji nazwany teletype). W rejestrze BH przechowujemy numer strony – u nas jest to strona zerowa. Rejestr BL zawiera natomiast kolory (tła i liter) – białe litery na czarnym tle.

Możemy teraz przejść do pętli wypisującej literę po literze. Instrukcja lodsb ładuje literę wskazywaną przez DS:SI do rejestru AL, a następnie przesuwa wskaźnik do następnej litery. Polecenie o tyle przydatne,  gdyż oszczędzamy w ten sposób cenne miejsce w naszych 512 bajtach. Kolejnym krokiem jest sprawdzenie, czy przypadkiem nie dotarliśmy do końca napisu i jeśli tak, to opuszczenie pętli (linie 15 i 16), następnie wywołanie samego przerwania (linia 17) oraz skok zamykający pętle. Bajecznie proste prawda? Po wykonaniu tego kawałka kodu powinniśmy ujrzeć na ekranie napis „Hello from bootloader!”. Czas na trochę bardziej przydatne i skomplikowane przerwanie.

int 0x13

Szczęśliwa „trzynastka” ;) pozwala nam na wczytywanie danych z dysku za pomocą adresowania typu LBA (jeśli nie wiesz co to, przeczytaj koniecznie podlinkowaną stronę Wikipedii). Rzut oka na kod:

	mov	si, read_pocket
	mov	ah, 0x42	; extension
	mov	dl, 0x80	; drive number (0x80 should be drive #0)
	int	0x13

read_pocket:
	db	0x10		; size of pocket
	db	0		; const 0
	dw	1		; number of sectors to transfer
	dw	0x7E00, 0x0000	; address to write
	dd	1		; LBA
	dd	0		; upper LBA

Tym razem w rejestrze SI znajduje się adres „kieszeni” (o której szerzej – zaraz) zawierającej informacje o tym co i gdzie wczytać. Rejestr AH zawiera numer identyfikujący tę właśnie funkcję (wśród dostępnych funkcji znajdują się również takie, które pozwalają na dostęp CHS). Natomiast rejestr DL zawiera numer dysku z którego chcemy czytać… zupełnie spodziewanie, dysk numer 0 znajduje się pod numerem 0x80 ;> Przerwanie wywołujemy w linii 24 i o ile wszystko poszło gładko, powinniśmy mieć dane załadowane pod wskazany adres. Co do samej kieszeni, to komentarze chyba mówią wszystko. Pierwszy bajt zawiera rozmiar kieszeni, drugim powinno być zawsze 0, kolejne dwa to liczba sektorów do wczytania, następne dwa słowa to człony adresu (u nas to 0x0000:0x7E00), potem następuje adres sektora startowego na dysku (zaczynamy od sektora numer 1, czyli efektywnie od drugiego) i jego „górna” część. Prosto, łatwo i przyjemnie! :) W prawdziwym, profesjonalnym systemie powinniśmy jakoś obsłużyć błędy wczytywania, których kody zwracane są w odpowiednich rejestrach (opis tu).

Wszystko do kupy

Finalnie nasz kod wygląda tak:

[ORG 0x7C00]			; here is where our code will be loaded by BIOS
[BITS 16]

bootloader:
	mov	ax, 0x0000
	mov	ds, ax		; round way to do that...
	mov	si, msg

	mov	ah, 0x0E	; mode -> teletype (advance and scroll)
	mov	bh, 0x00	; page number
	mov	bl, 0x07	; colors

.next_char:
	lodsb
	or	al, al		; letter here
	jz	.continue
	int	0x10		; BIOS video interrupt
	jmp	.next_char

.continue:
	mov	si, read_pocket
	mov	ah, 0x42	; extension
	mov	dl, 0x80	; drive number (0x80 should be drive #0)
	int	0x13
	cli			; turn off maskable interrupts, we don't need them now
	hlt
.halt:	jmp	.halt

msg:
	db	'Hello from bootloader!', 13, 10, 0
read_pocket:
	db	0x10		; size of pocket
	db	0		; const 0
	dw	1		; number of sectors to transfer
	dw	0x7E00, 0x0000	; address to write
	dd	1		; LBA
	dd	0		; upper LBA 

times 510-($-$$) db 0		; fill rest with zeros
dw 0xAA55			; bootloader indicator, used by BIOS

Do celów testowych musimy stworzyć dalszą część naszego wirtualnego dysku, jako że ma on tylko 512 bajtów i wczytanie kolejnego sektora zaowocuje błędem. Proponuję zrobić to tak:

echo 'Ala ma kota' > ./bin/text
dd if=/dev/zero of=./bin/zeros bs=1  count=500
cat ./bin/boot.bin ./bin/text ./bin/zeros > ./bin/hda.img

Kolejno: tworzę plik z zawartością „Ala ma kota” (plus nowa linia!), tworzę plik 500 bajtowy z samymi zerami, a następnie wszystko łącze w kolejności: kod + tekst + zera, co nam daje 1024 bajty.

Po uruchomieniu naszego bootloadera w QEMU zobaczymy na ekranie „Hello from bootloader!”, natomiast w pamięci pod adresem 0x7E00 powinien być napis „Ala ma kota”. Dla pewności, w trybie monitora sprawdźmy to komendą xp /12bc 0x7E00. Wspaniale!

Single stage

Nasz bootloader aktualnie posiada tylko jeden etap (ang. stage) działania. Bardziej złożone bootloadery, jak np. GNU GRUB posiadają kilka etapów działania (najczęściej 2) ze względu na wspomniane ograniczenie 512 bajtów. W kolejnej części cyklu spróbujemy napisać stage 2 dla naszego bootloadera, który odpowiednio skonfiguruje środowisko pracy dla naszego jądra. Tymczasem zachęcam do zapoznania się z pełnymi źródłami części 0x02:

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

Jeśli chcesz go tylko przejrzeć, wejdź tutaj.

7 przemyśleń nt. „Tworzenie systemu operacyjnego – część 0×02: Dobrodziejstwa BIOS-u”

  1. Takie pytanko.
    Próbowałem zrobić coś z tym bootloaderem. Wyświetla błąd na linijce 16 przy jz .continue. Czym to może być spowodowane ?

  2. Przepraszam nie doczytałem do końca (facepalm). W każdym razie muszę powiedzieć, że Pana kurs jest bardzo dobry ;).

  3. Jeszcze jedno pytanie :P
    Czy jak nagrywam bootloadera na płytkę, to nagrać go jako obraz, czy jako dysk El-Torino?

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