Asembler w kodzie linuxa | ||
---|---|---|
<<< Wstecz | Przykłady | Dalej >>> |
Asembler znajduje zastosowanie przy pisaniu oprogramowania odpowiedzialnego za ładowanie systemu operacyjnego do pamięci komputera (bootowanie) i przygotowywanie środowiska przed uruchomieniem samego jądra. W tym rozdziale zostaną omówione cztery pliki wchodzące w skład źródeł jądra 2.4.18, zawierające kod wykonujący wyżej wymienione zadania:
arch/i386/boot/bootsect.S
arch/i386/boot/setup.S
arch/i386/boot/compressed/head.S
arch/i386/kernel/head.S
W przypadku plików bootsect.S i setup.S ciekawostką jest to, że zostały one napisane w asemblerze 16-bitowym, z czym raczej nie spotkamy się w żadnym innym miejscu w źródłach Linuxa. Pozostałe dwa pliki są już napisane w asemblerze dla procesorów 32-bitowych.
Na początku zajmiemy się standardowym boot loaderem Linuxa, czyli programem bootsect. Źródła tego programu znajdują się w pliku bootsect.S. Jak to jest wspomniane powyżej jest on napisany w asemblerze 16-bitowym, z 20-bitowym adresowaniem pamięci w trybie rzeczywistym (a więc wszystkie adresy w kodzie można traktować jako adresy fizyczne). Do dyspozycji jest, jak z tego wynika, tylko 1MB pamięci. Adresy zapisuje się w postaci SEGMENT:OFFSET, czyli na przykład liniowy adres 0x90200 może być zapisany jako 0x9000:0x0200. Adresy segmentów, w których bootsect umieszcza podczas swojego działania poszczególne dane lub części kodu, zdefiniowane są tam jako stałe w pliku include/asm-i386/boot.h.
Program bootsect jest używany do linuxa ładowania z dyskietki. W przypadku ładowania z dysku zwykle posługujemu się bardziej wyrafinowanymi boot loaderami, jak LILO czy GRUB. Jak wiadomo, jądro może być zapisane na dyskietce startowej w formie spakowanego obrazu. Wersja o której tu mowa radzi sobie tylko z obrazami jądra o rozmiarze do 508kB, czyli takimi jakie zapisuje się w pliku zImage, nie radzi sobie natomiast z dużym obrazem jądra (bzImage); to samo dotyczy części setup. Dla dużego jądra przeznaczone są wersje, których pliki źródłowe mają nazwy poprzedzone dodatkową literką "b" - bbootsect.S, bsetup.S. Różnica jest dość znaczna, w szczególności kod z pliku bootsect.S nie musi adresować pamięci powyżej 1MB - ten z pliku bbootsect.S - musi.
Skompilowany bootsect jest zapisywany na samym początku pliku zImage i umieszczany w pierwszym sektorze dyskietki startowej w czasie jej tworzenia. Każdy boot loader w architekturze Intelowskiej musi zajmować dokładnie jeden 512 - bajtowy sektor. Na końcu tego sektora musi się dodatkowo znaleźć słowo 0x55AA, dzięki któremu BIOS potrafi stwierdzić, że dany sektor jest boot sectorem. BIOS ładuje pod adres 0x7C00 zawartość pierwszego napotkanego boot sectora (czyli w naszym przypadku program bootsect), potem przekazuje do załadowanego kodu sterowanie. Bootsect po przejęciu sterowania wykonuje między innymi następujące czynności:
przepisuje siebie w pamięci pod adres 0x90000; (żródła)
ustawia stos (co sprowadza się do przekazania do rejestrów SS:SP odpowiedniej wartości - konkretnie 0x9000:0x4000-12); (żródła)
określa ilość sektorów na ścieżce, żeby zczytywać dane od razu całymi ścieżkami; co ciekawe, robi to metodą prób i błędów, to znaczy najpierw próbuje zczytać 36 sektorów z jednej ścieżki, jeśli się nie uda, to próbuje zczytać 18 sektorów (co odpowiada zwykłej dyskietce 1,44MB), potem 15 i w końcu 9; (żródła)
wyświetla na ekranie komunikat, że ładuje system; (żródła)
ładuje z dyskietki kod setup pod adres 0x90200 (czyli "tuż za siebie"), oraz resztę obrazu jądra pod adres liniowy 0x10000; jak widać, w tym przypadku jądro musi się zmieścić w 512kB (od 0x10000 do 0x90000, gdzie zaczyna się już kod bootsecta); (żródła)
wykonuje pewne czynności, które mają na celu zagwarantowanie zachowania znanego stanu, na przykład wyłącza silnik napędu dyskietki; (żródła)
przekazuje sterowanie do setup. (żródła)
Podczas tych wszystkich czynności do dyspozycji są tylko przerwania BIOSu (a używane są tylko dwa: 0x10 - do wypisywania na ekranie komunikatów i 0x13 - do obsługi dysku).
Duży obraz jądra (bzImage) jest ładowany pod adres liniowy 0x100000, czyli boot loader musi mieć możliwość adresowania pamięci powyżej 1MB.
Setup po skompilowaniu jest umieszczany w pliku z obrazem jądra zaraz za bootsectem; również do pamięci operacyjnej jest ładowany zaraz za nim - pod adres liniowy 0x90200 (inne boot loadery same mogą zajmować inne miejsca w pamięci, ale setup i obraz jądra są zawsze umieszczane pod tymi samymi adresami bezwzględnymi). Odpowiada on za przejęcie z BIOSu wszystkich dostępnych informacji na temat sprzętu i umieszczenie ich w miejscu niepotrzebnego już bootsecta, czyli w zakresie adresów 0x90000 - 0x901FF. Jest to znów realizowane przy użyciu przerwań BIOSu. Potem przełącza procesor w tryb chroniony i przechodzi do wykonania startup_32.
Kolejno, setup wykonuje następujące czynności:
sprawdza typ i wersję loadera oraz poprawność załadowania;
sprawdza rozmiar dostępnej pamięci RAM, co odbywa się kolejno na trzy różne sposoby, przy czym każdy z tych sposobów sprowadza się do wywołania przerwania BIOSu 0x15 z odpowiednią wartością rejestru AX:
AX = 0xE820 - umieszcza w pamięci mapę pamięci, zawierającą do 32 wpisów (adres, wielkość, typ) odpowiadających 32 spójnym obszarom pamięci z różnymi prawami dostępu; (źródła)
AX = 0xE801 - wypisuje tylko wielkość dostępnej pamięci; (źródła)
AX = 0x88 - sprawdza wielkość spójnego fragmentu dostępnej pamięci powyżej 1MB; ta funkcja zwraca jednak maksymalnie, w zależności od wersji BIOSu, tylko 16 do 64MB; (źródła)
ustawia parametry pracy klawiatury; (źródła)
rozpoznaje kartę graficzną i dokonuje wyboru trybu wyświetlania (w nowszych wersjach kod odpowiedzialny za tą część został wydzielony i znajduje się w pliku arch/i386/boot/video.S);
sprawdza ile dysków jest podłączonych do pierwszego kontrolera i pobiera ich parametry; (źródła)
sprawdza obecność MCA (Machine Check Architecture); (źródła)
sprawdza czy jest podłączona mysz PS/2; (źródła)
sprawdza czy jest wsparcie BIOSu dla APM (Advanced Power Management); (źródła)
Potem następują czynności związane z przejściem do trybu chronionego:
w przypadku, gdy obraz jądra był załadowany pod adres 0x10000 (zwykły setup, mały obraz), przeładowuje go pod 0x1000 (duży obraz jądra pozostaje pod adresem 0x100000); (źródła)
włącza linię adresową A20 (jeśli nie była włączona, w przypadku dużego obrazu jądra musiała być), umożliwiając adresowanie pamięci powyżej 1MB - jest to operacja ściśle związana z historią architektury intelowskiej, jej początki sięgają przejścia od architektury 8086 do 80286, a podczas jej wykonywania wykorzystuje się... kontroler klawiatury; jest to jednak niezbędne do przejścia w tryb chroniony; (źródła)
ładuje 0,0 do rejestru IDTR (Interruption Descriptor Table Register), co oznacza, że tablica deskryptorów przerwań (IDT) jest pod adresem 0x0 i ma wielkość 0; w ten sposób wstępnie inicjalizuje IDT; podobnie postępuje w przypadku globalnej tablicy deskryptorów (GDT): wylicza adres jej początku i wielkość, i ładuje tą parę do rejestru GDTR; w tej początkowej wersji tablicy GDT przestrzeń adresowa jądra obejmuje całe 4GB; (źródła) tablice deskrytporów są zdefiniowane na końcu pliku; (źródła)
resetuje koprocesor (źródła)
maskuje wszystkie przerwania, z wyjątkiem drugiego (IRQ2); (źródła)
Teraz następuje samo przejście do trybu chronionego, co sprowadza się do ustawienia bitu PE w słowie stanu procesora:
movw $1, %ax # protected mode (PE) bit lmsw %ax # This is it! |
W końcu jest wywoływana funkcja startup_32 (z pliku arch/i386/boot/compressed/head.S w przypadku spakowanego jądra!).
Kod asemblerowy funkcji startup_32, o której mowa w tym akapicie, jest umieszczony w pliku arch/i386/boot/compressed/head.S. Skompilowany kod wykonywalny znajduje się (po załadowaniu przez boot loadera) pod adresem liniowym 0x1000 w przypadku zImage, lub pod 0x100000 w przypadku bzImage.
Funkcja o identycznej nazwie jest zdefiniowana w arch/i386/kernel/head.S. Ta druga została skrótowo omówiona poniżej. Zbieżność nazw nie służy wyłącznie zmyleniu osób próbujących zagłębić się w temat bootowania Linuxa. Funkcja z pliku arch/i386/boot/compressed/head.S jest wykonywana w przypadku, gdy mamy do czynienia ze spakowanym obrazem jądra. Przygotowuje ona warunki do rozpakowania jądra i wywołuje tą operację. Następnie jądro zostaje rozpakowane, a pod adresem 0x100000 (czyli w przypadku bzImage w tym samym miejscu pamięci, gdzie była "stara" funkcja startup_32) pojawia się "nowa" funkcja startup_32 i zostaje wywołana. Jeśli jądro nie było spakowane, to pod 0x100000 mamy od razu właśnie tą "nową" funkcję startup_32 i tylko ona zostaje wywołana.
Startup_32 wykonuje między innymi następujące czynności:
wypełnia zerami obszar niezainicjalizowanych danych jądra. (źródła)
wywołuje dekompresję jądra; na ekranie wyświetla się napis "Uncompressing Linux..."; jeśli mamy do czynienia z małym obrazem, to rozpakowany kod jest umieszczany w pamięci począwszy od adres fizycznego 0x100000, jeśli natomiast rozpakowujemy duże jądro, to kod jest umieszczany dalej w pamięci; (źródła)
jeśli mamy do czynienia z dużym jądrem, to zostaje ono przesunięte w pamięci tak, aby zaczynało się w 0x100000; (źródła)
w końcu wykonuje skok do 0x100000 (czyli do "właściwej" funkcji startup_32).
Startup_32 z pliku arch/i386/kernel/head.S przygotowuje środowisko do uruchomienia funkcji start_kernel (init/main.c), napisanej w C i kończącej bootstrap. Pewne czynności wykonane w funkcji setup trzeba powtórzyć, żeby wszystko działało zgodnie z oczekwaniami przy 32-bitowym adresowaniu, trybie chronionym i ze wszystkimi innymi dobrodziejstwami architektur nowszej daty. Startup_32 między innymi:
ustawia katalogi i tablice stron; (źródła)
włącza stronicowanie pamięci przez ustawienie bitu PG w słowie stanu procesora; (źródła)
ustawia wszystkie deskryptory w IDT na funkcję ignore_int; (źródła) funkcja ta jest zdefiniowana dalej; (źródła)
uaktualnia IDTR i GDTR;
wykonuje skok do start_kernel.
Dopiero w start_kernel w tablicy IDT zostają umieszczone sensowne wartości i przerwania zostają aktywowane. Wykonywane są też dalsze czynności przygotowujące środowisko dla pracy systemu, jednak funkcja start_kernel() jest już napisana w C (kod znajduje się w init/main.c).
LILO (Linux Loader) - jego zasadniczą i podstawową częścią jest program umieszczany w Master Boot Recordzie (pierwszym sektorze) twardego dysku, lub Boot Sectorze partycji (aktywnej), gdzie na 512 bajtach musi koegzystować z tablicą partycji (o maksymalnie czterech pozycjach; partycja również ma w swoim Boot Sectorze własną tablicę partycji), oraz specjanym słowem 0x55AA. Programik ten jest ładowany przez BIOS pod adres 0x7C00, podobnie jak to było w przypadku bootsecta.
Przepisuje on siebie w pamięci pod adres 0x9A000, ustawia stos trybu rzeczywistego i ładuje kolejną część LILO.
Druga część LILO pozwala użytkownikowi wybrać system operacyjny do załadowania (jeśli jest kilka dostępnych). Potem może załadować Boot Sector odpowiedniej partycji (czyli boot loader wybranego systemu innego niż Linux) do pamięci (pod adres 0x7C00) i przekazać mu sterowanie. Może też sama skopiować wybrany obraz jądra Linuxa do pamięci. W obu przypadkach dochodzi w końcu do wywołania funkcji setup.
Ta część LILO jest oczywiście napisana w asemblerze.
<<< Wstecz | Spis treści | Dalej >>> |
Funkcje systemowe | Początek rozdziału | Asembler inaczej |