Do spisu tresci tematu 2

2.1.4 Wywolywanie innych programow - algorytm exec



Spis tresci


Informacje ogolne

Przeznaczenie

Procedura exec ma za zadanie zmienic kontekst poziomu uzytkownika danego procesu na kopie programu wykonywalnego umieszczonego w pliku dyskowym. Tym samym, statyczny kod, jakim jest program na dysku, staje sie dynamicznym, dzialajacym w pamieci operacyjnej, procesem (byc moze nie jest to jedyny proces odpowiadajacy danemu programowi). Jesli procedura powiedzie sie, to poprzedni kontekst procesu zostanie bezpowrotnie stracony.

Funkcje towarzyszace

Wywolanie funkcji exec wystepuje na ogol wraz z wywolaniami funkcji fork i wait, co pozwala realizowac w Unixie mechanizm typu spawn dostepny np. w systemie MS-DOS. Mechanizm ten polega na chwilowym przekazaniu sterowania do uruchamianego programu, a po jego zakonczeniu, kontynuowaniu wykonywania procesu wywolujacego bez ustraty zadnych danych. Schematyczny kod realizujacy opisany mechanizm w Unixie moglby wygladac nastepujaco:


Interfejs programisty w jezyku C

W C, typowym dla srodowyska unixowego jezyku programowania, istnieje az szesc roznych sposobow wywolania funkcji systemowej exec. Sa to rozne funckje biblioteczne jezyka, ktore w rzeczywistosci odwoluja sie do jednej tylko funkcji systemowej. W tym punkcie przedstawionych zostanie wymienione szesc funkcji bibliotecznych, a w dalszej czesci opracowania zajmiemy sie tylko wewnetrzym ich odpowiednikiem tj. funkcja systemowa.

Przekazywanie parametrow wywolania

Parametry wywolania nowego programu (napisy, ktore umieszcza sie zwykle za nazwa programu w linii komend systemu operacyjnego) mozana podac badz jako kolejne argumenty wywolania funkcji (odpowiada tej mozliwoasci kod l w nazwie funkcji) badz tez, jako tablice wskaznikow na napisy (kod: v). W kazdym z wymienionych przypadkow ciag argumentow musi byc zakonczony zerem (a nie napisem pustym, ktory stanowi pelnoprawny argument).

Przekazywanie srodowiska

Srodowisko jest pomocniczym zbiorem napisow zorganizowanym wg stalego wzorca: "nazwa-zmiennej=wartosc-zmiennej". Mozliwe jest jawne wyspecyfikowanie srodowiska, z ktorym zostanie uruchomiony nowy program (kod: e). Jesli tego nie zrobimy, zostanie mu przekazane srodowisko, w jakim dziala proces wywolujacy (jest ono dostepne poprzez zmienna globalna jezeka C o nazwie environ).

Nazwa pliku a nazwa sciezkowa

Mozemy wreszcie wybierac pomiedzy pelna nazwa sciezkowa programu, a tylko ostatnim jej czlonem. W tym drugim przypadku (kod: p) wykorzystana zostanie zmienna srodowiskowa PATH.

Laczenie sposobow wywolania

lista argumentow tablica wskaznikow
wykorzystanie zmiennej PATH

execlp

execvp

wywolanie zwykle

execl

execv

jawne okreslenie srodowiska

execle

execve

Tabela przedstawia zestawienie mozliwych sposobow wywolania algorytmu exec


Procedura exec w Linuxie

W systemie Linux istnieje funkcja sys_execve (do ktorej odwoluja sie funkcje biblioteczne jezyka C) dostepna dla programow uzytkownika, ktora po dodkonaniu drobnych, technicznych manipulacji wywoluje do_execve. Ta ostatnia realizuje wlasciwy algorytm exec.

Glowne kroki wykonywane przez algorytm exec to:

  1. odczytanie i-wezla pliku z programem wykonywalnym i wykonanie wstepnych kontroli majacych na celu ustalenie, czy uruchomienie nowego programu jest mozliwe
  2. kopiowanie argumentow wywolania i srodowiska do tymczasowego obszaru pamieci, ktory pozniej zostanie przylaczony do przestrzeni adresowej procesu
  3. odszuaknie procedury odpowiedzialnej za dalsze ladowanie i uruchomienie programu

Trzeba w tym miejscu wyjasnic, ze Linux obsluguje, w pelni automatycznie, wiele formatow plikow wykonywalnych (np. pliki zapisane w typowym dla starszych wersji Unixa standardzie a.out i nowoczesnym, coraz powszechniejszym standardzie elf). Co waznejsze jednak, budowa wewnetrzna jadra pozwala niezwykle latwo usuwac istniejace lub dodawac nowe formaty przy minimalnych zmianach istniejacego kodu systemu. Formaty plikow wykonywalnych i procedury ich obslugi zostaly omowione dakladniej w pukcie 2.2.4.1; tutaj zajmiemy sie jedynie wyborem formatu odpowiadajacego ladowanemu plikowi.


Czynnosci wstepne

W czasie ladowania programu wykonywalnego wykorzystuje sie strukture linux_binprm

struct linux_binprm {

        char buf[128];    /* niewielki bufor pozwalajacy zaladowac 
                             poczatkowy fragment pliku, ktory okresla 
                             jego format */

        unsigned long page[MAX_ARG_PAGES];
                          /* tablica stron zawierajacych argumenty
                             wywolania i srodowisko (aktualnie stala 
                             MAX_ARG_PAGES ustawiona jest w ten sposob, 
                             ze argumenty wraz ze srodowiskiem moga
                             zajmowac do 128kB, czyli calkiem sporo) */

        unsigned long p;  /* adres w pamieci zawierajacej argumenty i
                             srodowisko; adresy wyzsze sa zajete, nizsze -
                             moga nawet nie miec przydzielonych stron */
  
        int sh_bang;      /* znacznik wlaczenia iterpretowania; 
                             opisany w pukcie 2.2.4.1 */

        struct inode * inode;
                          /* i-wezel pliku wykonywalnego */ 

        int e_uid, e_gid; /* obowiazujacy identyfikator uzytkownika i 
                             grupy dla nowego programu */ 

        int argc, envc;   /* liczba napisow, odpowiednio:
                             argumentow i srodowiska */

        char * filename;  /* nazwa sciezkowa pliku wykonywalnego */

        unsigned long loader, exec;
                          /* pomocnicze adresy uzywane w czasie ladowania */ 

        int dont_iput;    /* znacznik zwolnienia i-wezla */      
};

Do odszukania i-wezla zwiazanego z danym plikem wykonywalnym wykorzystuje sie procedure open_namei. Nastepnie, po przypisaniu wartosci poczatkowych niektorym polom struktury linux_binprm wywoluje sie prepare_binprm, ktora to procedura wykonuje wlasciwe testy.

Przeprowadza sie nastepujace kontrole:

Na koniec odczytywane jest pierwsze 128 bajtow pliku zawierajace na ogol informacje potrzebna do rozpoznania formatu pliku wykonywalnego. Na poczatku pliku znajduja sie najczesciej tzw. liczby magiczne okreslajace jego rodzaj.


Przenoszenie napisow pomiedzy przestrzeniami adresowymi

Ladowanie nowego programu powoduje wymiane kontekstu poziomu uzytkownika procesu z usunieciem poprzedniej jego wersji. Istnieje jednak moment, w ktorym obie przestrzenie adresowe (a przynajmniej okreslone ich fragmenty) musza wspolistniec w pamieci. Bez tego nie bylo by mozliwe przeniesienie napisow oznaczajacych argumenty wywolania i srodowiska nowego programu.

Linux radzi sobie z tym problemem przydzielajac nowe strony pamieci, na ktorych umieszcza kopie odpowiednich danych (tablica page w linux_binprm). Po udanym ladowaniu strony te zostana dolaczone do nowej przestrzeni adresowej, a jesli ladowanie nie powiedzie sie, zwolni sie je. Ponizej przedstawiono uklad pamieci po skopiowaniu w procedurze do_execve: nazwy pliku, srodowiska i argumentow wywolania.

obrazek przedstawia uklad pamieci po kopiowaniu dokonanym w procedurze 
do_execve

Po pomyslnym zakonczeniu ladowania programu opisywany obszar pamieci zostanie przylaczony jako stos na koncu wirtualnej przestrzeni adresowej procesu przez funkcje setup_arg_pages.


Rozpoznawanie formatu pliku

Jak wspomniano wczesniej, Linux wspiera wiele roznorodnych formatow plikow wykonywalnych. Ladowanie pliku w kazdym z tych formatow przebiega nieco inaczej, choc czesc czynnosci jest wspolna. W szczegolnosci, wszystkie opisane wczesniej kontrole wstepne i przenoszenie danych miedzy przestrzeniami adresowymi odbywa sie zanim jadro podejmie probe rozpoznania formatu pliku. Wynika z tego, ze sa to czynnosci na tyle uniwersalne, iz powinny zostac wykonane niezaleznie od formatu ladowanego pliku.

Rozpoznanie formatu dokonywane jest na podstawie odczytanego wczesniej poczatkowego fragmentu pliku. Kolejno sprawdzane formaty zapisane sa na liscie wskazywanej przez zmienna globalna jadra formats w strkturach linux_binfmt w procedurze search_binary_handler. Jesli nie uda sie znalezc formatu odpowiadajacego ladowanemu plikowi, a zachodzi podejrzenie, ze modul jadra obslugujacy dany format moze zostac zaladowany dynamicznie, to po takim ladowaniu ponownie podejmuje sie probe odnalezienia pasujacego formatu. Jesli i ta proba nie powiedzie sie, to funkcja systemowa exec zasygnalizuje blad (nieznany format pliku) i wroci do wolajacego ja procesu.

Ostatnia opisana cecha czyni Linuxa systemem bardzo oszczednym. Jesli zezwolimy na dynamiczne ladowanie modulow jadra odpowiedzialnych za poszczegolne formaty, to beda one sprowadzane do pamieci tylko wtedy, gdy zajdzie potrzeba zaladowania programu w okreslonym formacie. Jesli wiec dysponujemy modulem jadra zwiazanym z bardzo rzadko wystepujacym formatem, to smialo mozemy go dolaczyc bez obawy o utrate miejsca w pamieci lub urzadzeniu wymiany - program ladujacy pojawi sie w niej w odpowiednim momencie i do tego - na krotko.

Procedury rozpoznajace formaty potrafia, oprocz identyfikacji obslugiwanych przez siebie plikow, dokonczyc dziela ladowania programu do pamieci. Zostaly one opisane w punkcie 2.2.4.1


Usuwanie poprzedniego kontekstu

Mimo, ze od momentu rozpoznania formatu pliku wykonywalnego cala inicjatywa przekazana zostaje funkcjom zwiazanym z danym formatem, sa jednak pewne typowe czynnosci, ktore powinny zostac wykonane zawsze. Do czynnosci tych nalezy przede wszystkim usuniecie, niepotrzebnych juz teraz, fragmentow kontekstu procesu dotyczacych poprzednio dzialajacego programu. Czynnosci te powinny zostac zrealizowane przy kazdorazowym ladowaniu programu, choc o najwlasciwszym momencie ich uruchomienia zadecuduje procedura specyfyczna dla formatu.

Usuniecie poprzedniego kontekstu realizowane jest w procedurze flush_old_exec, ktorej dzialanie sprowadza sie glownie do wywolania, omowionych dalej, specjalistycznych procedur odpowiedzialnych za usuwanie rozmaitych elementow kontesktu. Oprocz tego, wpisuje ona ostatnia czesc nazwy sciezkowej uruchamianego programu do pola comm umieszczonego w strukturze task_struct, ktore jest uzywane do celow rozliczeniowych i zrzucania na dysk obrazu procesu przerwanego sygnalem.

Zwalnianie pamieci

Segmenty pamieci danych, kodu i stosu poprzedniego procesu nie beda juz potrzebne. Zwolni je procedura exec_mmap pozostawiajac kontekst procesu bez zadnych przylaczonych segmentow. Warto zwrocic uwage, ze nie ma w tym miejscu rozroznienia pomiedzy segmentami prywatnymi, ktore nalezy obowiazkowo zwalniac, a segementami dzielonymi (czy to danych, czy kodu), ktore powinny pozostac w pamieci tak dlugo, jak istnieje choc jeden korzystajacy z nich proces. Problemem tym zajma sie wlsciwe procedury obslugi pamieci wywolywane przez exec_mmap.

Inna ciekawa obserwacja jest fakt, ze w Linuxie nie ma (opisywanego np. przez Bacha na stronie 242) bitu lepkosci (ang. sticky bit). Bit taki, zwiazany z kazdym segmentem kodu umieszczonym w pamieci, mialby zapewniac wieksza efektywnosc dzialania systemu osiagana poprzez stale przewchowywanie kodu czesto uruchamianych programow. Zastosowane w Linuxie mechanizmy stronicowania zwalniaja administratora z koniecznosci decydowania o tym, ktore programy trzeba przechowywac w pamieci, a ktore nie.

Obsluga sygnalow

Moze zdarzyc sie sytuacja, w ktorej proces zmieni standardowa obsluge pewnych sygnalow, a nastepnie wywola inny program poleceniem exec. Nie zawsze pozostawienie niezmienionej obslugi sygnalow przy zmianie kontekstu ma sens (kod obslugi sygnalow mogl przestac istniec). Z drugiej strony, nachalne przywrocenie standardowej obslugi wszystkich sygnalow pozbawiloby tworcow oprogramowania silnego narzedzia programistycznego.

Wybrane w Unixie rozwiazanie kompromisowe przywraca standardowa obsluge tylko tych sygnalow, dla ktorych ustawiono wczesniej nowa procedure obslugi. Oznacza to, ze sygnaly, ktore byly ignorowane w programie wywolujacym, pozostana takimi w programie wywolywanym. Za zmiane sposobu reagowania na sygnaly odpowiada procedura flush_old_signals.

Zamykanie plikow

Z kazdym deskryptorem otwartego pliku zwiazana jest w procesie flaga informujaca, czy dany plik powinien zostac automatycznie zamkniety przy odwolaniu do exec (ang. close on exec). Dzieki tej mozliwosci, pewne pliki, co do ktorych mamy pewnosc, ze nie beda potrzebne nowemu programowi, mozna zaznaczyc jako przeznaczone do automatycznego zamkniecia. Ich deskryptory zostana w odpowiednim momencie zwolnione, udostepniajac wiecej zasobow kolejnemu zadaniu. Zamykanie plilow odbywa sie w procedurze flush_old_files, a zmiane sposobu ich traktowania uzyskujemy za pomoca funkcji fcntl.


Bibliografia

M. J. Bach, WNT Warszawa 1995,
rozdzial 7.5: Wywolywanie innych programow, strony 234-244

W. R. Stevens WNT Warszwa 1996,
rozdzial 2.5.3: Funkcja systemowa exec, strony 82-84


Autor: Adam Wasylewski