next next
Next: Szybkie zapoznanie z GDB Previous: Funkcja printk() oraz demony Klogd i Syslogda

Narzędzia strace i ltrace

Są to narzędzia do śledzenia wywołań funkcji wewnątrz śledzonego programu oraz sygnałów do niego wysyłanych. W ogólności będziemy badać wykonanie polecenia z linii komend, poprzedzając owe polecenie komendą strace lub ltrace.

Uwaga: W przypadku tych narzędzi do odpluskwiania nie trzeba posiadać źródeł śledzonego programu, ani nie jest wymagana wcześniejsza specjalna kompilacja owych źródeł (np. opcja -g przed użyciem gdb).

Czym się różni strace i ltrace?

Najprościej rzecz ujmując strace śledzi wywołania funkcji systemowych (stąd s- od system), a ltrace funkcji bibliotecznych (l- library).

Jakie dostajemy informacje?

Po wywołaniu s(l)trace polecenie dostajemy na ekranie (standardowo stderr) lub (przy użyciu opcji -o) wypisane do pliku informacje o nazwie każdej wywołanej funkcji, jej parametrach przy wywołaniu oraz wartości przez nią zwróconą, a w przypadku błędy nazwę symboliczną błędu i jej opis, np.

open(,,/foo/bar'', O_RDONLY) = -1 ENOENT (No such file or directory)

Uwaga: Największym mankamentem tego narzędzia jest właśnie ilość informacji, które dostajemy, a problemem odnalezienie tych istotnych.

Co się kryje pod nazwami: funkcje systemowe i biblioteczne?

W przypadku funkcji systemowych najczęściej będziemy mieli do czynienia z funkcjami do czytania, pisanie, otwierania i zamykania plików, sprawdzanie uprawnień, ale także wszelkim innymi funkcjami udostępnionymi przez system (jak choćby obsługa sygnałów sigprocmask) i działającymi w trybie jądra.

Jeśli chodzi o funkcje biblioteczne to mamy to do czynieniu na przykład z funkcjami bibliotecznymi C.

Jakie ważne opcje mamy do dyspozycji?

-p pid dołącz się do procesu (attach) i śledź go aż do przerwania przez CTRL-C
-e trace=set śledzenie tylko wyspecyfikowanych w zbiorze set wywołań
-e signal=set podobnie jak wyżej, ale śledzimy tylko wybrane sygnały
-f śledź także dzieci (powstałe przez fork) procesu (dopiero od momentu kiedy pid dziecka jest znany przez powrót z fork-a)

Przykład 1:

login@Kubuntu:~$ strace -o plik1 find /etc -name "passwd" /etc/pam.d/passwd find: /etc/ssl/private: Brak dostępu /etc/passwd

Jak zatem wygląda "plik1"?

execve("/usr/bin/find", ["find", "/etc", "-name", "passwd"], [/* 36 vars */]) = 0 uname({sys="Linux", node="Kubuntu", ...}) = 0 brk(0) = 0x8067000 ... open("etc", O_RDONLY|O_LARGEFILE|O_NOFOLLOW) = 4 fchdir(4) = 0 close(4) = 0 stat64(".", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("fstab", {st_mode=S_IFREG|0644, st_size=398, ...}) = 0 lstat64("X11", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 open("X11", O_RDONLY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY) = 4 fstat64(4, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 fcntl64(4, F_SETFD, FD_CLOEXEC) = 0 getdents64(4, /* 17 entries */, 4096) = 544 getdents64(4, /* 0 entries */, 4096) = 0 close(4) = 0 open("X11", O_RDONLY|O_LARGEFILE|O_NOFOLLOW) = 4 fchdir(4) = 0 close(4) = 0 stat64(".", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat64("Xresources", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 open("Xresources", O_RDONLY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY) = 4 fstat64(4, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 fcntl64(4, F_SETFD, FD_CLOEXEC) = 0 getdents64(4, /* 2 entries */, 4096) = 48 getdents64(4, /* 0 entries */, 4096) = 0 close(4) = 0 ... itd.

Przykład 2:

login@Kubuntu:~$ strace -o plik2 cat /etc/shadow cat: /etc/shadow: Brak dostępu

Jak znaleźć w "plik2" to co naistotniejsze?
(grep -C num -> wypisanie szukanych linii wraz z kontekstem wynoszącym num linii poprzednich i następnych, gdzie ,,--'' rozdziela konteksty)

login@Kubuntu:~$ cat plik2 |grep -C 1 shadow execve("/bin/cat", ["cat", "/etc/shadow"], [/* 36 vars */]) = 0 uname({sys="Linux", node="Kubuntu", ...}) = 0 -- fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 1), ...}) = 0 open("/etc/shadow", O_RDONLY|O_LARGEFILE) = -1 EACCES (Permission denied) write(2, "cat: ", 5) = 5 write(2, "/etc/shadow", 11) = 11 open("/usr/share/locale/pl_PL/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)

Przykład 3 (dwa warianty programów liczących kwadraty liczb; odpowiednio test1.c i test2.c):

#include < stdio.h> #include "kwadrat.h" int main() { int i, x, n; scanf("%d",&n); for (i = 0; i < n; i++) { x = kwadrat(i); (* test1 *) x = kwadrat(&i); (* test2 *) printf("%d\n",x); } return 0; }

Bardzo nieciekawa funkcja kwadrat:

#include "kwadrat.h" int kwadrat(int *x) { (*x) *= (*x); return *x; }

Wykonanie tych prawie poprawnych programów da nam odpowiednio:

login@Kubuntu:~$ ./test1 5 Naruszenie ochrony pamięci

oraz...

login@Kubuntu:~$ ./test1 5 0 1 4

Strace użyty do prorgamu test1:

login@Kubuntu:~$ strace ./test1 execve("./test1", ["./test1"], [/* 36 vars */]) = 0 uname({sys="Linux", node="Kubuntu", ...}) = 0 ... read(0, 5 "5\n", 1024) = 2 --- SIGSEGV (Segmentation fault) @ 0 (0) --- +++ killed by SIGSEGV +++

Gdy ltrace w tym samym przypadku zwróci:

... scanf(0x8048508, 0xbfecce50, 0xbfecce58, 0x8048433, 15 ) = 1 --- SIGSEGV (Segmentation fault) --- +++ killed by SIGSEGV +++

Równie mało informacji uzyskamy w przypadku test2:

... read(0, 5 "5\n", 1024) = 2 fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 1), ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7fc7000 write(1, "0\n", 20 ) = 2 write(1, "1\n", 21 ) = 2 write(1, "4\n", 24 ) = 2 munmap(0xb7fc7000, 4096) = 0 exit_group(0) = ?

Oraz ltrace (tym razem w całości):

__libc_start_main(0x8048394, 1, 0xbfa40bd4, 0x8048420, 0x8048471 scanf(0x8048510, 0xbfa40b40, 0xbfa40b48, 0x804843b, 15 ) = 1 printf("%d\n", 00 ) = 2 printf("%d\n", 11 ) = 2 printf("%d\n", 44 ) = 2 +++ exited (status 0) +++