frostbyte
Hobbystyczny kernel systemu operacyjnego na x86_64, w C i asemblerze. Bootuje przez Multiboot2, przechodzi w tryb 64-bit z własnymi tablicami stron, ma preempcyjny scheduler i uruchamia programy w ring 3 - realnie startuje w QEMU.
Jądro systemu operacyjnego napisane od zera - od bootloadera aż do uruchamiania własnych programów w trybie użytkownika. Startuje naprawdę, w QEMU.
Wprowadzenie
frostbyte to hobbystyczny kernel na x86_64, napisany w C i asemblerze. Celem nie było napisanie "systemu operacyjnego" w potocznym sensie, tylko przejście całej drogi, która zwykle jest ukryta: od momentu, w którym firmware oddaje sterowanie, aż do wykonania własnego programu w trybie użytkownika, odizolowanym od jądra.
To jeden z najbardziej bezlitosnych rodzajów projektu, jakie można sobie postawić. Nie ma tu bibliotek, które cokolwiek za ciebie załatwią, ani warstwy, która złapie błąd. Jest goły procesor, dokumentacja architektury i ty. Właśnie dlatego jest tak dobrą szkołą.
W zwykłym programie błąd jest lokalny: leci wyjątek, dostajesz stack trace, poprawiasz linijkę. W jądrze nie ma żadnej z tych wygód. Jeden zły bit w tablicy stron i maszyna po prostu się resetuje - bez komunikatu, bez logu, bez niczego. Debugowanie zamienia się w czytanie rejestrów i zgadywanie, który z ostatnich trzech ruchów był tym zabójczym.
Ta bezlitosność wymusza inny sposób pracy. Każdy krok trzeba domykać osobno i sprawdzać w izolacji, zanim dołoży się kolejny, bo inaczej nie da się odróżnić, która z dziesięciu zmian wywaliła całą maszynę. Dyscyplina nie jest tu cnotą, tylko warunkiem, żeby w ogóle ruszyć do przodu.
Od firmware do ring 3
Cała droga to sekwencja przejść między coraz wyższymi poziomami kontroli. Firmware ładuje jądro przez Multiboot2 i podaje mapę pamięci. Potem trzeba włączyć tryb 64-bit, co wymaga wcześniej wstępnego stronicowania. Następnie jądro przejmuje pamięć wirtualną na własnych tablicach stron, uruchamia scheduler przełączający zadania przerwaniem zegara, i dopiero na końcu skacze do ring 3.
bootloader ładuje jądro i przekazuje mapę pamięci.
włączenie long mode wymaga wcześniej wstępnego stronicowania.
jądro przejmuje zarządzanie pamięcią wirtualną.
przerwanie zegara przełącza zadania, bez ich zgody.
skok do trybu użytkownika i uruchomienie pierwszego własnego programu.
Droga od zasilania do własnego programu
| Etap | Co się dzieje | Tryb |
|---|---|---|
| Multiboot2 | firmware oddaje sterowanie, jest mapa pamięci | ring 0 |
| Long mode | wejście w tryb 64-bit | ring 0 |
| Tablice stron | jądro przejmuje pamięć wirtualną | ring 0 |
| Scheduler | zegar przełącza zadania bez ich zgody | ring 0 |
| Ring 3 | pierwszy własny program, odcięty od jądra | ring 3 |
Debugowanie, gdy maszyna po prostu znika
Skoro błąd nie zostawia śladu, potrzebny był sposób, żeby jądro mogło cokolwiek powiedzieć, zanim ma sterownik ekranu. Rozwiązanie to QEMU z wyjściem na port szeregowy: jądro pisze logi tekstem na serial, a ten leci wprost do terminala. Dzięki temu widać ostatnią linijkę sprzed resetu, zamiast czarnego ekranu.
W trybie jądra nie ma "prawie działa". Błąd w mapowaniu pamięci nie wywala funkcji - wywala całą maszynę. Dlatego każdy krok testowaliśmy osobno w emulatorze, zanim doszedł kolejny, a flagi no-reboot i no-shutdown pilnują, żeby maszyna zamarła na błędzie, a nie znikła w pętli restartów.
Efekt: system, który naprawdę startuje
Na wyjściu jest kernel, który realnie bootuje w QEMU i dochodzi do wykonania własnego programu w ring 3 - nie zatrzymuje się na wypisaniu "hello", tylko przechodzi całą drogę do trybu użytkownika. To projekt zbudowany po to, żeby zrozumieć komputer aż do samego dna, i doprowadzony do punktu, w którym to zrozumienie jest dowiedzione działaniem, a nie deklaracją.
Więcej projektów
Inne realizacje z tej samej kategorii - zobacz, jak podchodzimy do podobnych wyzwań.
Masz podobny projekt?
Napisz do nas - wycena jest bezpłatna i wraca w godzinę.



