====== Narzędzia pomocne przy diagnozowaniu problemów w trakcie pisania programów sieciowych ====== Lista narzędzi które mogą być przydatne przy diagnostyce dlaczego program nie działa jak powinien (z naciskiem na obsługę sieci). **Opis jest bardzo skrótowy. Jeśli mam go rozwinąć, proszę o informację które narzędzia opisać dokładniej.** Jeśli korzystacie z jakiegoś narzędzia które warto tutaj dodać, to chętnie je tutaj dopiszę. ===== Kompilacja z symbolami do debugu ===== Część opisanych tutaj narzędzi nie będzie produkować użytecznych wyników jeśli program nie zostanie zbudowany z symbolami debugowania. \\ Np. w gcc/clang należy dodać opcję ''-g'' do linii poleceń, dla cmake wybrać typ budowania Debug lub RelWithDebInfo (''cmake -DCMAKE_BUILD_TYPE=Debug //ścieżka//''). Obecność symboli do debugu można sprawdzić np. przez: \\ ''objdump -h | grep -B1 DEBUGGING'' \\ lub ''readelf -S | grep '.debug_'' \\ a także w wynikach np. ''nm'' czy ''bingrep'' W C+''''+ nazwy funkcji w trakcie kompilacji są zniekształcone (ang. mangled) tak by linker poradził sobie z namespace'ami, temple'ami i funkcjami o tej samej nazwie z różną listą parametrów.
Stąd np. funkcja:std::chrono::microseconds::count()
która po rozwinięciu typedefów nazywa się: std::chrono::duration<long, std::ratio<1l, 1000000l> >::count()
w binarce nazywa się:_ZNKSt6chrono8durationIlSt5ratioILl1ELl1000000EEE5countEv
Aby dostać z powrotem czytelną nazwę, należy użyć programu ''c+////+filt '' lub ustawić odpowiednią opcję narzędzia które wypluwa nieczytelne nazwy tak, by przekształcało je do czytelnej formy (demangling). W ''valgrind'' i ''gdb'' demangling powinien być włączony automatycznie.
===== Tracing ===== Tracing to śledzenie wykonania programu, zwykle przez wypisywanie informacji o wybranych zdarzeniach. Poniższe narzędzia można traktować jako automatyczne dodanie wypisywania na ekran wywołań funkcji systemowych / bibliotecznych. ==== strace ==== ''strace'' śledzi wywołania systemowe i wypisuje je na standardowy błąd.\\ Dla każdego wywołania pokazuje w czytelny sposób argumenty i uzyskany wynik. W programie wielowątkowym można śledzić wszystkie wątki podając opcję ''-f''. Dla analizy wywołań systemowych dla programu ''a.out'' z argumentami np. ''localhost'' i ''1234'' należy uruchomić: \\ ''strace -f ./a.out localhost 1234'' //Kiedy warto używać:// \\ Kiedy podejrzewa się problem z łączeniem / nasłuchiwaniem / odczytami / zapisami, ale nie wiadomo do końca gdzie. Uwaga: niektóre funkcje biblioteczne (np. ''getaddrinfo'') nie są jednym wywołaniem systemowym, a kodem złożonym z zera lub więcej wywołań. Uwaga: start programu to zwykle kilkadziesiąt wywołań systemowych, więc początek wyników ''strace'' można pominąć. Szukajcie np. pierwszego wywołania ''socket''. ''strace'' pozwala filtrować jakie wywołania was interesują (szczegóły w ''man strace''). ++++ Fragment ''strace -f nc localhost 1313'':| read(3, "se\t2433/tcp\t\t\t# tcp side effects"..., 4096) = 4096 read(3, "2/tcp\t\t\t# XMPP Client Connection"..., 4096) = 4096 read(3, "\nsctp-tunneling\t9899/udp\ndomaint"..., 4096) = 3904 close(3) = 0 socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 3 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 setsockopt(3, SOL_SOCKET, SO_REUSEPORT, [1], 4) = 0 rt_sigaction(SIGALRM, {sa_handler=SIG_IGN, sa_mask=[ALRM], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7efe059b58f0}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0 alarm(0) = 0 rt_sigprocmask(SIG_BLOCK, NULL, [], 8) = 0 connect(3, {sa_family=AF_INET, sin_port=htons(1313), sin_addr=inet_addr("127.0.0.1")}, 16) = 0 rt_sigaction(SIGALRM, {sa_handler=SIG_IGN, sa_mask=[ALRM], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7efe059b58f0}, {sa_handler=SIG_IGN, sa_mask=[ALRM], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7efe059b58f0}, 8) = 0 alarm(0) = 0 select(16, [0 3], NULL, NULL, NULL) = 1 (in [3]) read(3, "wto, 5 sty 2021, 23:30:32 CET\n", 8192) = 30 write(1, "wto, 5 sty 2021, 23:30:32 CET\n", 30wto, 5 sty 2021, 23:30:32 CET ) = 30 select(16, [0 3], NULL, NULL, NULL) = 1 (in [3]) read(3, "", 8192) = 0 close(3) = 0 ++++ ==== ltrace ==== ''ltrace'' śledzi wywołania biblioteczne i wypisuje je na standardowy błąd.\\ Dla każdego wywołania pokazuje argumenty i uzyskany wynik (ale nie tłumaczy co oznaczają te argumenty). Użycie i zastosowanie podobnie jak dla strace. ++++ Fragment ''ltrace -f nc 0 1313'':| [pid 15807] inet_aton("localhost", { 0x7f39 }) = 0 [pid 15807] gethostbyname("localhost") = 0x7f39d43fbea0 [pid 15807] strncpy(0x55d132de24c0, "localhost", 254) = 0x55d132de24c0 [pid 15807] inet_ntoa({ 0x100007f }) = "127.0.0.1" [pid 15807] strncpy(0x55d132de25c0, "127.0.0.1", 24) = 0x55d132de25c0 [pid 15807] inet_ntoa({ 0x100007f }) = "127.0.0.1" [pid 15807] strncpy(0x55d132de25d8, "127.0.0.1", 24) = 0x55d132de25d8 [pid 15807] strchr("1313", '-') = nil [pid 15807] strtol(0x7ffc76e3cb2f, 0, 10, 0x7ffc76e3abe8) = 1313 [pid 15807] getservbyport(8453, "tcp") = 0x7f39d43fc1c0 [pid 15807] strncpy(0x55d132de2460, "xtel", 64) = 0x55d132de2460 [pid 15807] __sprintf_chk(0x55d132de24a0, 1, 8, 0x55d1326058d9) = 4 [pid 15807] getservbyport(8453, "tcp") = 0x7f39d43fc1c0 [pid 15807] strncpy(0x55d132de2460, "xtel", 64) = 0x55d132de2460 [pid 15807] __sprintf_chk(0x55d132de24a0, 1, 8, 0x55d1326058d9) = 4 [pid 15807] __errno_location() = 0x7f39d42416c8 [pid 15807] socket(2, 1, 6) = 3 [pid 15807] setsockopt(3, 1, 2, 0x7ffc76e3aa24) = 0 [pid 15807] setsockopt(3, 1, 15, 0x7ffc76e3aa24) = 0 [pid 15807] signal(SIGALRM, 0x1) = 0 [pid 15807] alarm(0) = 0 [pid 15807] __sigsetjmp(0x55d132807760, 1, 0x7ffc76e3a800, 0x7f39d430a267) = 0 [pid 15807] connect(3, 0x55d132dde2c0, 16, 0) = 0 [pid 15807] signal(SIGALRM, 0x1) = 0x1 [pid 15807] alarm(0) = 0 [pid 15807] __fdelt_chk(3, 0x55d132de24c0, 0x55d132de25c0, 0) = 0 [pid 15807] __errno_location() = 0x7f39d42416c8 [pid 15807] __fdelt_chk(3, 0x55d132de24c0, 0x55d132de2340, 9) = 0 [pid 15807] select(16, 0x55d132de23d0, 0, 0) = 1 [pid 15807] __fdelt_chk(3, 0x55d132de23d0, 0, 0x7f39d4333c26) = 0 [pid 15807] read(3, "wto, 5 sty 2021, 23:29:26 CET\n", 8192) = 30 [pid 15807] write(1, "wto, 5 sty 2021, 23:29:26 CET\n", 30wto, 5 sty 2021, 23:29:26 CET ) = 30 [pid 15807] __fdelt_chk(3, 0x55d132de0330, 30, 0x7f39d432d333) = 0 [pid 15807] select(16, 0x55d132de23d0, 0, 0) = 1 [pid 15807] __fdelt_chk(3, 0x55d132de23d0, 0, 0x7f39d4333c26) = 0 [pid 15807] read(3, "", 8192) = 0 [pid 15807] __fdelt_chk(3, 0x55d132de0330, 8192, 0x7f39d432d28e) = 0 [pid 15807] __fdelt_chk(3, 0x55d132de0330, 0x55d132de2340, 1) = 0 [pid 15807] close(3) = 0 [pid 15807] close(3) = -1 [pid 15807] exit(0 ++++ ==== bpftrace ==== Dla zaawansowanych. Śledzenie dynamiczne. Wymaga roota. \\ Więcej na: https://github.com/iovisor/bpftrace i https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md Wypisywanie każdego wywołania funkcji ''nazwa_funkcji'' z ''programu'' wraz z pierwszym argumentem (nazwa funkcji //w binarce//, więc np. ''_ZN3foo3barEi'' a nie ''namespace foo { void bar(int baz); }''): \\ ''# bpftrace -e 'uprobe:/pełna/ścieżka/do/programu:nazwa_funkcji {printf("Foo: %ld\n", arg0);}' '' \\ ''$ /pełna/ścieżka/do/programu argument1 argument2'' Wypisywanie wyników każdego reada wywoływanego przez program o nazwie ''programu''): # bpftrace -e 'tracepoint:syscalls:sys_enter_read /comm == "programu"/ {@s[tid]=args->buf;} tracepoint:syscalls:sys_exit_read /comm == "programu"/ { if (args->ret==-1) { printf("Read failed\n"); } else { printf("Read of %ld bytes: |%s|\n", args->ret, str(@s[tid], args->ret)); } }' $ /pełna/ścieżka/do/programu argument1 argument2 ===== Profilowanie ===== Profilowanie (profiling) to zbieranie metadanych o działaniu programu (w trakcie jego działania) dla zaprezentowania statystyk np. zużycia procesora, pamięci, wystąpienia konkretnych zdarzeń (np. cache miss), … w konkretnych liniach, funkcjach, stosach wywołań, … . Profilowanie zwykle jest wykonywane dla dowiedzenia się co zjada zasoby lub do optymalizacji kodu. ==== perf ==== ''perf'' jest rozbudowanym narzędziem m. inn. do profilowania kodu w Linuksie. \\ Bez uprawnień roota lub ustawień kernela pozwalających zwykłemu użytkownikowi profilować kod działa w ograniczonym zakresie. Wywołanie (dla programu ''a.out'' z argumentami ''localhost'' i ''1234'') składa się z dwóch etapów – uruchomienia programu połączonego ze zbierania danych i analizy:\\ ''perf record --call-graph=dwarf ./a.out localhost 1234'' \\ ''perf report'' //Kiedy warto używać:// \\ Kiedy program nadużywa procesora, a nie powinien. Wynik programu ''perf'' można przetworzyć na [[https://github.com/brendangregg/FlameGraph| flame graph]]. \\ ''perf'' może zbierać informacje o innych zdarzeniach niż cykle procesora – wykonaj ''perf list''. ===== Zarządzanie pamięcią ===== ==== valgrind ==== ''valgrind'' (a dokładniej memcheck tool z valgrinda) śledzi dostępy, zajmowanie i zwalnianie pamięci i sprawdza poprawność każdego dostępu i zwalniania pamięci. \\ Jeśli jakaś operacja jest niepoprawna (dostęp poza przydzieloną pamięć, zwalnianie niezaalokowanej pamięci etc.) to opisuje na czym on polega i gdzie została wywołana (stack trace) i //nie// przerywa działania program jeśli to możliwe. \\ Po wykonaniu programu ''valgrind'' sprawdza czy cała zajęta pamięć została zwolniona, a jeśli nie to raportuje ile jej jest i gdzie została zaalokowana. Wywołanie (dla programu ''a.out'' z argumentami ''localhost'' i ''1234''):\\ ''valgrind ./a.out localhost 1234'' //Kiedy warto używać:// \\ Kiedy program kończy się segmentation fault lub w wynikach pojawiają się "śmieci" z RAMu. \\ Poza tym zawsze jeśli używa się dynamicznej alokacji pamięci dla sprawdzenia czy program nie ma memleaków. Przykłady: https://www.valgrind.org/docs/manual/mc-manual.html ===== Debugging ===== ==== gdb ==== ''gdb'' jest standardowym debuggerem w Linuksie. Zwykle prostych zadaniach wystarczy użycie gdb z IDE. \\ Tutaj krótko spisałem ważniejsze polecenia konsoli gdb. Wywołanie (dla programu ''a.out'' z argumentami ''localhost'' i ''1234''):\\ ''gdb --args ./a.out localhost 1234''\\ ''(gdb) ………'' Wywołanie (dla pliku zrzutu pamięci ''core_dump''' programu ''a.out''):\\ ''gdb -c core_dump ./a.out ''\\ ''(gdb) ………'' Pomoc do polecenia to ''help ''.\\ Podstawowe polecenia to:\\ Łapanie: ''(b)reak '' ''catch '' ''watch '' ''condition '' \\ Listowanie break/watch-pointów: ''info breakpints/watchpoints'' \\ Usuwanie break/watch-pointów: ''del break/watch'' \\ Uruchomienie programu: ''start'' ''(r)un'' ''kill'' \\ Komendy do kolejnych kroków: ''(n)ext'' ''(s)tep'' ''(fin)ish'' ''(u)ntil'' ''(c)ontinue''\\ Wypisywanie informacji: ''print '' ''whatis '' ''ptype '' ''info arg'' ''list'' ''x''\\ Stos: ''backtrace'' ''frame '' \\ Wątki: ''info threads'' ''thread '' //Kiedy warto używać:// \\ Kiedy program nie działa jak powinien i nie wiecie dlaczego – należy ustawić breakpointy i sprawdzać do którego momentu program działa zgodnie z oczekiwaniami. \\ Kiedy program wygeneruje zrzut pamięci (core dump) i chcecie wiedzieć dlaczego uległ awarii. ===== Analizatory statyczne ===== Analiza statyczna to wnioskowanie o programie na podstawie kodu programu, bez uruchamiania go. Przez swoje ograniczenia (działanie tylko na kodzie) pozwala na znalezienie tylko pewnej grupy błędów, natomiast nie wymaga żeby te błędy się wydarzyły w trakcie uruchomienia. //Kiedy warto używać:// \\ Zawsze dla sprawdzenia czy kod nie ma błędów. ==== kompilator ==== Kompilatory z włączonymi ostrzeżeniami czasami wskazują gdzie mógł zostać popełniony jakiś typowy błąd. Należy włączyć i czytać te ostrzeżenia. ==== scan-build z llvm ==== ''scan-build'' to analizator statyczny dla C/C+''''+ "w pakiecie" z clang i LLVM. Dla przykładowego użycia sprawdź: https://clang-analyzer.llvm.org/scan-build.html Jeśli scan-build znajdzie problemy na etapie analizy statycznej, wygeneruje raport co i kiedy może pójść nie tak. ==== cppcheck ==== ''cppcheck'' to w miarę sensowny analizator statyczny dla C+''''+. Przykładowe użycie – analizuje pliki w bieżącym katalogu:\\ ''cppcheck-gui ./'' ===== Sieć ===== Analiza tego co faktycznie dzieje się w sieci pomaga w określeniu np. czy klient generuje błędne wiadomości czy też serwer źle przetwarza prawidłowe wiadomości. ==== wireshark ==== ''wireshark'' pokazuje wszystkie ramki przechodzące przez podany interfejs. Konsolowym odpowiednikiem jest ''tcpdump''. \\ ''tcpdump'' może zapisać plik który później da sie przeglądać w wiresharku: ''tcpdump -w -i ''. //Kiedy warto używać:// \\ Kiedy coś jest nie tak z komunikacją, szczególnie do porównania czy przesyłane dane wyglądają tak jak tego oczekuje programista.