Wywołania systemowe w systemie GNU/Linux
Na początku krótkie wyjaśnienie tego czym jest wywołanie systemowe w ogólnym tego słowa znaczeniu. Wywołanie systemowe (System Call) to funkcja używana przez proces w celu zażądania od systemu operacyjnego (a ściślej - jądra) określonej usługi. Wywołania systemowe są generowane przez przerwania w samym programie (procesie).
Nowoczesne procesory przetwarzają instrukcje w kilkudziesięciu trybach dostępu (różniących się poziomem bezpieczeństwa oraz np. czasem trwania). W systemie GNU/Linux, definiujemy tylko dwa tryby - Tryb Użytkownika oraz Tryb Nadzorcy. Poziomy dostępu zostały stworzone po to, aby system mógł w prosty i jasny sposób decydować o przyznaniu dostępu do zasobów dla procesu. Jądro systemu operacyjnego GNU/Linux uruchamiane jest zawsze w uprzywilejowanym trybie - z oczywistej przyczyny - musi przyznawać dostęp do warstwy sprzętowej dla aplikacji (warstwy programowej), zarządzać przerwaniami, zmieniać tryb dostępu procesora czy też zarządzać pamięcią (tutaj przychodzi ku pomocy sheduler, którego opisałem całkiem niedawno).
Jednak system (kernel) sam w sobie nie potrafi kontrolować mechanizmu zmiany trybu dostępu. Aby zapewnić obsługę sytuacji, w której mnie uprzywilejowany kod transferuje swoje dane (lub jakiekolwiek wygenerowane przez niego) do wyżej uprzywilejowanego kodu potrzebujemy małego "włamu" i przełamania zabezpieczeń. Oczywiście - nie dosłownie. Nie możemy dopuścić do hipotetycznej sytuacji, w które mniej uprzywilejowany proces stara się wymusić na procesie o wyższym priorytecie np. błąd stosu. Standardowo system operacyjny dostarczany jest z biblioteką obsługującą zdarzenia i pośredniczącą między jądrem a procesem. W GNU/Linux tą biblioteką jest libc (Biblioteka C). To ona zarządza i nadzoruje wszystkie niskopoziomowe przepływy informacji pomiędzy procesami i kernelem, a także dzięki niej odbywa się "magiczna zamiana" trybu pracy procesora (z mniej uprzywilejowanego na bardziej lub na odwrót).
Posiadamy więc bibliotekę, którą użytkownik może wywołać w swoim programie i skorzystać z jej funkcji, aby przekazać "coś" "gdzieś" (mówiąc kolokwialnie:)). Przyjrzyjmy się zatem troszkę bliżej libc a w szczególności tematem wywołań systemowych. Jako przykład zatem - kawałek kodu odpowiedzialny za ustawienie UID w GNU/Linuksie.
_syscall1(int,setuid,uid_t,uid); _setuid: subl $4,%exp pushl %ebx movzwl 12(%esp),%eax movl %eax,4(%esp) movl $23,%eax movl 4(%esp),%ebx int $0x80 movl %eax,%edx testl %edx,%edx jge L2 negl %edx movl %edx,_errno movl $-1,%eax popl %ebx addl $4,%esp ret L2: movl %edx,%eax popl %ebx addl $4,%esp ret
Jak widać - operujemy assemblerem. Każda funkcja posiada co najmniej 2 argumenty: numer wywołania systemowego i rodzaj (definicję) działania tegoż wywołania. Każda funkcja wywołania systemowego biblioteki libc zwalnia przerwanie 0x80. Z tego adresu możemy pobrać (np. strace'm) wszystkie wywołania systemowe dowolnego programu. Gdy przerwanie zwolni się (najczęściej po określonym w jądrze "tiku"(czasie)) kod wyjścia jest pobierany z pamięci i możliwy do odczytania przez inną funkcję, bądź też przez wymieniony strace. Argumenty przekazywane są w rejestrach w następującej kolejności: eax, ebx, ecx, edx, edi, esi, ebp. Numer funkcji systemowej jest przekazywany w eax.
Całą pracę za wywołania przejmuje system przerwań. Po wywołaniu funkcji system_call() "praca" rozpoczyna się w 'adresie wejścia', który jest zdefiniowany w arch/i386/kernel/entry. Niestety, rutyny te zapisane są w języku niezrozumiałym dla przeciętnego człowieka. Stąd też krótki algorytm, jako ilustracja tego co dzieje się w czasie wywołania systemowego:
zwraca typ system_call(sys_call_number, sys_call_arguments)
{
Zapisz rejestry procesu;
Sprawdź, czy sys_call_number reprezentuje prawdziwą wartość wywołania;
jeśli nie pokaż błąd
w innym wypadku
{
Sprawdź flagę PF_TRACESYS wybranego procesu;
Wywołaj planistę (jeśli potrzebny);
Przekaż sygnały;
Swolnij rejestry procesu;
}
}
I krótkie wyjaśnienie//opis opisanego algorytmu. Dwie zmienne są przekazywane do rutyny: Sys_call_number odwołuje się do wcześniej zdefiniowanej liczby identyfikującej wywołanie systemowe. Sys_call_arguments przekazuje kolejne argumenty dla wywołania (jeśli potrzebne). Zapisanie rejestrów jest potrzebne, gdyż bez tego wywołanie planisty w późniejszym czasie mogłoby skutkować SIGSEGV. Następnie - sprawdzenie czy numer wywołania jest prawidłowy. Flaga PF_TRACESYS pozwala stwierdzić, czy dany proces nie ma procesu-rodzica. Tutaj wchodzi do gry funkcja syscall_trace (oto jej kod źródłowy arch/i386/kernel/ptrace.c). Jeśli wykryje rodzica przesyła mu SIGTRAP, wcześniej ustawiając procesowi potomnemu flagę TASK_STOPPED. Tutaj wkracza nasz dzielny planista i robi co do niego należy - zajmuje się pamięcią obu procesów (bądź jednego, jeśli proces nie miał rodzica). Jeśli wysłaniu sygnału SIGTRAP i pracy planisty proces dostał//wysłał jakikolwiek sygnał zostaje on przetworzony w tym momencie, po czym przywracane są rejestry.
I na koniec przykład użycia. Oto kawałek strace dla /bin/true:
brk(0) = 0x804a308 brk(0x804b308) = 0x804b308 brk(0x804c000) = 0x804c000 _exit(0)
Przepiękny pogląd na funkcję brk(), która poprzez przerwanie (wywołanie systemowe) odwołała się do pamięci (adres 0x804a308). Program bez praw dostępu do pamięci (przynajmniej bezpośrednio - nie działający w trybie uprzywilejowanym) poprzez System Call odwołał się do jądra, aby ten dał mu mały fragment wolnej pamięci.
Celowo nie opisuję sposobu tworzenia własnych wywołań systemowych. Zainteresowani zajrzą zapewne do Google, a niezainteresowani w końcu skończą czytać ten nudny wpis :-).
I tą oto jogg-notką powracam do żywych :-) Witajcie już oficjalnie! Następny odcinek mam już praktycznie napisany - a w nim... DCOP - jedna z rzeczy za które kocham KDE! :-) Stay Tuned!
Heh już nie mogę się doczekać :-]
przyjemne.. czekam na więcej! :)
@radmen - to dziś sobie poczytasz :-)
@ciastek - dzięki dzięki :) Nie sądziłem że kogoś może ten artykuł zainteresować. Chociaż - skoro mnie interesują takie sprawy to pewnie znajdą się inni użytkownicy Linuksa, którzy chcą wiedzieć "co się dzieje pod maską" :)
Już redaguję DCOP'a :)