Kod Linuxa jest w znacznej wiekszosci pisany w jezyku C.
Wydawac by sie moglo, ze pisanie systemu operacyjnego w jezyku wysokiego poziomu nie jest najbardziej
optymalnym i wydajnym ze wzgledu na dzialanie systemu pomyslem.
Jest jednak jedna, powazna zaleta takiego rozwiazania - przenosnosc kodu.
Jak powszechnie wiadomo, Linux moze byc skompilowany na wiekszosci platform sprzetowych
bez koniecznosci zaopatrywania sie w osobny kod dla kazdej z platform.
Z drugiej jednak strony, w przypadku systemu operacyjnego potrzebujemy
np. bezposredniego dostepu do zasobow sprzetowych,
a tego, w obliczu zroznicowania platform i procesorow, jezyki wysokiego poziomu nie potrafia obsluzyc.
Z tego wlasnie powodu, niezbedna byla koniecznosc stworzenia czesci kodu Linuxa w jezykach
niskiego poziomu - odpowiednich dla konkretnych procesorow.
Oto jak wyglada polaczenie uniwersalnego kodu w jezyku wysokiego poziomu (w tym przypadku C)
z kodami ograniczonymi na dzialanie w specyficznych srodowiskach (assemblery roznych procesorow)
Czesc pierwsza: GDZIE jest assembler w kodzie Linuxa?
Czerwona czcionka zaznaczono katalogi w ktorych miesci sie kod Linux'a zorientowany na jedna, konkretna,
platforme sprzetowa.
Latwo mozna zauwazyc ze nazwy katalogow sa w prosty sposob powiazane z architerkuta procesora ktorej dotycza.
Ponadto kazdy z katalogow arch/[architektura] zaiwera m.in. podkatalogi:
linux-2.4.18-z-patchami
|__ arch
| |__ [architektura]
| | |__ boot
- za wyjatkiem architektury m86k oraz parisc
. | |__ kernel
. | |__ lib
. |__ mm
. |__ math-emu
- nie ma go tylko w arm, cris, parisc, s390x i sh
a czasem tez odpowiadajace nazwom konkretnym modelom platform sprzetowych, np. dla architektury m68k sa to m.in.:
|__ amiga
|__ atari
|__ hp300
|__ mac
|__ sun
Pliki zrodlowe w powyzej zaznaczonych miejscach maja przewaznie trzy mozliwe rozszerzenia: .c - kod w jezyku C .S - kod w assemblerze, odpowiednim dla danej architektury procesora .h - pliki naglowkowe, odwolujace sie zarowno do kodu w C, jak i kodu w assemblerze
Jak mozna sie przekonac przegladajac zrodla, pliki z rozszerzeniem .S
nie beda wystepowaly tak czesto jak mozna by sie bylo tego spodziewac.
Czasem dalo sie cos zapisac w czystym C,
czasem autorzy ograniczali sie jedynie do wstawek assemblerowskich,
ale chyba najczesciej kod assemblera jest umieszczany w plikach naglowkowych
(szczegolnie jako krotkie funkcje deklarowane inline, lub
makra wstawiajace tylko kilka instrukcji assemblera)
Czesc druga: CO jest kodowane assemblerem?
include/
W assemblerze zostaly zapisane przede wszystkim najmniejsze i najprostrze z funkcji, ktorych dzialanie
jest uzaleznione w jakis sposob od mozliwosci platformy sprzetowej (np. operacje wejscia/wyjscia na portach),
badz ktore nie wymagaly setek assemblerowskich instrukcji kodu, a ktore ze wzgledu na ich implementacje
w jezyku procesora w znaczny sposob zwiekszaja szybkosc dzialania systemu (wystarczy popatrzec na makro
current dla i386)
Funkcje te najczesciej sa pisane jako makra, lub ujmowane dyrektywa inline
i czesto implementowane bezposrednio w plikach naglowkowych
(w katalogu [linux source]/include/asm-[architektura])
include/asm-[architektura]
Ponizej wyszczegolnilem pliki ktore w roznych architekturach powtarzaja sie najczesciej.
Umyslnie pomijalem pozycje ktore sluza tylko jako pliki pomocnicze,
lub ktore nie mialy wiele wspolnego z kodem assemblera.
Te wyszczegolnione zas, dodatkowo zostaly pogrupowane ,,tematycznie''
pliki optymalizujace predkosc:
bitops.h
implementuje podstawowe operacje na bitach, np. set_bit, test_and_clear_bit czy find_first_zero_bit
byteorder.h
definiuje funkcje ujednolicajace kolejnosc bajtow w liczbach wielobajtowych (roznicuje pomiedzy little endian a bid endian) - przydatne do obslugi sieci gdzie jest z gory ustalony porzadek bajtow
checksum.h
implementuje zliczanie sum kontrolnych, glownie a'propo obslugi protokolu IP
xor.h
implementuje instrukcje zwiekszajace predkosc przy wyliczaniu sum kontrolnych dla RAID-5
(wykorzystujac do tego np. technologie MMX w Intelach)
string.h
implementacja makr i czesci funkcji operujacych na string'ach (biblioteka string.h) i pamieci (np. memcpy)
Kilka przykladow:
operacje wspomagajace wspolbieznosc
atomic.h
zawiera operacje niepodzielne, jak np. atomic_add czy atomic_dec_and_test
semaphore.h rwsem.h
definicje struktur semaforow systemowych (odpowiednio: zwyklych i do czytania/pisania) i niektorych operacji na nich
spinlock.h
definicja struktury spinlockow i implementacja niektorych operacji na nich
current.h
definiuje makro get_current
processor.h
implementuje m.in.:
- pobieranie aktualnych wartosci rejestrow czy wskaznika na wykonywana instrukcje (przydatne np. przy przelaczaniu procesow)
- procedury/makra/struktury obslugi watkow (np. thread_struct, start_thread, exit_thread
system.h
zawiera m.in. makra obslugujace obsluge przerwan (np. cli, sti, save_flags) oraz
makra prepare_to_switch i switch_to
organizacja pamieci:
page.h
definiuje stale i makra do obslugi stron pamieci (np. PAGE_SIZE, clear_page)
pgtable.h
definiuje stale i makra do obslugi tablicy stron w pamieci
uaccess.h
("user access") definiuje makra i instrukcje dajace dostep procesom uzytkownika (z poza jadra) do pamieci
(np. put_user, copy_from_user)
inne:
unistd.h
zawiera numery funkcji systemowych, makra wolajace te funkcje (syscallX)
a takze makra kilku instrukcji (m.in.: open, read, write, close, dup, execve, wait)
bugs.h
wykrywa niedociagniecia sprzetowe (np. na Intelu slynna wpadka z bledami przy dzieleniu)
io.h
implementuje obsluge wejscia/wyjscia, ale uwaga: to wejscie/wyjscie portow (w assemblerze Intela cos jak in i out).
arch/[architektura]
Kod umieszczony w katalogu arch/[architektura] rozni sie znacznie od kodu w plikach naglowkowych.
Przede wszystkim assembler nie wystepuje tu az tak czesto.
Poza tym kod pisany assemblerem przewaznie nie jest juz krociutki i prosty, a czesto sa
to dlugie i zmudnie pisane funkcje, ktorych nie sposob byloby zaimplementowac inaczej
(np. funkcja startujaca system, kiedy jadro jeszcze nie zostalo zaladowane do pamieci)
arch/[architektura]/boot
W arch/[architektura]/boot zostal zakodowany loader Linuxa.
Miesci sie tu procedura uruchamiajaca proces init.
W zaleznosci od sprzetu, moze byc ona wpisana scisle w pierwszy sektor dysku,
moze musiec rozpakowac dalsza czesc kodu,
zaladowac ja do wczesniej przygotowanego ramdisku lub do konkretnego miejsca w pamieci,
lub tez przetestowac mozliwosci zainstalowanego sprzetu
(w architekturze Intela sa np. sprawdzane podstawowe mozliwosci karty graficznej)
arch/[architektura]/kernel
W arch/[architektura]/kernel miesci sie kod podstawowych funkcji wykorzystywanych przez jadro,
jak np. obsluga semaforow systemowych czy przelaczania procesora pomiedzy trybami jadra i uzytkownika.
W znacznej mierze uniknieto tu jednak dosc pracochlonnego i nieczytelnego zapisu w jezyku assemblera.
Assembler wystepuje wiec zawsze tylko:
w pliku entry.S
implementuje przewaznie:
procedury wolania i wyjscia z funkcji systemowych i niskopoziomowych
w pliku head.S
implementuje rozpoznanie specyficznych funkcji aktualnego modelu procesora
(jak np. obsluge nowszego modelu pamieci,
czy chociazby operacji zmiennoprzecinkowych na poziomie 486 lub technologii MMX w procesorach Intela)
oraz czasami w innych pomocniczych plikach, zaleznie od architektury,
ktore jednak czesto implementuja wyzej wymieniona funkcjonalnosc
zamiast bezposrednio w plikach entry.S, head.S,
lub nawet zamiast w plikach naglowkowych z include/asm/
arch/[architektura]/lib
Ciezko jednoznacznie okreslic specyfike implementacji zawartych w arch/[architektura]/lib.
Sa tu przerozne funkcje, czesto pisane w assemblerze z czysto optymalizacyjnych powodow.
W znacznej wiekszosci nazwy plikow odpowiadaja implementowanym przez nie funkcjom (np. strlen.S)
lub nawet calym bibliotekom (np. string.S)
Wsrod kodow assemblera zawartych w tym podkatalogu, w zaleznosci od architktury, mozemy znalezc m.in. takie
"roznosci" jak:
implementacje operacji na blokach pamieci - memcpy, memcmp, memscan, memset, itp.
oraz operacji na string'ach - strcmp, strcpy, strlen, itp.
implementacje operacji na bitach, np. changebit, clearbit, setbit
implementacje operacji niepodzielnych, np. atomicadd (architektury sparc i sparc64)
implementacje operacji na semaforach i spinlockach (np. dla architektury m68k i sparc64)
arch/[architektura]/mm zawiera implementacje jednostki MMU.
W znacznej wiekszosci jest ona pisana w C.
Z assembler'a korzysta sie jedynie w architekturach arm, ppc, sh, sparc i sparc64
a i tam tylko w nielicznych przypadkach (np. przy implementacji kopiowania i czyszczenia calych stron pamieci)
arch/[architektura]/math_emu
Do zaimplementowanych tu rozwiazan Linux odwoluje sie w przypadku gdy maszyna nie jest w stanie wykonac
pewnego minimalnego dla Linuxa zestawu operacji arytmetycznych (np. przy architekturze Intela
Linux potrafi symulowac na procesorze 386 instrukcje procesora 486, jak wyliczanie pierwiastka lub sinusa)
Znaczna wiekszosc zawartego tu kodu jest jednak pisana w C. Z assemblera korzysta sie okreslajac jedynie
niektore z podstawowych funkcji, ktore specyfika danego procesora moze nieco uproscic
(np. na Intelu mozna wykorzystac fakt ze mnozac 32-bitowe rejestry dostajemy wynik 64-bitowy - EDX:EAX)
Czesc trzecia: JAK jest kompilowany kod dla odpowiedniej platformy sprzetowej?
Skoro istnieje wiele implementacji tych samych funkcji,
ktore na dodatek roznia sie jezykiem w ktorym zostaly napisane (kazdy assembler jest inny),
mozna sie zastanawiac jak podczas kompilacji wybrac ta odpowiednia implementacje.
W Linuxie rozwiazano to tworzac link symbiliczny [linux source]]/include/asm wskazujacy
na katalog [linux source]]/include/asm-[architektura] w ktorym
znajduja sie pliki naglowkowe charakterystyczne dla danej architektury.
W ten sposob, w zrodlach nie zwiazanych z konkretna platforma sprzetowa, dyrektywa #include
moze sie odwolywac do plikow naglowkowych z katalogu asm/, a dzialajacy system nakieruje
kompilator na odpowiednia czesc kodu.
W zrodlach funkcjonuje rowniez definicja __[architektura]__ (np. __i386__)
i w ten sposob na pomoca dyrektywy #ifdef (np. #ifdef __i386__) mozna odgrodzic fragment
kodu przeznaczony na ten procesor od fragmentu przeznaczonego na inna maszyne. W szczegolnosci
zas w "odgrodzonych" fragmentach kodu mozna zamiescic konkretnego assemblera.
Wykorzystane materialy
Korzystalem ze zrodel Linux'a w wersji 2.4.18 dostepnych w ramach labolatorium