Do spisu tresci tematu 3

3.3.1 Opis funkcji ptrace



Spis tresci


Wprowadzenie

Ze wzgledu na dosyc nietypowy charakter funkcji ptrace - oprocz osob piszacych programy uruchomieniowe malo kto uzywa jej na codzien - wstep jest dosyc dlugi i przedstawia - bez wchodzenia w szczegoly techniczne - jej mozliwosci.

Poniewaz nikt nie pisze bezblednych programow (chociaz niektorzy sa juz bliscy idealu), czasem trzeba usunac bledy z programu. W tym pomaga wlasnie funkcja ptrace. Pozwala ona jednemu procesowi, zwanemu dalej kontrolujacym, sledzic wykonanie drugiego procesu (sledzonego). Za pomoca funkcji ptrace proces kontrolujacy daje znac systemowi, ze interesuja go zdarzenia zwiazane z procesem sledzonym, takie jak otrzymanie sygnalu, napotkanie pulapki lub wywolanie funkcji systemowej, oraz, ze w przypadku zajscia takiego zdarzenia wykonanie programu sledzonego powinno zostac wstrzymane. System po zatrzymaniu procesu sledzonego informuje proces kontrolujacy za pomoca sygnalu (ustalonego przy tworzeniu procesu potomnego, zazwyczaj SIGCHLD). Proces kontrolujacy moze wowczas (oczywiscie, za pomoca funkcji ptrace) uzyskac dostep do przestrzeni danych sledzonego, zmienic jakies dane, a nastepnie wznowic jego wykonanie.

Typowy scenariusz wyglada nastepujaco:

Szczegolowo ilustruje to program.

Oczywiscie, czasem wygodnie jest "podlaczyc sie" do juz dzialajacego procesu. Temu sluzy parametr PTRACE_ATTACH. Dalej wszystko jest jak wyzej. Oprocz modyfikowania danych w przestrzeni adresowej przydatne moze byc rowniez modyfikowanie zapamietanego kontekstu - w szczegolnosci rejestrow procesora (pozwala to np. na zmiane parametrow wywolania funkcji systemowych), co umozliwia PTRACE_[PEEK,POKE]USR. Czasem mozemy rowniez chciec wstrzymywac proces po kazdej wykonanej instrukcji (w sensie instrukcji asemblera). Oczywiscie, ptrace to umozliwia, po wywolaniu z parametrem PTRACE_SINGLESTEP.


Jak to dziala?

Tu nieco bardziej szczegolowo opisuje, co sie dzieje z procesem sledzonym, gdy ma on ustawiona flage sledzenia.

Sygnaly (a raczej ich przechwytywanie).

Przypomne, ze przyjecie sygnalu przez proces polega na ustawieniu bitu sygnalu w strukturze zadania (oczywiscie jezeli sygnal nie jest zablokowany), a nastepnie - w funkcji ret_from_sys_call [arch/i386/kernel/entry.S], wywolywanej przy powrocie z funkcji systemowych jak rowniez przy powrocie z procedur obslugi przerwan (w szczegolnosci - przerwania zegara) - wywolaniu procedury do_signal [arch/i386/kernel/signal.c], ktora wywoluje wlasciwa procedure obslugi sygnalu (a wlasciwie ustawia na stosie programu dodatkowa ramke, powodujaca wykonanie procedury obslugi sygnalu po zakonczeniu wykonywania ret_from_sys_call).

Jednakze na poczatku funkcja ta sprawdza, czy proces ma ustawiona flage sledzenia - i jezeli tak, to jeszcze zanim zostanie wywolana procedura obslugi, wstrzymuje wykonanie procesu, i z pomoca funkcji notify_parent [kernel/exit.c] informuje proces kontrolujacy (wysylajac mu ustalony sygnal, zazwyczaj SIGCLD), ze proces sledzony zostal wstrzymany. (Tak zreszta dziala wspomniana juz obsluga sygnalu SIGTRAP.) Potem, jezeli funkcja ptrace ponownie uruchamiajaca proces nie zmieni numeru sygnalu (pole exit_code struktury procesu) na 0, wykonywana jest "zwykla" obsluga. (Ktora w przypadku sygnalu SIGTRAP jest zakonczenie dzialania procesu.)

Pulapki.

Na komputerach z procesorem i386 pulapki mozna zaimplementowac na dwa sposoby:

Instrukcja int 3.

Instrukcja ta, jak kazda instrukcja int X wywoluje przerwanie (wyjatek), w tym przypadku wyjatek numer 3, czyli pulapka (ang. breakpoint). Jednakze instrukcja int 3 rozni sie od pozostalych tym, ze ma specjalny, jednobajtowy kod, dzieki czemu latwiej wstawic ja w kod programu (wystarczy podmienic jeden bajt). Procesor, napotkawszy te instrukcje generuje wyjatek numer 3, ktory w jadrze Linuxa jest obslugiwany przez funkcje int3 [arch/i386/kernel/traps.c]. Obsluga ta to jedynie wyslanie do procesu sygnalu SIGTRAP, ktorego obsluga byla juz opisywana. Nalezy zauwazyc, ze proces kontrolujacy powinien usunac instrukcje int 3 (zastepujac ja zapamietanym, oryginalnym bajtem) przed wznawianiem wykonania procesu sledzonego, a nastepnie zmniejszyc adres powrotu o 1 (za pomoca funkcji ptrace(PTRACE_POKEUSR,...)) , gdyz wyjatek 3 nalezy do kategorii potrzaskow (ang. trap), czyli umieszczany przez niego na stosie adres powrotu wskazuje na instrukcje kolejna po int 3.

Rejestry uruchamiania i praca krokowa.

Procesor i386 posiada specjalne rejestry, zwane rejestrami uruchamiania (ang. debug registers). Poniewaz opisuje jadro Linuxa, a nie architekture procesorow Intela, bez wchodzenia w szczegoly powiem, ze pozwalaja one m.in. na wywolywanie wyjatku w przypadku odczytu/zapisu danych pod podanym adresem (mozna zdefiniowac 4 takie adresy). W oczywisty sposob moze to byc wygodniejsze od uzycia int 3 (gdy nie wiemy, ktora instrukcja powoduje zmiane naszych danych). Procesor sygnalizuje dostep do danych poprzez wyjatek 1 (nazywany po angielsku debug exceptions). Tak samo sygnalizowane jest wykonanie jednej instrukcji w trybie pracy krokowej (ustawianym przez ptrace(PTRACE_SINGLESTEP,...)). Tu procedura obslugi, do_debug [arch/i386/kernel/traps.c], musi byc nieco bardziej skomplikowana - bo wyjatek ten moze wystapic rowniez w trybie jadra (np. gdy zalozymy pulapke na adresie, ktory podamy jako adres bufora do funkcji read). W takiej sytuacji flaga pracy krokowej jest czyszczona, dzieki czemu proces konczy dzialanie w trybie jadra zanim zostanie zatrzymany. Reszta obslugi jest taka sama jak poprzednio - poprzez sygnal SIGTRAP.

Funkcje systemowe.

Jak wiadomo, w Linuxie funkcje systemowe sa wywolywane poprzez przerwanie 0x80. Procedura obslugi tego przerwania, system_call [arch/i386/kernel/entry.S] sprawdza, czy nie jest ustawiona flaga PF_TRACESYS (sledzenie funkcji systemowych), i jezeli tak - wywoluje funkcje syscall_trace. Nastepnie - oczywiscie, po wznowieniu procesu, obsluguje zadanie, i ponownie wywoluje funkcje syscall_trace. (Czyli gdy ustawimy sledzenie wywolan funkcji systemowych proces sledzony zostanie wstrzymany dwa razy na kazde wywolanie funkcji systemowej, nawet jezeli po pierwszym wstrzymaniu wyczyscimy flage sledzenia.)

Opis funkcji ptrace oraz funkcji pomocniczych

Funkcja ptrace:


DEFINICJA:
int ptrace(int request, int pid, int addr, int data);

WYNIK: 0 gdy wszystko sie udalo,
       -1 w przypadku bledu; errno = EPERM (proces nie moze byc sledzony
                                             lub juz jest sledzony)
				     ESRCH (proces nie istnieje, nie ma
				             ustawionej flagi sledzenia lub
					     nie jest zatrzymany)
				     EIO   (zle zadanie)

Najwazniejszym parametrem jest request, od niego zalezy znaczenie pozostalych parametrow.

request ==

PTRACE_TRACEME
Ustawia flage PF_PTRACED w strukturze zadania.
Pozostale parametry nieistotne.
Blad EPERM, jezeli proces juz jest sledzony.

PTRACE_ATTACH
sprawdza, czy mozemy sledzic proces (tzn. czy zgadzaja sie identyfikatory uzytkownika), czy proces nie jest juz sledzony, i jezeli jest OK, ustawia w nim flage sledzenia oraz zmienia rodzica procesu na proces wywolujacy.
pid - numer procesu, ktory chcemy sledzic.
Blad EPERM jezeli nie mamy prawa sledzic procesu lub juz ma ustawiona flage sledzenia.
PTRACE_DETACH
Jak nietrudno sie domyslec - odlacza wczesniej dolaczony proces, kasujac flagi PF_PTRACED oraz PF_TRACESYS i flage procesora TRAP_FLAG, oraz zmieniajac ojca procesu na oryginalnego ojca (co ma znaczenie jezeli dolaczylismy sie do procesu przez ptrace(PTRACE_ATTACH,...))
Parametry:
pid - numer procesu
data - wartosc wstawiana do pola exit_code procesu

PTRACE_PEEKTEXT
Parametry:
pid - numer procesu
addr - adres spod ktorego chcemy czytac
data - adres w ktorym mamy zapamietac wartosc
Odczytuje, z pomoca funkcji read_long, slowo spod adresu addr w przestrzeni procesu pid, i zapisuje je pod adresem data w przestrzeni procesu wywolujacego.

PTRACE_PEEKDATA
W obecnej wersji robi to samo, co funkcja poprzednia.

PTRACE_POKETEXT
Parametry:
pid - numer procesu
addr - adres w ktorym zapiszemy
data - adres spod ktorego chcemy czytac
Przeciwienstwo PTRACE_POKETEXT - przepisuje slowo data w procesie wolajacym pod adres addr w procesie pid, uzywajac w tym celu funkcji write_long.

PTRACE_POKEDATA
W obecnej wersji robi to samo, co funkcja poprzednia.

PTRACE_PEEKUSR
Parametry:
pid - numer procesu
addr - offset w USER AREA
data - adres, pod ktorym mamy zapamietac slowo
USER AREA to obszar pamieci (zapamietany na stosie poziomu 0), zawierajacy zapamietany kontekst procesu, w szczegolnosci rejestry procesora (zwykle i uruchomieniowe). PTRACE_PEEKUSR pozwala odczytac zawartosc tych rejestrow. Adresy rozpoznawane przez te funkcje to:

EBX ECX EDX ESI EDI EBP EAX DS
0x000x040x080x0C0x100x140x180x1C
ES FS GS EIP CS EFL UESP SS
0x200x240x280x300x340x380x3C0x40

oraz adresy rejestrow uruchomieniowych, obliczane jako offset pola u_debugreg[n], 0<=n<=7 struktury user [include/asm-i386/user.h].
Pozostale pola tej struktury sa niewidoczne.
Algorytm jest prosty: struktura zadania dla procesu pid zawiera m.in. pole tss (opis zadania w sensie zadania procesora), ktore zawiera pole esp0, czyli wskaznik stosu poziomu zerowego. Poniewaz jestesmy w trakcie wykonywania kodu jadra, wiec jestesmy na poziomie zerowym i ten stos jest w naszej przestrzeni adresowej, wiec po prostu dodajemy odpowiednie przemieszczenie do adresu stosu i odczytujemy zadana wartosc, nastepnie kopiowana do przestrzeni danych procesu wolajacego (po ewentualnych zmianach, wynikajacych z wymagan ochrony). Dlaczego akurat dane te sa na stosie poziomu zerowego wynika czesciowo z architektury procesora i386, a czesciowo z metody wywolywania funkcji systemowych w Linuxie, i jest troche poza zakresem opisywanego tematu. Zainteresowanych odsylam do ksiazek opisujacych procesor 386, wspomnianych w bibliografii.

PTRACE_POKEUSR
Parametry:
pid - numer procesu
addr - offset w USER AREA
data - slowo do umieszczenia
Oczywiscie, przeciwienstwo PTRACE_PEEKUSR. Rozpoznaje te same adresy, algorytm jest rowniez bardzo podobny.

PTRACE_SYSCALL
Parametry:
pid - numer procesu
data - slowo do wstawienia jako exit_code procesu
Zmienia numer sygnalu na data (co pozwala - w przypadku zatrzymania procesu z powodu otrzymania sygnalu - na wywolanie procedury obslugi innego sygnalu, lub zignorowanie go), budzi zastopowany proces (uzywajac funkcji wake_up_process), ustawia flage PF_TRACESYS, i kasuje flage pracy krokowej procesora.

PTRACE_CONT
Od powyzszej funkcji rozni sie tylko tym, ze nie ustawia flagi sledzenia wywolan funkcji systemowych (PF_TRACESYS).

PTRACE_KILL
Zabija proces. W zasadzie rownowazne kill(pid, SIGKILL).

PTRACE_SINGLESTEP
Parametry:
pid - numer procesu
data - slowo do wstawienia jako exit_code procesu
Podobne do PTRACE_SYSCALL, ale zeruje PF_TRACESYS, i ustawia flage pracy krokowej procesora.
Uwaga: jedynie PTRACE_TRACEME, PTRACE_ATTACH i PTRACE_KILL moga byc wywolane gdy proces nie jest w stanie TASK_STOPPED, proba uzycia innych argumentow spowoduje blad ESRCH.

Uwaga: wszystkie nastepne funkcje sa wewnetrznymi procedurami jadra, i nie ma mozliwosci odwolania sie do nich z zewnatrz.

Funkcja syscall_trace:


DEFINICJA:
asmlinkage void syscall_trace(void);
WYNIK: nie ma

Funkcja ta jest wywolywana gdy jadro wykryje, ze ustawiona jest flaga PF_TRACESYS podczas wchodzenia do/wychodzenia z funkcji systemowej.
Zatrzymuje ona proces (zmieniajac stan procesu na TASK_STOPPED), i zawiadamia o tym (za pomoca funkcji notify_parent) proces kontrolujacy (dla ktorego wyglada to jak zawiadomienie o otrzymaniu sygnalu SIGTRAP). Nastepnie wywoluje funkcje schedule (aby uspic proces sledzony, i umozliwic dzialanie innym). Po powrocie - czyli po wznowieniu procesu sledzonego przez kontrolujacy - traktuje zawartosc pola exit_code struktury zadania jako numer sygnalu, ktory ustawia.

Funkcja read_long:


DEFINICJA:
static int read_long(struct task_struct *tsk, unsigned long addr,
                     unsigned long *result);
WYNIK: 0 gdy zadzialala
       -EIO gdy nie dalo sie odczytac slowa (zly adres)

Odczytuje zawartosc adresu addr w przestrzeni zadania tsk, zapamietujac ja w zmiennej result.
Cala "czarna robote" wykonuje funkcja get_long, ta tylko przygotowuje odpowiednie parametry - numery obszarow zawierajacych szukane dane, oraz sklada wynik. (Funkcja get_long potrzebuje adresu podzielnego przez 4, wiec jezeli nasz addr jest zly, musimy 2 razy wykonac get_long, a nastepnie zlozyc wynik.)

Funkcja write_long:


DEFINICJA:
static int write_long(struct task_struct *tsk, unsigned long addr,
                     unsigned long data);
WYNIK: 0 gdy zadzialala
       -EIO gdy nie dalo sie odczytac slowa (zly adres)

Zapisuje slowo data w przestrzeni zadania tsk pod adresem addr.
Analogiczna do funkcji poprzedniej, tylko zamiast get_long jest put_long.

Funkcja get_long:


DEFINICJA:
static unsigned long get_long(struct task_struct *tsk,
       struct vm_area_struct *vma, unsigned long addr);
WYNIK: Wartosc slowa gdy wszystko dobrze
       0 gdy wystapi blad

Funkcja ta znajduje strone o adresie addr w przestrzeni adresowej zadania (zakladajac, ze strona ta jest w obszarze vma); jak to zrobic bedzie niewatpliwie opisane w rozdziale o zarzadzaniu pamiecia - ma to zwiazek z przegladaniem 3-poziomowej struktury stron zadania. Nastepnie, co chyba oczywiste, odczytuje szukane slowo i je zwraca.

Funkcja put_long:


DEFINICJA:
static void put_long(struct task_struct *tsk, struct vm_area_struct *vma,
                      unsigned long addr, unsigned long data);
WYNIK: nie ma

Analogicznie do funkcji poprzedniej, zapisuje data pod adresem addr w zadaniu tsk. Algorytm praktycznie taki sam.

Zrodla informacji:

  1. Zrodla jadra:
  2. Maurice J. Bach, Budowa systemu operacyjnego UNIX, WNT 1995, strony 383-388 - ogolny opis uzycia funkcji ptrace
  3. Ryszard Goczynski, Michal Tuszynski, Mikroprocesory 80286, 80386 i i486, HELP 1991
  4. Man pages: ptrace(2).

Pytania (wraz z odpowiedziami)

  1. Czy da sie odczytac wiecej niz jedno slowo za pomoca jednego wywolania funkcji ptrace?

    Nie, do tego moze sluzyc proc_fs.


Autor: Lukasz Sznuk