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.
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 ?
Jaki błąd? Wklej tutaj czy coś :)
Przepraszam nie doczytałem do końca (facepalm). W każdym razie muszę powiedzieć, że Pana kurs jest bardzo dobry ;).
Dzięki za miłe słowa… Mam nadzieję, że uda mi się go pociągnąć dalej.
Jeszcze jedno pytanie :P
Czy jak nagrywam bootloadera na płytkę, to nagrać go jako obraz, czy jako dysk El-Torino?
Szczerze powiedziawszy nie wiem, gdyż nie studiowałem nigdy sposobu bootowania z CD. Sprawdź na http://wiki.osdev.org/Expanded_Main_Page
Bo raz mi wypisało „Disk I/O error”. Stąd moje pytanie.