Dydaktyka:
FeedbackTo jest stara wersja strony!
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ę.
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 <prog> | grep -B1 DEBUGGING
lub readelf -S <prog> | 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 |
c++filt <nazwa>
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 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
ś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
ś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 <no return ...>
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 (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
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 flame graph.
perf
może zbierać informacje o innych zdarzeniach niż cykle procesora – wykonaj perf list
.
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
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 <polecenie>
.
Podstawowe polecenia to:
Łapanie: (b)reak <expr>
catch <what>
watch <expr>
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 <expr>
ptype <expr>
info arg
list
x
Stos: backtrace
frame <nr>
Wątki: info threads
thread <nr>
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.
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.
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
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
to w miarę sensowny analizator statyczny dla C++.
Przykładowe użycie – analizuje pliki w bieżącym katalogu:
cppcheck-gui ./
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
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 <plik> -i <interfejs>
.
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.