Tworzenie systemu operacyjnego – część 0×01: Włączam komputer i…, bootloader

Power on!

W życiu każdego młodego informatyka nastaje moment, gdy zaczyna się zastanawiać, co tak właściwie się dzieje, gdy włącza komputer. Na ten temat można by mówić długo i na różnym poziomie szczegółowości. Ja nie będę zajmował się jednak skaczącymi elektronami i zmieniającymi się poziomami energetycznymi w krzemie, a tym co interesuje programistę systemów operacyjnych – poziomem oprogramowania.

Włączenie komputera rozpoczyna się od naciśnięcia przycisku (genialne spostrzeżenie, prawda?), który włącza zasilanie komputera, a konkretniej – zasila płytę główną. Ta z kolei, w pierwszej chwili uruchamia swój własny firmware. Ciężko określić jak dokładnie proces ten przebiega, gdyż dla każdej płyty głównej może być to trochę inne. Tak naprawdę chodzi o to, że płyta główna potrzebuje programu, który uruchomi procesor. Jeśli w tym momencie coś pójdzie nie tak, będziemy oglądać ciemny ekran, słyszeć kręcące się wiatraczki i piski z głośniczka na płycie głównej. Załóżmy jednak, że udało się uruchomić procesor. Co ciekawsi zastanawiają się pewnie – „a co jeśli komputer posiada dwa lub więcej procesorów?”. Otóż w systemach wieloprocesorowych jeden z procesorów jest tzw. bootstrap processor (BSP), który odpowiedzialny jest za uruchomienie BIOS-u oraz zainicjowanie jądra. Drugi z procesorów pozostaje w stanie zatrzymania (halt), aż do momentu kiedy explicite zostanie obudzony przez jądro systemu operacyjnego. Wracając jednak do naszej sekwencji – nasz świeżo uruchomiony procesor znajduje się w dobrze zdefiniowanym stanie – tj. znane są zawartości części jego rejestrów, w szczególności rejestru wskaźnika instrukcji CS:IP. Rejestr ten przechowuje magiczny adres nazwany reset vector. W tym momencie płyta główna musi się upewnić, że instrukcja znajdująca się pod tym adresem, to skok do miejsca w pamięci, w którym znajduje się zmapowany BIOS – jest to obszar adresów nieco poniżej magicznej granicy 1MB (np. 0x9FC00 – 0xFFFFF). Warto zwrócić uwagę na słowo zmapowany – tzn. dokładnie w tym miejscu w pamięci RAM znajdują się śmiecie, a płyta główna tworzy iluzję, jakoby znajdował się tam BIOS (wie jak to zrobić dzięki przechowywanej przez nią mapy pamięci). Od momentu gdy zacznie wykonywać się BIOS, ciężko powiedzieć co dzieje się dalej, gdyż to zależy od konkretnego BIOS-u – a więc od producenta płyty głównej. Standardowo jest to wykonanie „Power-on self-test”, czyli tzw. POST-u, który testuje – integralność samego BIOS-u, niektóre podzespoły, pamięć itp. Kolejnym krokiem po POST jest przejście do sekwencji bootowania. Pierwszym krokiem bootowania jest przejrzenie (predefiniowanej przez użytkownika w ustawieniach BIOS-u) listy urządzeń do wystartowania i odszukaniu urządzenia (np. dysku), którego pierwszy (a w zasadzie zerowy) sektor, czyli 512 bajtów, kończy się bajtami 0x55 oraz 0xAA. Bajty te oznaczają, iż urządzenie jest bootowalne – zawiera bootloader. Następnie, wspomniane 512 bajtów jest kopiowane pod adres 0x7C00, a procesor zaczyna wykonywać instrukcje spod tegoż adresu. Warto wspomnieć, że istnieje pewien standard, jak owe 512 bajtów powinno wyglądać – tzn. powinien być tam MBR. Nie jest to jednak istotne, gdyż w tym momencie pełną władzę przejmujemy my – programiści! :)

Przed dalszą lekturą upewnij się, że zapoznałeś się informacjami zawartymi w:

QEMU – podstawy

Tryby operacyjne – tryb rzeczywisty

Bootloader

Pierwsza wersja naszego bootloadera będzie robiła.. nic :) a dokładniej nasz bootloader „zatrzyma” procesor. W kolejnej części nieco go rozbudujemy. W tym miejscu trzeba zaznaczyć, że komputer pracuje w trybie rzeczywistym. Spójrzmy na kod naszego bootloadera:

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

bootloader:
	cli			; turn off maskable interrupts, we don't need them now
	hlt			; halt CPU
	jmp	bootloader	; this should not happen, but.. ;)

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

Teraz będą objaśnienia. Pierwsza linia to wskazówka dla naszego kompilatora, żeby przyjął odpowiednie przesunięcie w adresach. Gdyby tej linii nie było (co możecie sprawdzić) kompilator „myślałby”,  że kod wskazywany przez etykietę bootloader znajduje się pod adresem 0x0000, a co za tym idzie skok z linii 7 by się nie powiódł.

Druga linia, to kolejna wskazówka, mówiąca tym razem, że chcemy kod wynikowy 16 bitowy – pamiętacie o trybie rzeczywistym?

Linie 5, 6 i 7 należy rozpatrywać razem. Instrukcja cli wyłącza maskowalne przerwania. O tym co to dokładniej znaczy będzie innym razem, ważne żeby zapamiętać iż przerwania wybudzają procesor, ze stanu halt, aktywowanego poleceniem hlt. W stanie halt procesor zatrzymuje się i nie wykonuje następującej instrukcji, aż do wznowienia. Tak więc brak instrukcji cli spowodowałby chwilowe wstrzymanie procesora, a następnie (gdyż przerwań w tle dzieje się dużo!) wykonanie instrukcji z linii 7, która jest swoistą „ostatnią deską ratunku” :) Polecam poeksperymentować usuwając linie 5 i 7 i zobaczyć dokąd zabrnie nasz IP i czy nie skończy się to restartem.

Linia 9 to makro NASM-a mówiące, aby wypełnić następujące bajty, aż do 510, zerami.

Linia 10 to wspomniany indykator świadczący o tym, iż jest to kod bootloadera.

W ten sposób otrzymujemy 512 bajtów bootloadera.

Nasz kod kompilujemy poleceniem:

nasm boot.asm -f bin -o ./bin/boot.bin

Przełącznik -f bin mówi, iż chcemy, aby wynikowy kod był tzw. płaską binarką (ang. flat binary), czyli ma nie zawierać żadnych nagłówków, ani informacji pomocniczych, a całość kodu źródłowego jest bezpośrednio zamieniana na kod. Możemy to sprawdzić używając narzędzia hexdump – zobaczymy, iż pierwszym bajtem wynikowego pliku boot.bin jest 0xFA, które jest kodem operacji cli :)

Pozostaje nam teraz uruchomienie naszego bootloadera za pomocą polecenia:

qemu -hda ./bin/boot.bin

I voila! Nasz bootloader działa i nic nie robi :) Będąc w QEMU polecam pobawić się trybem monitora, w szczególności wyświetlić fragmenty pamięci.

Końcowe słowa

To tyle w dzisiejszym odcinku. W następnym postaramy się rozbudować nasz bootloader w sposób, który pozwoli załadować nam nasz przyszły kernel. W repozytorium znajduje się pełen kod wraz z plikiem Makefile (niezbyt pięknym, swoją drogą), który pozwala na automatyzację kompilacji i uruchamiania kodu.

Aby ściągnąć kod części 0x01 wykonaj:

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

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

3 przemyślenia nt. „Tworzenie systemu operacyjnego – część 0×01: Włączam komputer i…, bootloader”

    1. Zobacz Makefile – tam wszystkie kompilatory są wyszczególnione. Generalnie to nasm, GCC i kompiluje to na hoście, nie na gościu :)

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