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:
fork
,
clone
),
ptrace(PTRACE_TRACEME,0,0,0)
,
co ustawia
bit sledzenia w strukturze zadania i wywoluje funkcje exec
,
uruchamiajac program sledzony,
SIGTRAP
,
exec
, ale jeszcze przed powrotem
do wywolujacego kodu (czyli w trybie jadra) obsluguje ten sygnal,
co polega na zmianie stanu procesu na TASK_STOPPED
oraz
przeslaniu sygnalu do procesu kontrolujacego.
ptrace(PTRACE_PEEKTEXT,...)
,
ptrace(PTRACE_POKETEXT,...)
)
a nastepnie wznowic jego wykonanie
(ptrace(PTRACE_CONT,...)
,
ptrace(PTRACE_SYSCALL,...)
).
Az do nastepnego sygnalu, funkcji systemowej etc.
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
.
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.)
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
.
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
.
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.)
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)
request ==
PF_PTRACED
w strukturze zadania. EPERM
, jezeli proces juz jest sledzony.
EPERM
jezeli nie mamy prawa sledzic procesu lub
juz ma ustawiona flage sledzenia.
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,...)
)read_long
, slowo spod adresu
addr
w przestrzeni procesu pid
, i zapisuje
je pod adresem data
w przestrzeni procesu wywolujacego. PTRACE_POKETEXT
- przepisuje slowo
data
w procesie wolajacym pod adres addr
w procesie pid
, uzywajac w tym celu
funkcji write_long
.
PTRACE_PEEKUSR
pozwala odczytac zawartosc tych rejestrow.
Adresy rozpoznawane przez te funkcje to:
EBX | ECX | EDX | ESI | EDI | EBP | EAX | DS |
0x00 | 0x04 | 0x08 | 0x0C | 0x10 | 0x14 | 0x18 | 0x1C |
ES | FS | GS | EIP | CS | EFL | UESP | SS |
0x20 | 0x24 | 0x28 | 0x30 | 0x34 | 0x38 | 0x3C | 0x40 |
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_PEEKUSR
.
Rozpoznaje te same adresy,
algorytm jest rowniez bardzo podobny.
exit_code
procesudata
(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.
PF_TRACESYS
).
kill(pid, SIGKILL)
.
exit_code
procesuPTRACE_SYSCALL
, ale zeruje
PF_TRACESYS
, i ustawia flage
pracy krokowej procesora.
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.
syscall_trace
:
DEFINICJA: asmlinkage void syscall_trace(void); WYNIK: nie ma
PF_TRACESYS
podczas wchodzenia do/wychodzenia z
funkcji systemowej.
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.
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)
addr
w przestrzeni zadania
tsk
, zapamietujac ja w zmiennej result
.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.)
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)
data
w przestrzeni zadania tsk
pod adresem addr
.
get_long
jest
put_long
.
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
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.
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
data
pod adresem
addr
w zadaniu tsk
. Algorytm praktycznie taki sam.
arch/i386/kernel/ptrace.c
- funkcje
ptrace
,
syscall_trace
,
read_long
,
write_long
,
get_long
,
put_long
.
include/asm-i386/ptrace.h
- adresy rejestrow na stosie
include/linux/ptrace.h
- definicje parametru request dla ptrace
arch/i386/kernel/entry.S
- funkcje
system_call
i
ret_from_sys_call
arch/i386/kernel/signal.c
- do_signal
kernel/exit.c
-
notify_parent
arch/i386/kernel/traps.c
- int3
,
do_debug
include/asm-i386/user.h
- definicja struktury user
ptrace
? Nie, do tego moze sluzyc proc_fs.