Kategorie
Materiały
Linki zewnętrzne
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.
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.
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.
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.
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).
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.
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:
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.still reachable
czy definitely lost
.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.
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:
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.