Dydaktyka:
FeedbackLista 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>
condition <num> <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>
whatis <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.