gdb

Transkrypt

gdb
Sposoby wykrywania i
usuwania błędów
Tomasz Borzyszkowski
Mylić się jest rzeczą ludzką
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
uruchomieniu otrzymujemy błąd segmentacji.
2
Sposoby testowania
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
Patrz debug2.c
opcją -DDEBUG.
3
Sposoby testowania cd
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ą:
 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)
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
gcc -g -o debug3 debug3.c
debugera.
gdb debug3
komenda (gdb) help wyświetla dostępne komendy
(patrz: man gdb i info gdb).
4
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]
(gdb)print a[$-1]
5
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
kontynuujemy do natępnego przerwania (gdb)cont.
6
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.
Po uruchomieniu program zadziała poprawnie.
7
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 uwzględnić łatanie w pliku Makefile
8
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
9
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){ Linie 1, 2, 3, 6 i 10 są testowane
2 : int r = 1;
zawsze. Aby pokryć linię 4 należy
3 : if(a>0){
przetestować f(1,0), natomiast by
4 :
r = 0;
pokryć linię 7, f(0,1). Teraz nasz
5 : };
test pokrywa już wszystkie instrukcje
6 : if(b>0){
programu.
7 :
r = 3/r;
Co się jednak stanie gdy sprawdzimy
8 : }
wywołanie f(1,1)?
10: return r;
11:}
Ze względów formalnych należy także przetestować f(0,0).
10
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.
11
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ę
śledzenia rozgałęzień.
12
Narzędzie gcov 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)
13
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
w pliku gmon.sum.
14

Podobne dokumenty