Multiboot Specification
Multiboot Specification jest próbą stworzenia ustandaryzowanego sposobu bootowania systemów operacyjnych. Chodzi o to, aby każdy bootloader zgodny z Multiboot Specification był w stanie bootować każdy system operacyjny również zgodny z Multiboot Specification. Ma to ułatwić tworzenie środowisk z wieloma systemami operacyjnymi.
Multiboot Specification nie definiuje tego, jak ma być napisany bootloader, a jedynie odpowiedni interfejs. Referencyjną implementacją Multiboot Specifciation jest GNU GRUB. Wiele systemów operacyjnych (np. Linux), bootloaderów i maszyn wirtualnych (np. QEMU) jest zgodnych z tą specyfikacją, więc nie jest ona tylko teoretycznym dokumentem. Niezwykle istotną cechą bootloaderów zgodnych z Multiboot Specification jest to, że są one w stanie bootować kernel skompilowany do popularnych formatów plików wykonywalnych np. ELF. Dzięki temu możemy korzystać ze wszelkich dobrodziejstw, które formaty oferują.
O konkretnych cechach Multiboot Specification wspomnę przy okazji kodu, poniżej.
Multiboot Specification mam i ja!
W swoim projekcie systemu operacyjnego postanowiłem porzucić własny bootloader na rzecz bootloadera zgodnego z Multiboot Specification. Może się to wydawać dziwne, bo napisanie całego kodu związanego z bootowaniem zajęło mi sporo czasu, ale tak naprawdę ma to głębokie uzasadnienie. Przede wszystkim, mój bootloader jest bardzo ubogi i prosty – nie potrafi bootować żadnych formatów plików wykonywalnych, a jedynie płaskie binarki, co jest niezwykle uciążliwe, gdyż muszę polegać na magicznych stałych. Ponadto nie dostarcza on żadnych informacji systemowi operacyjnemu – wszystko trzeba zrobić samodzielnie. Oczywiście, można napisać ten kod samodzielnie… Jednak uważam, że nie jest on wystarczająco pasjonujący, aby się nim zajmować :). Dzięki Multiboot Specification będziemy mogli przestać zajmować się szczegółami, a przejść do rzeczy :).
Kod – czyli co my musimy zrobić dla Multiboot Specification, a co on zrobi dla nas?
Na chwilę musimy powrócić do assemblera, aby dostarczyć kilku informacji wymaganych przez bootloader i od razu możemy skoczyć do kodu w C.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | global loader ; set visible to linker extern main ; main from main.c ; some useful macro values FLAGS equ 0 ; this is the multiboot 'flag' field MAGIC equ 0x1BADB002 ; 'magic number' lets bootloader find the header CHECKSUM equ -(MAGIC + FLAGS); checksum required STACKSIZE equ 0x4000 ; 16 KiB for stack section .text align 4 ; setting multiboot header multiboot_header: dd MAGIC dd FLAGS dd CHECKSUM loader: mov esp, stack + STACKSIZE ; set up the stack push eax ; pass multiboot magic number as second parameter push ebx ; pass multiboot info structure as first parameter call main ; call C code section .bss align 4 stack: resb STACKSIZE ; reserve stack space |
Najbardziej istotne są makra z linii 5-7. Pierwsze z nich określa flagi, które informują bootloader, czego od niego oczekujemy. Wśród możliwych opcji jest: wyrównanie modułów do rozmiaru strony, dołączenie mapy pamięci oraz dostępnych trybów video. Po więcej informacji zapraszam tu. Póki co, nie potrzebujemy niczego, stąd wartość 0. Drugie to wartość magiczna, która pozwala bootloaderowi zidentyfikować nagłówek. Liczba 0x1BADB002 jest urocza, prawda? :) Trzecia wartość, to suma kontrolna, która powinna mieć wartość, taką że dodana do pól: FLAGS i MAGIC daje zero.
Kolejne linijki są oczywiste. W sekcji kodu musimy zamieścić kolejno wartość magiczną, flagi oraz sumę kontrolną, co dzieje się w liniach 14-16. Etykieta loader to rzeczywisty punkt wejściowy naszego jądra. W wierszu 19 ustalamy początek stosu, na którego miejsce rezerwujemy w sekcji bss (linia 28). Istotne jest, aby miejsce rezerwowane było w sekcji bss, bo inaczej rezerwacja będzie polegała na stworzeniu dużego pliku z wieloma zerami, a przecież nie o to nam chodzi. Etykieta bss załatwia sprawę – kompilator „wie”, że tylko rezerwujemy przestrzeń w odpowiednim miejscu w pamięci. Następnie w liniach 20 i 21 odkładamy zawartość dwóch rejestrów na stos (istotna jest kolejność – jeśli masz wątpliwości czemu pierwsza instrukcja odpowiada drugiem parametrowi, spójrz do konwencji wołania) tak, aby przekazać je do funkcji main, którą wołamy w linii 23. Znaczenie przekazanych parametrów omówię poniżej.
Spójrzmy teraz na kod jądra w C:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | char hello[] = "Hello from kernel!"; int main(void* mbd, unsigned int magic) { int count = 0; int i = 0; unsigned char *videoram = (unsigned char *) 0xB8000; /* 0xB0000 for monochrome monitors */ if ( magic != 0x2BADB002 ) { /* something went wrong.. */ while(1); /* .. so hang! :) */ } /* clear screen */ for(i=0; i<16000; ++i) { videoram[count++] = 'A'; videoram[count++] = 0x00; /* print black 'A' on black background */ } /* print string */ i = 0; count = 0; while(hello[i] != '\0') { videoram[count++] = hello[i++]; videoram[count++] = 0x07; /* grey letters on black background */ } while(1); /* just spin */ return 0; } |
Widzimy, że tym razem do funkcji main przekazane są dwa parametry, które włożyliśmy na stos. Pierwszy z nich to struktura informacyjna dostarczona przez bootloader, która zawiera informacje zarządane za pomocą flag. Druga, to kolejna wartość magiczna, infomująca nas o tym, czy wszystko poszło prawidłowo. Tym razem jest to 0x2BADB002. W liniach 41-45 widzimy bardzo prymitywną :) obsługę sytuacji błędnej. Reszta kodu jest taka, jak w odcinku poprzednim.
Co tak właściwie się stało? Specyfikacja Multiboot zapewnia nam że:
- rejestr EAX będzie zawierał magiczną wartość 0x2BADB002, jeśli jądro zostało prawidłowo załadowane (stąd wkładanie rejestru EAX na stos w pierwszym przedstawionym kodzie)
- rejestr EBX będzie zawierał adres struktury informacyjnej z danymi zażądanymi we fladze (stąd wkładanie rejestru EBX na stos w pierwszym przedstawionym kodzie)
- linia A20 będzie aktywowana
- rejestry segmentowe będą ustawione tak, aby realizowany był płaski model pamięci
- będzie aktywowany tryb chroniony procesora
- bit 17 i 9 rejestru EFLAGS będzie zgaszony
Jak widać, Multiboot Specification zapewnia nam wszystko, co poprzedni bootloader oraz sporo więcej. Więcej na temat struktur i gwarantowanego stanu tutaj.
Skrypt linkera pozostaje prawie bez zmian. Istotne jest, że tym razem musimy zdefiniować punkt wejściowy naszego programu (jądra), czyli etykietę loader oraz to, że możemy zażądać, aby nasz kernel został załadowany daleeeeeko za granicą 1MiB! Ja ładuję go zaraz za tą granicą:
ENTRY (loader)
SECTIONS {
. = 0x00100000;
.text : {
*(.text)
}
.rodata ALIGN (0x1000) : {
*(.rodata)
}
.data ALIGN (0x1000) : {
*(.data)
}
.bss : {
*(.bss)
}
}Uważny czytelnik zauważy ustawienie wyrównań (polecenia ALIGN), które jednak aktualnie nie mają dużego znaczenia, więc nie będę ich omawiał.
Pozostała nam kompilacja do formatu elf w wariancie dla architektury i386 (x86):
nasm -f elf -o ./bin/loader.o loader.asm gcc -o ./bin/main.o -c main.c -m32 -nostdlib -nostartfiles -nodefaultlibs ld -melf_i386 -T linker.ld -o ./bin/kernel.bin ./bin/loader.o ./bin/main.o
Oraz uruchamianie:
qemu -kernel ./bin/kernel.bin
Użycie QEMU może być zdziwieniem, gdyż nie używamy tu żadnego bootloadera typu GRUB. QEMU jednak, tak jak wspominałem, posiada wbudowany bootloader zgodny z Multiboot Specification. Włącza się go za pomocą przełącznika -kernel. Jeśli ktoś jednak bardzo chce może użyć GRUBa.
To tyle, nasz stuningowany kernel powinien działać.
NIH
Jestem zwolennikiem unikania syndromu NIH, stąd decyzja o użyciu dobrze napisanego i przetestowanego bootloadera zgodnego z Multiboot Specification. Stan naszej wiedzy nie ucierpiał na tej decyzji, gdyż mamy już za sobą napisanie prostego bootloadera :). W przyszłości będzie można do niego wrócić i wzbogacić go o ładowanie plików w formacie ELF i jeszcze kilka ficzerów. Na razie jednak, zajmijmy się tym co najważniejsze, czyli jądrem naszego systemu operacyjnego.
Tradycyjnie, pełen kod odcinka można pobrać tak:
git clone git://github.com/luksow/OS.git --branch 0x05
Lub obejrzeć go tu.

masz ort w wyrazie „zażądzanymi”
poza tym bardzo ciekawa seria
pozdrawiam
Dzięki wielkie :).
Poprawiłem – a podobno wciskanie „Sprawdź ortografię” nie boli.
Pozdrawiam!