W Linuksie używana jest składnia asemblerowa AT&T. Przedstawimy ją poprzez porównanie z bardziej znaną składnią opracowaną przez firmę Intel. Wszystko, co jest opisywane poniżej odnosi się do tylko do architektury i386.
MOV CEL, ŹRÓDŁO
Zarówno źrodlo jak i cel mogą być rejestrem, lub komórką pamięci, ale równocześnie tylko jeden z operatorów może być pamięcią. Nie możemy przesyłać danych bezpośrednio na linii pamięć-pamięć, bez trudu natomiast można kopiować wartość jednego rejestru do innego. Warto zaznaczyć, że mov kopiuje, a nie przenosi wartości, w związku z czym wartość "źródło" po wykonaniu instrukcji pozostaje niezmieniona.
Często zdarza się sytuacja, gdy musimy zapamiętać wartości jakiś rejestrów, wykonać pewne operacje, a następnie odtworzyć wartości tych rejestrów. Aby ułatwić czynności z tym związane wykorzystuje się stos.
Do obsługi stosu służą instrukcje:
PUSH REJ PUSH WAR POP REJ
PUSHA POPA
INT numer_przerwania
Numer_przerwania to liczba z zakresu 0-255, musi być podana konkretna wartość, nie można używać rejestrów. Odpowiednie ustawienie rejestrów i wywołanie danego przerwania powoduje wykonanie określonej procedury lub funkcji.
CALL ETYKIETA
CMP X, Y
JMP ETYKIETA
Jwarunek ETYKIETAKilka przykładów:
Instrukcje te nazwane są według pewnych zasad. Pierwszą literę instrukcji stanowi zawsze J (jump if - skok jeśli), później może wystąpić N (not - nie), a na końcu A (above - ponad) lub E (equal - równe) lub B (below - poniżej)
LOOP ETYKIETA
AT&T: %eax Intel: eax
"movb" - kopiuje bajt "movw" - kopiuje słowo (2 bajty) "movl" - kopiuje długie słowo (4 bajty)
... 0: ... jmp 0b ... jmp 1f ... 1: ...
instrukcja żródło, cel
Załadowanie wartości eax do ebx:
AT&T: movl %eax, %ebx Intel: mov ebx, eax
AT&T: movl $0xd00d, %ebx Intel: mov ebx, d00dh
AT&T: przesunięcie(rejestr_bazowy, rejestr_indeksowy, wartość_skalująca) Intel: [rejestr_bazowy + rejestr_indeksowy*wartość_skalująca + przesunięcie]
Niektóre z tych pól są opcjonalne, ale zawsze wymagane jest jedno z przesunięcie i rejestr_bazowy. Gdy nie podamy wartość_skalująca bądź rejestr_indeksowy, to mają one wartość 1.
Kilka przykładów:AT&T: _zmienna Intel: [_zmienna]
AT&T: (%eax) Intel: [eax]
AT&T: _zmienna(%eax) Intel: [eax + _zmienna]
AT&T: _tablica(,%eax,4) Intel: [eax*4 + _tablica]
int $0x80Pełną listę numerów funkcji systemowych można znaleźć pod adresem: www.linuxassembly.org/syscall.html
Asembler wbudowany w (inline assembler) GCC umożliwia używanie asemblera w połączeniu z kodem napisanym w C. Deklaracja wstawki asemblerowej ma następującą postać:
__asm__(lista rozkazów : zmienne wyjściowe : zmienne wejściowe : modyfikowane wartości);
"instrukcja\n\t" ... "instrukcja\n\t" "instrukcja"
a eax b ebx c ecx d edx S esi D edi I stała z przedziału (0, 31) q,r dynamicznie przydzielany rejestr, z tym, że używając 'q' może być przydzielony eax, ebx, ecx lub edx, natomiast 'r' dopuszcza dodatkowo esi i edi g eax, ebx, ecx, edx lub zmienna w pamięci
Zilustrujmy to prostym przykładem, obliczenie sumy trzech składników:
int asm_suma(int składnik1, int składnik2, int składnik3) { int suma; __asm__("addl %%ebx,%%eax\n\t" "addl %%ecx,%%eax" : "=a" (suma) : "a" (składnik1), "b" (składnik2), "c" (składnik3) : "ax","bx", "cx"); return suma; }
W powyższym przykładzie argumenty składnik1, składnik 2 i składnik3 są wczytywane odpowiednio do rejestrów eax, ebx, ecx, które są symbliozowane przez "a", "b", "c". Wynik otrzymany w rejestrze eax ma być przypisany na zmienną suma. "=a" (suma) oznacza suma=a. W modify podano "ax", "bx", "cx", ponieważ rejestry ax, bx, cx ulegają zmianie.
Do zmiennych możemy odwoływać się za pomocą %i. Przy równoczesnym użyciu zmiennych wejściowych i wyjściowych, obowiązuje następująca reguła ich numerowania:
od %0 do %K - to są zmienne wyjściowe od %K+1 do %N - to są zmienne wejściowe.
Jeśli używamy zmiennych wejściowych, wyjściowych lub modyfikujemy rejestry, to nazwy rejestrów na liście rozkazów muszą być poprzedzone dodatkowym znakiem %. Dzieje się tak dlatego, iż w przeciwnym przypadku, gcc rozpoznający %0, %1, itd, napotykając na przykład %edx zinterpretowałby to jako parametr %e, co prowadziłoby do błędu.
Istnieje możliwość użycia rejestru jednocześnie jako wejściowego i wyjściowego. Zilustrujmy to na przykładzie:
#define _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \ type name (type1 arg1, type2 arg2, type3 arg3, type4 arg4) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \ "d" ((long)(arg3)),"S" ((long)(arg4))); \ __syscall_return(type,__res); \ }
Użycie oznaczenia "=a" w zmiennych wyjściowych powoduje, że wartość powrotna funkcji systemowej, która jest przechowywana w %eax, jest wkładana do zmiennej __res. Dzięki użyciu oznaczenia "0" jako pierwszego w sekcji danych wejściowych, numer funkcji systemowej _NR_##name jest wkładany do %eax, skąd pobierany jest przy przerwaniu. W ten sposób %eax służy tutaj zarówno jako rejestr wejściowy i wyjściowy. Nie są do tego wykorzystywane żadne oddzielne rejestry. Zauważmy, że wejściowy _NR_##name jest użyty zanim wyprodukowana jest wartość zwracana.
Do zabezpieczenia kodu zawartego we wstawce asemblerowej przed zmianami (zmiany takie mogą mieć miejsce przy włączonej opcji optymalizowania kodu) służy słowo kluczowe __volatile__, jakiego należy użyć po __asm__.
Po omówieniu składni asemblera, przedstawimy teraz kilka fragmentów kodu jądra wykorzystujących tego asemblera w praktyce. Postaramy się też do każdego fragmentu podać powody, dla których kod nie został napisany w C, tylko w asemblerze.
Kod tego makra wygląda następująco:
1 static inline struct task_struct * get_current(void) 2 { 3 struct task_struct *current; 4 __asm__("andl %%esp,%0; ":"=r" (current) : "" (~8191UL)); 5 return current; 6 } 8 9 #define current get_current()
Makro to służy do szybkiego dostawania się do deskryptora aktualnego procesu. Korzysta ono z tego, że w trakcie wykonywania się jakiegoś procesu w trybie jądra, wskaźnik stosu, który jest trzymany w rejestrze esp, wskazuje na wierzchołek stosu trybu jądra aktulnego procesu.
Zauważmy, że stos trybu jądra i struktura task_struct przechowywane są w 8KB strukturze task_union, przy czym struktura task_struct znajduje się na początku tego 8KB obszaru. Widać więc, że aby dostać się do task_struct aktulanego procesu, wystarczy zamaskować ostatnich 13 bitów w rejestrze esp.
Dokonujemy tego poprzez wykonanie instrukcji andl dla rejestru esp i pierwszego argumentu, którym jest ~8191UL. Liczba 8191 w systemie dwójkowym składa się z 13 jedynek, zatem ~8191UL na trzynastu najmniej znaczących bitach ma zera, a na pozostałych jedynki.
W powyższym przykładzie asembler użyty jest po to, aby jak najbardziej zoptymalizować czas dostępu do deskryptora aktulnego procesu, co jest tym bardziej istotne, że ten fragment kodu wywoływany jest dosyć często. Inną zaletą tego sposobu dostawania sie do deskrytora, jest brak konieczności pamiętania dodatkowego wskaźnika.
Mimo tego, że Linux jest monolitycznym systemem opercyjnym, istnieje kilka "wątków jądra" np: keventd czy kswapd.
Wątki te tworzone są za pomocą funkcji kernel_thread:
0 int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags) 1 { 2 long retval, d0; 3 4 __asm__ __volatile__( 5 "movl %%esp,%%esi\n\t" 6 "int $0x80\n\t" /* Linux/i386 system call */ 7 "cmpl %%esp,%%esi\n\t" /* child or parent? */ 8 "je 1f\n\t" /* parent - jump */ 9 /* Load the argument into eax, and push it. That way, it does 10 * not matter whether the called function is compiled with 11 * -mregparm or not. */ 12 "movl %4,%%eax\n\t" 13 "pushl %%eax\n\t" 14 "call *%5\n\t" /* call fn */ 15 "movl %3,%0\n\t" /* exit */ 16 "int $0x80\n" 17 "1:\t" 18 :"=&a" (retval), "=&S" (d0) 19 :"" (__NR_clone), "i" (__NR_exit), 20 "r" (arg), "r" (fn), 21 "b" (flags | CLONE_VM) 22 : "memory"); 23 return retval; 24 }
W linii 4 powyżego kodu pojawiło się słowo kluczowe __volatile__. Zabezpiecza ono kod zawarty we wstawce assemblerowej przed zmianami. Przy włączonej opcji optymalizowania kodu, kompilator mógłby sprawić, że kod nie pojawiłby się dokładnie w miejscu wyznaczonym przez programistę, a gdy używa się wstawki assemblerowej w C lub C++, to na ogół nie jest dobrze, jeśli kompilator coś poprzestawia.
W linii 5: "movl %%esp,%%esi\n\t", w rejestrze esi zapamiętywana jest wartość z wierzchołka stosu.
Następnie przerwanie w linii 6 powoduje wywołanie funkcji systemowej. Jest to funkcja clone(), ponieważ w rejestrze eax znajduje się _NR_clone. Załadowanie tego numeru do eax odbywa się w linii 15: "" (__NR_clone). Zauważmy też, że w ebx znajdują się argumenty dla tego wywołania czyli CLONE_VM | flags. CLONE_VM oznacza, że rodzic i potomek współdzielą deskryptor pamięci i wszystkie tablice stron.
Po powrocie z funkcji clone(), możemy znajdować się w w rodzicu lub w potomku i żeby to sprawdzić wykorzystamy skopiowaną do esi wartość z wierzchołka stosu sprzed wywołania funkcji systemowej.
W linii 7 wykonywane jest więc porównanie między wartościami w esi i edi. Jeżeli wartości te są równe, to znajdujemy się w rodzicu, a gdy są różne to w potomku.
Jeżeli znajdujemy się w rodzicu, to nic już nie trzeba robić, zatem wykonujemy jest skok w przód do etykiety 1 i działanie wstawki asemblerowej kończy się:
7 "cmpl %%esp,%%esi\n\t" /* child or parent? */ 8 "je 1f\n\t" /* parent - jump */ ... 17 "1:\t" 18 : ...
Ciekawiej jest gdy wracamy z funkcji clone() jako potomek. Ponieważ chcemy aby nasze wątki jądra wykonywały jakąś konkretną pracę, trzeba jeszcze wywołać funkcję fn przekazaną jako parametr wejściowy. Realizowane jest to w liniach 12-14, gdzie na rejestr eax wstawiamy wskaźnik do argumentów funkcji fn, po czym odkładamy go na stos. Jest to związane z tym, że nie wiemy w jaki sposób była kompilowana funkcja fn, którą właśnie w linii 14 wywołujemy.
Po zakończeniu wykonywania funkcji fn, wywoływana jest funkcja exit(). Jest to realizowane analogicznie jak poprzednie wywołanie funkcji systemowej, a mianowicie do rejestru eax ładujemy czwartą zmienną czyli _NR_exit i zgłaszamy przerwanie:
15 "movl %3,%0\n\t" /* exit */ 16 "int $0x80\n"
W linii 18, w sekcji modify podano "memory", co informuje gcc o tym, że nie powinien wprowadzać optymalizacji polegającej na trzymaniu zmiennych w rejestrach, bo zawartość pamięci może ulec zmianie i zrobi się bałagan.
Funkcja kernel_thread została napisana w asemblerze ponieważ w Linuksie wywołanie systemowe musi być zgłoszone poprzez wykonanie instrukcji asemblera:
int $0x80
Instrukcja ta zgłasza przerwanie systemowe o wektorze 128, za pomocą którego są realizowane wywołania systemowe w Linuxie.
#ifdef CONFIG_SMP #define LOCK_PREFIX "lock ; " #else #define LOCK_PREFIX "" #endif
Jak widać w powyższym fragmencie kodu preprocesor zamienia wszystkie wystąpienia LOCK_PREFIX na "lock ;" bądz pusty string.
Zamiana na "lock ;" odbywa się w przypadku architektury SMP, czyli wielu procesorów dzielących miedzy sobą zasoby. Jest to związane z synchronizacją procesorów. Instrukcja lock sprawia, że nastepująca po niej instrukcja (z tym, że nie każda, bo lock działa tylko dla niektórych) jest atomowa, a więc zasoby na których działa są chronione przed modyfikacją przez inne procesory (robione jest to poprzez zablokowanie szyny)..
Poniższe przykłady bedą używały makra LOCK_PREFIX w celu zapewnienia atomowści ich działania. Będą to funkcje wykorzystywane do synchronizacji (sekcje krytyczne, blokady i itp).
Zacznijmy od funkcji set_bit:
1 static __inline__ void set_bit(int nr, volatile void * addr) 2 { 3 __asm__ __volatile__( LOCK_PREFIX 4 "btsl %1,%0" 5 :"=m" (ADDR) 6 :"Ir" (nr)); 7 }
Funkcja ta wykonuje właściwie tylko jedną instrukcję:
btsl %2, %1
Jest to polecenie kopiowania bitu o numerze podanym w pierwszym operandzie z adresu z drugiego operandu do znacznika Carry, a następnie ustawienia bitu w miejscu z którego przed chwilą odbywalo się kopiowanie. Dzięki zastosowaniu LOCK_PREFIX jest to operacja niepodzielna. Przyjrzyjmy się teraz nieznacznie bardziej skomplikowanej funkcji test_and_set_bit:
1 static __inline__ int test_and_set_bit(int nr, volatile void * addr) 2 { 3 int oldbit; 4 5 __asm__ __volatile__( LOCK_PREFIX 6 "btsl %2,%1\n\tsbbl %0,%0" 7 :"=r" (oldbit),"=m" (ADDR) 8 :"Ir" (nr) : "memory"); 9 return oldbit; 10 }
Ta funkcja wykonuje już dwie instrukcje. Pierwsza jest identyczna do tej z funkcji set_bit, druga natomiast jest postaci:
sbbl %0, %0
Jest to polecenie odejmowania z przeniesieniem czyli odejmuje od wartości z %0 sumę wartośći z %0 oraz znacznika Carry, a wynik zapisuje na %0. Tak więc w tym wypadku na %0 zostaje przypisane -1 gdy znacznik Carry był ustawiony na 1, albo 0 gdy był ustawiony na 0. Możliwość wykonania atomowej operacji btsl, w odniesieniu do całej funkcji zapewnia, że inne procesory nie mogą nam zepsuć obliczeń, ponieważ operację sbbl wykonujemy już na własnych rejestrach (%0 w sekcji wejściowej jest przypisywany do któregoś z rejesetrów: eax, ebx, ecx lub edx).
W pliku include/asm/i-386/bitops.h znajdują się definicje funkcji analogicznych do wyżej przedstawionych tzn. clear_bit, test_and_clear_bit, change_bit, test_and_change_bit. Powodem zastosowania asemblera przy ich implementacji jest oprócz wydajności, to że muszą one być atomowe.
Czasami potrzeba jest zagwarantowania, że pewne instrukcje wykonają się niepodzielnie. Dobrym przykladem wykorzystania niepodzielnych instrukcji są semafory.
Na PW czasem zakładaliśmy, że niektóre operacje na semaforach są niepodzielne (albo atomowe) - tzn. nic nie przerwie w środku ich wykonania.
Popatrzmy w kod <asm/semaphore.h>.
Niepodzielność zapisywana jest przy pomocy makr Gdy instrukcja jest prosta:
LOCK "<instrukcja>"
A w przypadku złożonych:
LOCK_SECTION_START(..) <instrukcje> LOCK_SECTION_END
Wracając do semaforów. I tak, np. instrukcja podniesienia semafora (up):
Gdyby operacja zwalniania nie była realizowana atomowo, to mogłoby się zdarzyć (tzn. mógłby się zdarzyć taki przelot), że zwolnimy więcej niż jednego czekającego. My potrzebujemy zwolnić jednego i tylko jednego - czyli między zwolnieniem czekającego a opuszczeniem ponownym semafora nie może się zdarzyć nic innego związanego z semaforem.
Jak jest zapewniana atomowość? Trzeba zobaczyć <asm/atomic.h>, który definiuje operacje atomowe. Widać jak zdefiniowane jest makro LOCK jako dodanie prefiksu lock dodawanego do instrukcji, który informuje proces, że na czas realizacji instrukcji ma zamknąć szynę. (więcej o prefiksach można znaleźć tutaj). Po takim zdefiniowaniu makra LOCK, pozostaje tylko zdefiniować operacje takie operacje atomowe jak:
Oczywiście można by nie definiować funkcji atomic_set, ale są one wygodnym skrótem dla pisania wywołania makra LOCK "addl %1,%0" czy podobnych... (poza taką instrukcją jeszcze następuje obsługa parametru i wyniku).
Czasami stosuje się asembler, kiedy wiadomo, że pewne instrukcje będą wykonywane bardzo częste i dlatego zależy nam na szybkości ich wykonania.
Dobrym przykładem jest tutaj plik kopiowanie bloków pamięci. (np. wykorzystywana w trzecim programie zaliczeniowym - moduł - funkcja copy_from_user czy copy_to_user). Można oczywiście ręcznie kopiować przy pomocji pętli
for (i=start, i<koniec; i++) pamiecUZYT[offU+i]=pamiecJADRA[offK+i].
Ale funkcje napisane w asemblerze (takie jak copy_to_user czy memcpy) zrobią to znacznie szybciej, dzięki bezpośredniemu dostępowi do sprzętu.
I tak np. wywołanie funkcji memcpy, w architekturze wspierającej MMX,
jest tłumaczone na specjalną instrukcję MMX, która kopiuje blok
pamięci.
patrz: <asm/string.h> (linie 199, 290)
oraz: <asm/mmx.h>.
Teraz porównajmy działanie dwóch programów.
p1.c:
#define rozmiar 10000000 typedef int tablica; int main() { char *pam, *pam2; int i; pam=(char *)malloc(rozmiar); pam2=(char *)malloc(rozmiar); for (i=0; i<rozmiar; i++) pam[i]=pam2[i]; free(pam); free(pam2); return 0; }
p2.c:
#define rozmiar 10000000 typedef int tablica; int main() { char *pam, *pam2; int i; pam=(char *)malloc(rozmiar); pam2=(char *)malloc(rozmiar); memcpy(pam2,pam,rozmiar); free(pam); free(pam2); return 0; }
Co porównując (u mnie na komputerze) daje takie rezultaty:
[mccartney@130-moc-6 proba]$ time ./p1 0.06user 0.05system 0:00.12elapsed 88%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (74major+4895minor)pagefaults 0swaps [mccartney@130-moc-6 proba]$ time ./p2 0.02user 0.02system 0:00.04elapsed 95%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (75major+4895minor)pagefaults 0swaps
W procesorach x86 istnieje sprzętowy mechanizm przełączania
kontekstu. Nie jest on jednak wykorzystywany, jako że sprawia
problemy przy przełączaniu się do innego zadania, gdy zapisany
stan zadania nie jest poprawny (np. nieaktualna jest zawartość
rejestrów segmentowych). Dlatego w linuksie przełączanie zadań
jest realizowane programowo. Za przełączanie kontekstu odpowiedzialna
jest funkcja switch_to (tak naprawdę wywołuje ona tylko funkcję
__switch_to - i to tą drugą funkcją się zajmiemy). Oto
diagram tej funkcji:
Funkcja ta
wygląda jak napisana w czystym c, lecz w rzeczywistości korzysta
z wielu makr asemblerowych. Co ciekawe, owe makra przekładają
się na kod w dziwny sposób. Dobrym przykładem jest tu makro
loadsegment. W źródłach zajmuje 15 linii kodu, z których
większośc jest niezrozumiała i nie pojawia się w żaden sposób
w kodzie po skompilowaniu:
84 #define loadsegment(seg,value) \ 85 asm volatile("\n" \ 86 "1:\t" \ 87 "movl %0,%%" #seg "\n" \ 88 "2:\n" \ 89 ".section .fixup,\"ax\"\n" \ 90 "3:\t" \ 91 "pushl $0\n\t" \ 92 "popl %%" #seg "\n\t" \ 93 "jmp 2b\n" \ 94 ".previous\n" \ 95 ".section __ex_table,\"a\"\n\t" \ 96 ".align 4\n\t" \ 97 ".long 1b,3b\n" \ 98 ".previous" \ 99 : :"m" (*(unsigned int *)&(value)))
W skompilowanym kodzie widoczne jest tylko przypisanie z linii 87, czyli:
mov fs, [ebx+0Ch] mov gs, [ebx+10h] ; wywolanie makra loadsegment po raz drugi.
Instrukcje z linii 91 i 92 maja ten sam efekt, co instrukcja z linii 87, to znaczy do danego rejestru wprowadzą żądaną wartość. To że znikają po skompilowaniu jest bardzo dziwne, bo teoretycznie kompilator nie ma prawa ich usunąć (dyrektywa volatile). Zwróćmy jeszcze uwagę na deklarację parametru tego makra: (*(unsigned int *)&(value))). Jest to wskaznik do elementu value, zrzutowany na wskaźnik do inta bez znaku, a następnie wzięta wartość tegoż inta. Jest to troche niezrozumiałe, bo przecież można zrzutować value bezpośrednio na unsigned int. Ponadto rzutowanie nie jest konieczne, bo argumenty tego makra są typu unsgined int. Być może jest to jakieś obejście buga w gcc, a być może wymuszenie pewnego sposobu optymalizacji - nie wiadomo.
Kod odpowiedzialny za wywoływanie funkcji systemowej znajduje się
w pliku arch/i386/kernel/entry.S. Kod ten jest funkcją obsługi
przerwania 0x80, czyli własnie wywołania funkcji systemowej.
Ze względu na użycie w funkcji kilku makr, oraz żeby pokazać
ciekawą technikę obchodzenia się z kodem przedstawię teraz
rozwiniętą (bez makr) wersję kodu, którą uzyskałem disasemblując
skompilowany kernel, dodatkowo rozrysowany jako
schemat blokowy,
po spojrzeniu na który można łatwo zorientować się, co tak naprawdę
robi nasza funkcja:
Po pierwsze, zapamiętujemy na stosie stan rejestrów. Po drugie, do ebx ładujemy wskaźnik do task_structa zadania. Następnie, sprawdzamy pole [ebx+18], czyli task_struct->ptrace. Jeśli pole to jest ustawione, to przed wywołaniem żądanej funkcji wołamy funkcję systemową trace. Oczywiście po drodze jest dokonywane sprawdzenie, czy żądana funkcja systemowa istnieje (cmp eax, 100). Następnie blokujemy przerwania sprzętowe, i jeśli trzeba (flaga task_struct->need_resched ustawiona) to wołamy schedulera. Jeśli nie mamy do obsłużenia żadnego sygnału, to kończymy procedure obsługi przerwania - zdjemujemy co nasze ze stosu, iret i do domu.
Gdy na obsługę czeka jakiś sygnał (flaga task_struct->sigpending jest ustawiona) to przedy zakończeniem pracy wykonujemy jeszcze procedure obsługi tegoż sygnału. Dla porównania - kod źródłowy:
87 #define SAVE_ALL \ 88 cld; \ 89 pushl %es; \ 90 pushl %ds; \ 91 pushl %eax; \ 92 pushl %ebp; \ 93 pushl %edi; \ 94 pushl %esi; \ 95 pushl %edx; \ 96 pushl %ecx; \ 97 pushl %ebx; \ 98 movl $(__KERNEL_DS),%edx; \ 99 movl %edx,%ds; \ 100 movl %edx,%es; 101 102 #define RESTORE_ALL \ 103 popl %ebx; \ 104 popl %ecx; \ 105 popl %edx; \ 106 popl %esi; \ 107 popl %edi; \ 108 popl %ebp; \ 109 popl %eax; \ 110 1: popl %ds; \ 111 2: popl %es; \ 112 addl $4,%esp; \ 113 3: iret; \ ... ... 202 ENTRY(system_call) 203 pushl %eax # save orig_eax 204 SAVE_ALL 205 GET_CURRENT(%ebx) 206 testb $0x02,tsk_ptrace(%ebx) # PT_TRACESYS 207 jne tracesys 208 cmpl $(NR_syscalls),%eax 209 jae badsys 210 call *SYMBOL_NAME(sys_call_table)(,%eax,4) 211 movl %eax,EAX(%esp) # save the return value 212 ENTRY(ret_from_sys_call) 213 cli # need_resched and signals atomic test 214 cmpl $0,need_resched(%ebx) 215 jne reschedule 216 cmpl $0,sigpending(%ebx) 217 jne signal_return 218 restore_all: 219 RESTORE_ALL 220 221 ALIGN 222 signal_return: 223 sti # we can get here from an interrupt handler 224 testl $(VM_MASK),EFLAGS(%esp) 225 movl %esp,%eax 226 jne v86_signal_return 227 xorl %edx,%edx 228 call SYMBOL_NAME(do_signal) 229 jmp restore_all 230 231 ALIGN 232 v86_signal_return: 233 call SYMBOL_NAME(save_v86_state) 234 movl %eax,%esp 235 xorl %edx,%edx 236 call SYMBOL_NAME(do_signal) 237 jmp restore_all 238 239 ALIGN 240 tracesys: 241 movl $-ENOSYS,EAX(%esp) 242 call SYMBOL_NAME(syscall_trace) 243 movl ORIG_EAX(%esp),%eax 244 cmpl $(NR_syscalls),%eax 245 jae tracesys_exit 246 call *SYMBOL_NAME(sys_call_table)(,%eax,4) 247 movl %eax,EAX(%esp) # save the return value 248 tracesys_exit: 249 call SYMBOL_NAME(syscall_trace) 250 jmp ret_from_sys_call 251 badsys: 252 movl $-ENOSYS,EAX(%esp) 253 jmp ret_from_sys_call 254 255 ALIGN 256 ENTRY(ret_from_intr) 257 GET_CURRENT(%ebx) 258 ret_from_exception: 259 movl EFLAGS(%esp),%eax # mix EFLAGS and CS 260 movb CS(%esp),%al 261 testl $(VM_MASK | 3),%eax # return to VM86 mode or non-supervisor? 262 jne ret_from_sys_call 263 jmp restore_all 264 265 ALIGN 266 reschedule: 267 call SYMBOL_NAME(schedule) # test 268 jmp ret_from_sys_call
Jak wiemy, komputer po uruchomieniu pracuje w trybie rzeczywistym. Dlatego w momencie startu systemu, po wczytaniu do pamięci i rozpakowaniu kernela niezbęne jest przejście procesora do trybu chronionego i rozpoczęcie pracy kernela. Kod za to odpowiedzialny znajduje się w pliku /arch/i386/boot/setup.S. Nas interesują instrukcje począwszy od etykiety a20_done. Na początku załadowane zostają gdt i ldt, potem maskujemy przerwania i dochodzimy do najbardziej interesującej części kodu:
785 # Well, now's the time to actually move into protected mode. To make 786 # things as simple as possible, we do no register set-up or anything, 787 # we let the gnu-compiled 32-bit programs do that. We just jump to 788 # absolute address 0x1000 (or the loader supplied one), 789 # in 32-bit protected mode. 790 # 791 # Note that the short jump isn't strictly needed, although there are 792 # reasons why it might be a good idea. It won't hurt in any case. 793 movw $1, %ax # protected mode (PE) bit 794 lmsw %ax # This is it! 795 jmp flush_instr 796 797 flush_instr: 798 xorw %bx, %bx # Flag to indicate a boot 799 xorl %esi, %esi # Pointer to real-mode code 800 movw %cs, %si 801 subw $DELTA_INITSEG, %si 802 shll $4, %esi # Convert to 32-bit pointer 803 # NOTE: For high loaded big kernels we need a 804 # jmpi 0x100000,__KERNEL_CS 805 # 806 # but we yet haven't reloaded the CS register, so the default size 807 # of the target offset still is 16 bit. 808 # However, using an operand prefix (0x66), the CPU will properly 809 # take our 48 bit far pointer. (INTeL 80386 Programmer's Reference 810 # Manual, Mixing 16-bit and 32-bit code, page 16-6) 811 812 .byte 0x66, 0xea # prefix + jmpi-opcode 813 code32: .long 0x1000 # will be set to 0x100000 814 # for big kernels 815 .word __KERNEL_CS
Zwróćmy uwagę na instrukcję lmsw z linii 794 - instrukcja ta, zgodnie z materiałami firmy Intel, instnieje jedynie w celu zachowania zgodnosci z procesorami 286. W procesorach 386 i nowszych załadowanie słowa stanu powinno być realizowane instrukcją mov cr0, ...
instrukcja skoku w linii 795, powoduje prawdopodobnie wyczyszczenie potoku wewnętrznie pobranych przez procesor instrukcji, co jest konieczne, ponieważ po przejściu w tryb chroniony adresy sa wyliczane w inny sposób (selektor:offset zamiast segment:offset) i mogą być nieprawidłowe. W nowszych procesorach chyba nie ma takiej konieczności, procesor sam to robi. (To dziwne, że wcześniej tego nie robił ;)
Ale naprawde tajemnicze rzeczy zaczynają dziać się dopiero w linii 812. Nagle ni stąd ni z owąd w kodzie pojawia się definicja kilku bajtów danych. Czym one są? Otóż jest to chwyt, który pozwala wykonać instrukcję 32 bitową w 16 bitowym segmencie kodu. Chwyt ten jest niezbędny, ponieważ adres początku kernela jest 48mio bitowy. Jak widać, czasami asembler to też za mało... :)