Warning: Undefined array key "userinfo" in /home/pp/.public_html/lib/plugins/indexmenu/action.php on line 54
Wykrywanie błędów programu i wycieków pamięci za pomocą narzędzia Valgrind [Podstawy Programowania]
Warning: Undefined array key "stylesheets" in /home/pp/.public_html/inc/StyleUtils.php on line 102


Podstawy Programowania

Instytut Informatyki Stosowanej, Politechnika Łódzka

Narzędzia użytkownika

Narzędzia witryny


Wykrywanie błędów programu i wycieków pamięci za pomocą narzędzia Valgrind

Autor: Mateusz Nowak, student II roku, kontakt: 209409@edu.p.lodz.pl

Programy tworzone w ramach Podstaw Programowania 2 będą przechodzić przez dodatkowe testy. Ma to na celu przygotowanie Studentów do pisania wydajnych programów, poprawnie zarządzających pamięcią.

Podczas pracy danej aplikacji może być konieczne przechowywanie ogromnych struktur danych. Przygotowanie dużej tablicy już na etapie tworzenia programu nie jest dobrym rozwiązaniem, ponieważ w ten sposób marnujemy pamięć kiedy jej nie używamy. Dodatkowo, ze względu na ograniczony rozmiar stosu programu, taka operacja tworzenia może się po prostu zakończyć niepowodzeniem.

Rozsądnym rozwiązaniem wydaje się więc wydzielenie obszaru pamięci sterty, wykonanie operacji na danych, a następnie zwolnienie zarezerwowanego bloku. Niestety, często zdarza się, że zapominamy o tym lub wykonujemy to w niewłaściwy sposób. Niepoprawne działanie programu wpływa na ogólną kondycję systemu operacyjnego. W zależności od wewnętrznej polityki zarządzania pamięcią, może rozpocząć się wymiana danych na dysk twardy, zamykanie innych programów (albo programu-winowajcy), znaczące spowolnienie lub całkowite zawieszenie się systemu.

Dostępne rozwiązania

Jest wiele możliwości monitorowania zużycia pamięci. Zaawansowane środowiska programistyczne, takie jak Visual Studio, podczas debugowania programu wyświetlają wszystkie żądane informacje:

Jest także możliwość generowania odpowiedniego raportu z poziomu linii komend. W systemie testów maszynowych wykorzystywany jest Memcheck - składnik pakietu Valgrind, służacego do profilowania (monitorowania i optymalizacji) programów. Więcej informacji o dostępnych narzędziach dostępne jest na oficjalnej stronie. Warto wspomnieć, że pakiet ten nie jest dostępny dla systemu Windows. Można jednak ten problem rozwiązać, używając wirtualnej maszyny albo dostępnego w nowych kompilacjach 64-bitowego Windows 10, komponentu Windows Subsystem For Linux.

Rozpoczęcie pracy z narzędziem

Zakładamy, że został już utworzony program w języku C (C++ też jest wspierany), i chcemy go przetestować. W pierwszej kolejności należy go skompilować z flagami debugowania:

$ gcc -g nazwaPliku.c -o program

Powstanie wynikowy plik program. Aby sprawdzić jego działanie, wystarczy wpisać w terminalu:

$ ./program

Jeśli program nie działa poprawnie, pojawią się odpowiednie komunikaty, np. „Aborted” („Zatrzymano”) lub „Segmentation fault” („Naruszenie ochrony pamięci”), i jego praca zostanie zatrzymana. Znalezienie źródła problemu jest możliwe dzięki następującej komendzie:

$ valgrind --leak-check=full ./program

Domyślnie używanym narzędziem jest Memcheck, więc nie musimy uwzględniać tego w poleceniu. W dalszej części dokumentu, mówiąc „Valgrind”, będziemy mieli na myśli ten konkretny komponent.

Wyjście programu składa się z informacji o samym narzędziu, błędach w testowanym programie (ich rodzaj i źródło) i podsumowaniu zużytej pamięci. Aby dokładnie opisać działanie Valgrinda, utworzony został prosty program naszpikowany błędami. Będziemy likwidować je krok po kroku, co opatrzone będzie dużą ilością zrzutów ekranu.

Przebieg pracy z narzędziem

Oto nasz przykładowy program:

Niepoprawne wartości funkcji

W przypadku niektórych funkcji bibliotecznych, pewne wartości nie mają sensu. Niemożliwe jest na przykład utworzenie bloku w pamięci o ujemnym rozmiarze. Takie błędy są wykrywane i odpowiednio opisane.

Oto wynik działania programu. Linie zaczynające się od ==numer==, to wynik działania Valgrinda:

Rozwiązywanie błędów najlepiej zacząć od pierwszego z nich.

W tym przypadku wartość używana przez funkcję malloc() jest ujemna (-4). Rzeczywiście, w kodzie źródłowym, w linii 10. znajduje się niepożądany znak:

*(bar + i) = (int*)malloc(-sizeof(int));

Po usunięciu błędu należy skompilować program i uruchomić poprzez narzędzie jeszcze raz.

Nieprawidłowy zapis/odczyt

Aktualna wersja kodu:

Raport z Valgrinda:

Teraz zajmijmy się błędem „Invalid write of size 8” w linii 7 (możliwy jest również błąd „Invalid read of size…”)

Valgrind pozwala na wykrywanie zapisu i odczytu danych w niedozwolonych miejscach. Błąd jest trudniejszy do wykrycia, ale ostatecznie okazuje się, że użyliśmy sizeof(int) zamiast sizeof(int*). Docelowo chcieliśmy utworzyć blok 64 wskaźników do zmiennej typu int, zamiast bloku 64 zmiennych typu int. Co ciekawe, ten błąd nie wystąpi, jeśli program bedzie skompilowany dla 32-bitowych systemów (ponieważ w takim przypadku int oraz int* byłyby takich samych rozmiarów).

Nieprawidłowe alokacja/zwalnianie pamięci

Aktualna wersja kodu:

Raport z Valgrinda:

Pierwszy z błędów:

W tym przypadku do funkcji free() został wysłany niepoprawny wskaźnik na blok danych. Jest on przesunięty o 24 bajty w prawo, względem początku 512-bajtowego bloku („Address 0x5204058 is 24 bytes inside…”). Aby zaprezentować dodatkowe możliwości Valgrinda, tymczasowo usuniemy niepoprawną linię kodu.

Brak zwalniania pamięci

Jeżeli program nie został zakończony przedwcześnie (np. z powodu naruszenia ochrony pamięci), na końcu raportu powinny znajdować się informacje o zaalokowanej pamięci.

Aktualna wersja kodu:

Podsumowanie:

Oto możliwe rodzaje wycieków:

  • Definitely lost – istnieje co najmniej jeden wskaźnik na pierwszy element bloku pamięci. Błąd wystąpi jeżeli nie użyjemy funkcji free(), najpóźniej na końcu działania programu. W przedstawionym przykładzie utworzono blok 64 wskaźników. Ponieważ wskaźniki mają rozmiar 8 bajtów, to 8 * 64 = 512.
  • Indirectly lost – pod koniec działania programu istnieją zaalokowane bloki pamięci, na które nie wskazuje żaden wskaźnik. Są nazwane „lost”, ponieważ programista przy kończeniu pracy programu nie ma już możliwości ich zwolnić. Tutaj zostały zaalokowane 64 bloki po 4 bajty = 256 bajtów.
  • Possibly lost – istnieją bloki pamięci, w przypadku których Valgrind nie jest w stanie określić, czy są still reachable czy definitely lost.
  • Still reachable – istnieją wskaźniki na pewien fragment bloku danych, ale nie na jego początek (więc nie można ich bezpośrednio użyć np. w funkcji free()).

W każdym przypadku podana jest ilość operacji alokowania i zwalniania pamięci. W ten sposób można sprawdzić, czy nie alokujemy/dealokujemy zbyt wiele razy.

Niezamknięte uchwyty do zasobów

Jeżeli w trakcie pracy programu zostanie otworzony uchwyt do zewnętrznego zasobu (np. pliku), a nie zostanie poprawnie zamknięty, również pojawią się odpowiednie informacje. W razie wątpliwości, można uruchomić Valgrinda z parametrem –track-fds=yes:

W powyższym przykładzie:

  • deskryptor pliku 0 - standardowe wejście
  • deskryptor pliku 1 - standardowe wyjście
  • deskryptor pliku 2 - standardowe wyjście błędów
  • deskryptor pliku 3 - plik z raportem Valgrinda
  • deskryptor pliku 4 - otwarty przykładowy plik

Finalna wersja programu

Raport z Valgrinda:

Jak widać, nasz program działa teraz bezbłędnie.

Podsumowanie

W analizowanym przykładzie nie udało się zawrzeć wszystkich możliwych błędów. Mamy jednak nadzieję że w odpowiedni sposób pokazaliśmy, jak duże możliwości mają narzędzia profilujące.

Należy mieć na uwadze fakt, że Valgrind nie jest jednak w stanie wykrywać wszystkich błędów. Większości z nich można się pozbyć już na poziomie kompilacji. Dlatego warto czytać wszystkie komunikaty kompilatora - również ostrzeżenia. Niestety, błędy logiczne które nie wpływają na stabilność działania programu, są często niemożliwe do wychwycenia bez angażowania w to programisty lub, co gorsza, końcowego użytkownika.

mater/valgrind.txt · ostatnio zmienione: 11/03/2018 18:50 (edycja zewnętrzna)