Sposoby wykrywania i
usuwania błędów
Mylić się jest rzeczą ludzką
2
Typy błędów:
błędy specyfikacji: źle określone wymagania
błędy projektowe: nieodpowiednie struktury danych i algorytmy błędy kodu: powstają na etapie realizacji projektu przy pomocy
wybranego narzędzia programistycznego
Etapy debugowania, poprawiania programu:
testowanie: znalezienie błędu
lokalizacja: zidentyfikowanie błędnego kodu korekta: poprawienie błędu
weryfikacja: sprawdzenie czy poprawka działa
Program z błędem:
W pliku debug1.c znajduje się funkcja sort, która ma sortować
struktury typu item metodą bąbelkową. Niestety po
Sposoby testowania
3 Przeglądanie kodu
Podstawowym sposobem wykrywania błędów jest przeglądanie kodu. Najlepiej, gdy nasz kod przejrzy inna osoba. Do wykrywania błędów składniowych możemy użyć kompilatora:
gcc -Wall -pedantic -ansi -o debug1 debug1.c
Oprzyrządowanie kodu
Oprzyrządownie polega na dodawaniu nowego kodu do testowanego programu w celu zebrania jak największej liczby informacji o
programie. Wykorzystuje się do tego preprocesor C, pozwalający
decydować podczas kompilacji, czy dołączyć kod wykorzystywany do debugowania. Np.:
#ifdef DEBUG
printf("debug: x=%d\n", x); #endif
powyżej printf wykona się, gdy program będzie skompilowany z
4
cd
Sposoby testowania
Oprzyrządowanie kodu bez ponownej kompialcji
Wadą poprzedniego rozwiązania była konieczność ponownej
kompilacji przed każdym debugowaniem. Alternatywnymi sposobami umożliwiania debugowania kodu przez użytkownika są:
Program gdb
gdb jest programem umożliwiającym śledzenie wykonywania innych
programów. Śledzony program powinien być skompilowany z opcją
-g by do kodu wynikowego dołączyć informacje istotne dla
debugera. gcc -g -o debug3 debug3.c gdb debug3
komenda (gdb) help wyświetla dostępne komendy
(patrz: man gdb i info gdb).
Obsługa przez debugowany program opcji -d podawanej z linii
komend (trzeba ją oprogramować samemu)
Użycie w programie możliwości rejestracji zachowania programu przez funkcję syslog (patrz man 3 syslog)
5
Program
gdb
gdb uruchamianie
Rozpoczynamy wykonanie programu komendą (gdb)run. Argumenty
polecenia (gdb)run zostaną przekazane testowanemu programowi.
W naszym przypadku pojawi się błąd segmentacji wraz z linią, w której po raz pierwszy ten błąd wystąpił.
gdb śledzenie stosu
Komenda (gdb)backtrace pozwala śledzić stos wywołań aż do
miejsca, w którym jesteśmy. Używane do programów z większą liczbą wywołań podprogramów i funkcji.
gdb badanie zmiennych
Wypisywania wartości zmiennych: (gdb)print zmienna.
Rezultaty są przechowywane w pseudozmiennych $<liczba>,
ostatni rezultat to $, a przedostatni $$.
Sprawdzamy (gdb)print j, problem sprawiło: a[4]=a[4+1].
Spróbuj: (gdb)print a[$-1].klucz lub print a[3]
6
Program
gdb
cd
gdb listing programu
Polecenie (gdb)list wypisuje fragment programu wokół bieżącej
pozycji. Widzimy, że zmienna j nie powinna przyjmować wartości 4.
Zmieńmy więc linię 23. na następującą:
/* 23 */ for( j=0; j<n-1; j++ ) {
Po kompilacji i uruchomieniu (patrz debug4.c) program działa ale wynik nie jest niepoprawny. Sprawdźmy co robi w trakcie dziłania.
gdb puntky przerwania
gdb posiada komendy pozwalającę wstawiać i kontrolować tzw.
punkty przerwania. Zobacz (gdb)help breakpoint.
My wstawimy punkt przerwania w linii 21.: (gdb)break 21
uruchomimy program (gdb)run. Program zatrzyma się w na linii 21.
Wypisujemy stan tablicy: (gdb)print tablica[0]@5 i
7
Program
gdb
jeszcze
Wypisanie stanu tablicy za każdym razem, gdy program się zatrzyma:
(gdb)display tablica[0]@5. Używając komendy (gdb)
commands. ustalamy, że program ma wykonać cont za każdym
razem, gdy dojdzie do bieżącego punktu przerwania.
Analiza zachowania programu doprowadza nas do wniosku, że zewnętrzna pętla nie wykonuje się tyle razy ile powinna.
Podejrzewamy, że winna temu jest linia 31. Spróbujmy to sprawdzić.
gdb łatanie
Łatą nazywamy kod, który musimy dodać do programu aby działał
poprawnie. Stosując punkty przerwania możemy sprawdzić łatę zanim zmienimy kod źródłowy.
Wyłączamy poprzednio ustawiony punkt przerwania i związane z nim wyświetlanie: (gdb)disable break 1 i
(gdb)disable display 1. Ustawiamy przerwanie w linii 31. i
kojarzymy z nim komendy: set variable n = n+1 i cont.
8
Program
patch
Dystrybucja nowych wersji programów jest kłopotliwa (zwłaszcza, gdy dostarczamy wersje binarne). Oprogramowanie typu Open
Source znacznie ułatwia dystrybucję nowych wersji. Zamiast
udostępniać nową wersję w pełnej (wielo-MB) postaci, udostępnia się jedynie różnice pomiędzy wersjami. Program diff służy do
tworzenia plików zawierających różnice między źródłami wersji.
diff plik1.txt plik2.txt > ró?nice.txt
Łatanie programu, tj. uaktualnianie źródeł można wykonać tak:
patch plik1.txt ró?nice.txt Odwracanie tego procesu:
patch -R plik1.txt ró?nice.txt
Zadania:
1. Zrobić łatę/łaty dla przykładu z gdb
2. Zrobić łatę do programu składającego się z wielu plików -
9
Testy pokrycia
Jedynym sposobem potwierdzenia poprawności programu jest
udowodnienie, że dla każdej możliwej wartości danych wejściowych program zwraca poprawny wynik. Dla większości programów, z
wyjątkiem najprostszych, jest to zadanie tak skomplikowane, że praktycznie jest niewykonalne.
Kompromisem nieograniczającym zakresu testów są tzw. testy
pokrycia. Idea testów pokrycia polega na próbie oszacowania, jaka część kodu została wykonana podczas testów. Jeżeli w czasie testów wykonana została każda część programu, ich wyniki uznamy za
bardziej godne zaufania.
Istnieją trzy rodzaje testów pokrycia: Pokrycie instrukcji
Pokrycie rozgałęzień programu Pokrycie danych
10
Pokrycie instrukcji
Pokrycie instrukcji polega na sprawdzeniu czy podczas testów została wykonana każda linijka programu. Pokrycie instrukcji ma wadę: nie bierze się w nim pod uwagę wzajemnego oddziaływania części programu. Przykład:
1 :int f(int a,int b){ 2 : int r = 1; 3 : if(a>0){ 4 : r = 0; 5 : }; 6 : if(b>0){ 7 : r = 3/r; 8 : } 10: return r; 11:} Linie 1, 2, 3, 6 i 10 są testowane
zawsze. Aby pokryć linię 4 należy
przetestować f(1,0), natomiast by
pokryć linię 7, f(0,1). Teraz nasz
test pokrywa już wszystkie instrukcje programu.
Co się jednak stanie gdy sprawdzimy wywołanie f(1,1)?
11
Pokrycie rozgałęzień i danych
Test pokrycia rozgałęzień programu polega na rozważeniu
wszystkich możliwych ścieżek działań programu. Liczba możliwych ścieżek programu znacznie wzrasta w miarę dodawania pętli i
instrukcji warunkowych. Powoduje to także wzrost liczby testów do ich pełnego pokrycia.
Test pokrycia danych obejmuje testowanie każdej możliwej kombinacji użytych danych.
Istnieje kilka narzędzi do badania stopnia pokrycia badanego programu za pomocą przeprowadzonych testów. Narzędzia te
pracują na zasadzie zwbogacania testowanego programu w trakcie kompilacji. Dodatkowy kod służy do gromadzenia informacji o tym, które instrukcje były wykonywane i jak często.
Ponieważ narzędzia te działają na poziomie instrukcji, kod programu powinien być tak napisany by każda linia zawierała najwyżej jedną istrukcję. Złym rozwiązaniem jest umieszczanie instrukcji
warunkowej w jednej linii lub stosowanie makr zawierających wyrażenia warukowe.
12
Narzędzie
gcov
gcov jest narzędziem do testowania pokrycia instrukcji programu.
Aby korzystać z gcov, należy przygotować specjalną wersję badanej
aplikacji (podobnie jak dla gdb). Do przygotowania kodu musimy użyć
kompilatora C w wersji GNU ze specjalnymi opcjami linii poleceń: -fprofile-arcs zmusza kompilator do umieszczania w
testowanym programie dodatkowego kodu, pozwalającego
rejestrować, która instrukcja jest wykonywana. Informacja taka będzie zapisywana w pliku o nazwie takiej jak plik źródłowy z końcówką .da
-ftest-coverage prócz pliku z kodem obiektowym (.o)
powstaną pliki o końcówkach .bb i .bbg, zawierające zapis
struktury rozgałęzień kodu źródłowego. Używane są przez gcov do
tworzenia mapy działania programu.
-fbranch-probabilities opcja ta powoduje optymalizację
Narzędzie
gcov
13
przykład
W katalogu testy znajduje się przykładowa aplikacja obliczająca wyrażenia arytmetyczne zadane w Odwrotnej Notacji Polskiej. Aplikacja składa się z kilku plików zawierających funkcje
zewnętrzne, pliku Makefile oraz plików test[0-6] zawierających testowe wyrażenia.
Przykładowy przebieg testów: Kompilacja aplikacji:
$ make
Wykonanie testów:
$ make test
Wykonanie analiz pokrycia:
$ make gcov
Wykonanie kasowania liczników:
$ make clean_da
Analizując testy aplikacji należy szczególną uwagę zwrócić na:
Opcje polecenia gcov
Wpływ plików *.da na
zawartość plików *.c.gcov
Analizę rozgałęzień kodu
(patrz opcja -b polecenia gcov
i instrukcje rozgałęziające: if, case, for, while)
14
Testowanie wydajności
Ważnym aspektem testowania jest wydajność. Aplikacja prócztego, że musi działać poprawnie musi być użyteczna, czyli oddawać wyniki w rozsądnym czasie. Stąd ważne jest znajdowanie w programie
takich miejsc, w których traci się najwięcej czasu.
Narzędziem, które może nam pomóc w testowaniu wydajności jest
gprof. Aby przygotować kod programu dla gprof należy go
skompilować z opcją -pg, następnie uruchomić program i po nim
wywołać gprof z nazwą programu. Przykładowe wywołania w
plikach Makefile w katalogach testy i testy2.
Po uruchomieniu programu powstaje plik gmon.out, zawierający
zapis profilu działania. Program w trakcie działania zapisał w nim wyniki pomiarów czasu spędzonego w każdej z funkcji.
Program gprof może gromadzić dane z wielu uruchomień badanego
programu. Aby skorzystać z tej możliwości, należy użyć opcji -s w
wywołaniu gprof. Informacja o profilu będzie wówczas gromadzona