Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00662 008556 10998292 na godz. na dobę w sumie
Znajdź błąd. Sztuka analizowania kodu - książka
Znajdź błąd. Sztuka analizowania kodu - książka
Autor: Liczba stron: 288
Wydawca: Helion Język publikacji: polski
ISBN: 83-7361-855-4 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> programowanie >> inne - programowanie
Porównaj ceny (książka, ebook, audiobook).

Wyszukiwanie błędów w kodzie to czynność, którą programiści wykonują niemal równie często, jak pisanie kodu. Narzędzia do wykrywania i poprawiania błędów tylko częściowo rozwiązują problem. W wielu przypadkach błąd nie tkwi w nieprawidłowo sformułowanym poleceniu lub źle zdefiniowanej zmiennej, ale w miejscu, którego nawet najlepsze narzędzie nie znajdzie. Programista musi się nauczyć samemu bronić przed ukrytymi pomyłkami i nieprzyjemnymi niespodziankami. Błędy trzeba znaleźć, zanim one znajdą nas.

Książka 'Znajdź błąd. Sztuka analizowania kodu' to zbiór 50 programów napisanych w językach Perl, C, Java, Python i asembler x86. Każdy z nich zawiera jeden, trudny do znalezienia, ale jak najbardziej realistyczny błąd. Wykrycie go wymaga przewidzenia sposobu, w jaki program będzie wykonywany, i prześledzenia krok po kroku jego działania. Każdy przykład opatrzony jest wskazówkami pomocnymi przy wyszukiwaniu błędów. Książka przedstawia sposoby analizowania programów i przewidywania miejsc, w których może wystąpić błąd.

Wykonując zadania zawarte w tej książce, nie tylko nauczysz się odnajdywać błędy, ale także udoskonalisz swoje umiejętności w zakresie pisania aplikacji.

Znajdź podobne książki Ostatnio czytane w tej kategorii

Darmowy fragment publikacji:

IDZ DO IDZ DO PRZYK£ADOWY ROZDZIA£ PRZYK£ADOWY ROZDZIA£ SPIS TREĎCI SPIS TREĎCI KATALOG KSI¥¯EK KATALOG KSI¥¯EK KATALOG ONLINE KATALOG ONLINE ZAMÓW DRUKOWANY KATALOG ZAMÓW DRUKOWANY KATALOG TWÓJ KOSZYK TWÓJ KOSZYK DODAJ DO KOSZYKA DODAJ DO KOSZYKA CENNIK I INFORMACJE CENNIK I INFORMACJE ZAMÓW INFORMACJE ZAMÓW INFORMACJE O NOWOĎCIACH O NOWOĎCIACH ZAMÓW CENNIK ZAMÓW CENNIK CZYTELNIA CZYTELNIA FRAGMENTY KSI¥¯EK ONLINE FRAGMENTY KSI¥¯EK ONLINE Wydawnictwo Helion ul. Chopina 6 44-100 Gliwice tel. (32)230-98-63 e-mail: helion@helion.pl Znajdĥ b³¹d. Sztuka analizowania kodu Autor: Adam Barr T³umaczenie: Bart³omiej Garbacz ISBN: 83-7361-855-4 Tytu³ orygina³u: Find the Bug: A Book of Incorrect Programs Format: B5, stron: 288 Wyszukiwanie b³êdów w kodzie to czynnoġæ, któr¹ programiġci wykonuj¹ niemal równie czêsto, jak pisanie kodu. Narzêdzia do wykrywania i poprawiania b³êdów tylko czêġciowo rozwi¹zuj¹ problem. W wielu przypadkach b³¹d nie tkwi w nieprawid³owo sformu³owanym poleceniu lub ĥle zdefiniowanej zmiennej, ale w miejscu, którego nawet najlepsze narzêdzie nie znajdzie. Programista musi siê nauczyæ samemu broniæ przed ukrytymi pomy³kami i nieprzyjemnymi niespodziankami. B³êdy trzeba znaleĥæ, zanim one znajd¹ nas. Ksi¹¿ka „Znajdĥ b³¹d. Sztuka analizowania kodu” to zbiór 50 programów napisanych w jêzykach Perl, C, Java, Python i asembler x86. Ka¿dy z nich zawiera jeden, trudny do znalezienia, ale jak najbardziej realistyczny b³¹d. Wykrycie go wymaga przewidzenia sposobu, w jaki program bêdzie wykonywany, i przeġledzenia krok po kroku jego dzia³ania. Ka¿dy przyk³ad opatrzony jest wskazówkami pomocnymi przy wyszukiwaniu b³êdów. Ksi¹¿ka przedstawia sposoby analizowania programów i przewidywania miejsc, w których mo¿e wyst¹piæ b³¹d. • Klasyfikacja b³êdów • Metody analizy kodu • B³êdy w programach w jêzyku C • Analiza aplikacji napisanych w jêzyku Python • Wyszukiwanie b³êdów w programach w jêzyku Java • Programy w jêzyku Perl i asembler x86 Wykonuj¹c zadania zawarte w tej ksi¹¿ce, nie tylko nauczysz siê odnajdywaæ b³êdy, ale tak¿e udoskonalisz swoje umiejêtnoġci w zakresie pisania aplikacji Spis treści O Autorze .......................................................................................... 9 Wstęp ............................................................................................. 11 Rozdział 1. Klasyfikacja błędów ......................................................................... 17 Rozdział 2. Wskazówki dotyczące analizy kodu ................................................. 19 Podział kodu na sekcje o określonych celach działania .................................................. 20 Identyfikacja sekcji w kodzie ...................................................u................................ 21 Identyfikacja celów działania każdej sekcji ...................................................u.......... 22 Komentarze ...................................................u...................................................u........ 23 Identyfikacja znaczenia każdej zmiennej ...................................................u.................... 24 Nazwy zmiennych ...................................................u................................................. 24 Określenie sposobów użycia każdej zmiennej ...................................................u...... 24 Zmienne ograniczone ...................................................u............................................ 26 Warunki niezmiennicze ...................................................u......................................... 27 Śledzenie zmian zmiennych ograniczonych ...................................................u.......... 28 Wyszukanie znanych pułapek ...................................................u..................................... 28 Liczniki pętli ...................................................u...................................................u...... 28 Wyrażenia występujące po lewej oraz po prawej stronie instrukcji przypisania ........ 30 Sprawdzenie operacji sprzężonych ..................................................u......................... 30 Wywołania funkcji ...................................................u................................................ 31 Wartości zwracane ...................................................u................................................ 32 Kod podobny do istniejącego błędu ...................................................u...................... 33 Wybór danych wejściowych dla celów analizy działania ............................................... 33 Pokrycie kodu ...................................................u...................................................u.... 35 Puste dane wejściowe ...................................................u............................................ 36 Banalne dane wejściowe ...................................................u....................................... 37 Gotowe dane wejściowe ...................................................u........................................ 38 Błędne dane wejściowe ...................................................u......................................... 38 Pętle ...................................................u...................................................u...................39 Liczby losowe ...................................................u...................................................u.... 39 Analiza działania każdej sekcji kodu ...................................................u........................... 40 Śledzenie wartości zmiennych ...................................................u.............................. 41 Układ kodu ...................................................u...................................................u......... 41 Pętle ...................................................u...................................................u...................43 Podsumowanie ...................................................u...................................................u......... 44 6 Spis treści Rozdział 3. C ..................................................................................................... 45 Krótkie omówienie języka C ...................................................u....................................... 45 Typy danych i zmienne ...................................................u......................................... 45 Ciągi znaków ..................................................u...................................................u....... 47 Wskaźniki ...................................................u...................................................u.......... 47 Struktury ...................................................u...................................................u............ 48 Instrukcje warunkowe ...................................................u........................................... 49 Pętle ...................................................u...................................................u...................50 Funkcje ...................................................u...................................................u............... 51 Sortowanie przez wybieranie ..................................................u........................................ 51 Wstawianie pozycji na liście jednokierunkowej ...................................................u.......... 54 Usuwanie pozycji z listy jednokierunkowej ...................................................u................ 57 Kopiowanie obszaru pamięci ..................................................u........................................ 60 Rozkład ciągu znaków na podciągi ...................................................u............................. 63 Mechanizm przydzielania pamięci ...................................................u.............................. 66 Zwalnianie pamięci ...................................................u...................................................u.. 69 Rekurencyjne odwracanie zdania ...................................................u................................ 72 Określanie wszystkich możliwych tras ...................................................u........................ 76 Znak cofania w alfabecie Kanji ...................................................u................................... 78 Rozdział 4. Python ............................................................................................ 83 Krótkie omówienie języka Python ...................................................u.............................. 83 Typy danych i zmienne ...................................................u......................................... 83 Ciągi znaków ..................................................u...................................................u....... 84 Listy i krotki ...................................................u...................................................u....... 85 Słowniki ...................................................u...................................................u............. 87 Instrukcje warunkowe ...................................................u........................................... 88 Pętle ...................................................u...................................................u...................88 Funkcje ...................................................u...................................................u............... 89 Klasy ...................................................u...................................................u.................. 89 Wyjątki ...................................................u...................................................u............... 90 Importowanie kodu ...................................................u............................................... 91 Dane wyjściowe ...................................................u...................................................u. 91 Określanie liczby pierwszej ...................................................u......................................... 91 Znajdowanie podciągu ...................................................u................................................ 93 Sortowanie alfabetyczne wyrazów ...................................................u.............................. 95 Kodowanie ciągów znaków za pomocą tablicy znaków ................................................. 97 Wyświetlanie miesiąca i dnia ...................................................u.................................... 100 Gra „Go Fish”, część I: pobieranie karty z talii ...................................................u......... 102 Gra „Go Fish”, część II: sprawdzenie posiadania karty przez drugą rękę .................... 105 Gra „Go Fish”, część III: pełna gra ...................................................u........................... 108 Analiza składniowa liczb zapisanych w języku angielskim .......................................... 112 Przypisywanie prezentów do obdarowywanych ...................................................u........ 115 Rozdział 5. Java .............................................................................................. 119 Krótkie omówienie języka Java ...................................................u................................. 119 Typy danych i zmienne ...................................................u....................................... 119 Ciągi znaków (i obiekty) ...................................................u..................................... 120 Tablice ..................................................u...................................................u............... 122 Instrukcje warunkowe ...................................................u......................................... 124 Pętle ...................................................u...................................................u................. 124 Klasy ...................................................u...................................................u................ 125 Wyjątki ...................................................u...................................................u............. 127 Importowanie kodu ...................................................u............................................. 128 Aplikacje wiersza poleceń i aplety ...................................................u...................... 129 Spis treści 7 Określanie roku przestępnego ...................................................u................................... 129 Konwersja liczby na tekst ...................................................u.......................................... 132 Rysowanie trójkąta na ekranie, część I ...................................................u...................... 135 Rysowanie trójkąta na ekranie, część II ..................................................u...................... 139 Odwracanie listy jednokierunkowej ...................................................u.......................... 141 Sprawdzenie, czy lista zawiera pętlę ...................................................u......................... 143 Sortowanie szybkie ...................................................u...................................................u. 146 Gra Pong, część I ...................................................u...................................................u.... 149 Gra Pong, część II ...................................................u...................................................u.. 153 Obliczanie wyników w grze w kręgle ...................................................u......................... 156 Rozdział 6. Perl ............................................................................................... 161 Krótkie omówienie języka Perl ...................................................u................................. 161 Typy danych i zmienne ...................................................u....................................... 161 Ciągi znaków ..................................................u...................................................u..... 162 Listy ...................................................u...................................................u................. 163 Skróty ...................................................u...................................................u............... 165 Warunki logiczne ...................................................u................................................ 165 Pętle ...................................................u...................................................u................. 167 Podprogramy ...................................................u...................................................u.... 168 Kontekst skalarny i listowy ...................................................u................................. 168 Uchwyty plików ...................................................u.................................................. 169 Wyrażenia regularne ...................................................u........................................... 170 Dane wyjściowe ...................................................u.................................................. 171 Parametry wywołania z wiersza poleceń ...................................................u............. 171 Sortowanie pliku według długości wierszy ...................................................u............... 172 Wyświetlanie czynników pierwszych liczby ...................................................u............. 174 Rozwijanie znaków tabulacji ...................................................u..................................... 176 Prosta baza danych ...................................................u...................................................u. 178 Znajdowanie powtarzającej się części ułamka ..................................................u............ 181 Rozszerzanie listy plików z wcięciami na pełne ścieżki dostępu ................................. 183 Sortowanie wszystkich plików w drzewie struktury katalogów ................................... 186 Obliczanie średnich ocen z testów ...................................................u............................. 189 Sortowanie przez scalanie wielu plików ...................................................u.................... 192 Gra Mastermind ..................................................u...................................................u....... 195 Rozdział 7. Język asemblera x86 ..................................................................... 201 Krótkie omówienie języka asemblera x86 ...................................................u................. 201 Typy danych i zmienne ...................................................u....................................... 201 Operacje arytmetyczne ...................................................u........................................ 204 Znaczniki, warunki i skoki ...................................................u.................................. 206 Pętle ...................................................u...................................................u................. 208 Procedury ...................................................u...................................................u......... 210 Wyjście ...................................................u...................................................u............ 213 Reszta z dolara ...................................................u...................................................u....... 213 Mnożenie dwóch liczb przy użyciu operacji przesunięcia ............................................ 215 Złączanie ciągów znaków z separatorem ...................................................u................... 217 Obliczanie wartości ciągu Fibonacciego ...................................................u................... 220 Sprawdzanie, czy dwa wyrazy są anagramami ...................................................u.......... 222 Konwersja 64-bitowej liczby na ciąg znaków z jej zapisem dziesiętnym ..................... 226 Suma wartości w tablicy liczb ze znakiem ...................................................u................ 230 Gra symulacyjna Życie ...................................................u.............................................. 234 Sprawdzenie dopasowania nawiasów w kodzie źródłowym ......................................... 238 Sortowanie przez zamianę w podstawie ...................................................u.................... 242 8 Spis treści Dodatek A Klasyfikacja błędów ....................................................................... 247 Składnia a semantyka ...................................................u................................................ 248 Klasyfikacja używana w książce ...................................................u............................... 249 A — Algorytm ...................................................u...................................................u....... 251 A.przesunięcie-o-jeden ...................................................u........................................ 251 A.logika ...................................................u...................................................u............ 252 A.walidacja ...................................................u...................................................u...... 253 A.wydajność ...................................................u...................................................u..... 255 D — Dane ...................................................u...................................................u.............. 255 D.indeks ...................................................u...................................................u........... 255 D.limit ...................................................u...................................................u.............. 256 D.liczba ...................................................u...................................................u............ 257 D.pamięć ...................................................u...................................................u.......... 259 Z — Zapomniane ...................................................u...................................................u... 261 Z.inicjalizacja ...................................................u...................................................u... 261 Z.pominięcie ...................................................u...................................................u.... 262 Z.lokalizacja ...................................................u...................................................u..... 263 P — Pomyłka ...................................................u...................................................u......... 264 P.zmienna ...................................................u...................................................u......... 264 P.wyrażenie ...................................................u...................................................u...... 265 P.język ...................................................u...................................................u.............. 266 Podsumowanie ...................................................u...................................................u....... 267 Dodatek B Indeks błędów według typu ............................................................ 269 Dodatek C Materiały źródłowe ......................................................................... 273 Klasyfikacja błędów ...................................................u.................................................. 273 Ogólne pozycje poświęcone typom błędów ...................................................u.............. 274 C ...................................................u...................................................u............................. 274 Python ...................................................u...................................................u.................... 275 Java ...................................................u...................................................u......................... 275 Perl ...................................................u...................................................u......................... 275 Język asemblera x86 ...................................................u.................................................. 275 Skorowidz ...................................................................................... 277 Rozdział 2. Wskazówki dotyczące analizy kodu Celem niniejszej książki jest udoskonalenie umiejętności Czytelnika w zakresie znajdo- wania błędów w kodzie. Zanim jednak przejdziemy do konkretnych przykładów, w ni- niejszym rozdziale zostaną przedstawione pewne porady odnośnie do czytania kodu. W zamierzeniu nie ma to być kompletne opracowanie technik usuwania błędów z kodu, lecz wprowadzenie do zagadnienia i przedstawienie informacji, które mogą okazać się przydatne podczas lektury problemów zawartych w kolejnych rozdziałach książki. W swoim artykule Tales of Debugging from the Front Lines Marc Eisenstadt omawia różne sposoby znajdowania błędów. Jednym z nich jest, jak to określa, „zbieranie da- nych”, które polega na przejrzeniu kodu w programie uruchomieniowym, dodaniu kodu opakowującego, wstawieniu instrukcji drukujących itd. Może się to okazać przydatnym sposobem usuwania błędów z kodu i wielu przypadkach będzie to metoda poprawna. Jednak problemy opisane w niniejszej książce nie poddają się łatwo rozwiązaniu przy użyciu techniki zbierania danych, ponieważ w ich przypadku nie ma czego zbierać. Programy mieszczą się na stronie i w zamierzeniu ich analiza ma odbywać się właśnie w ten sposób. Można by, co prawda, wprowadzić je do komputera i wykonać, jednak stałoby to w sprzeczności z celami stawianymi niniejszej książce. Zamierzeniem autora jest zmuszenie Czytelnika do analizy programów za pomocą, jak to określa Eisenstadt, inspekulacji, którą opisuje jako „połączenie »inspekcji« (inspek- cji kodu), »symulacji« (symulacji ręcznej) oraz »spekulacji«… Innymi słowy, [progra- miści] albo na pewien czas porzucają problem i zajmują się czymś innym, albo poświę- cają dużo czasu na czytanie kodu i jego przemyślenie, być może symulując ręcznie jego wykonywanie. Chodzi o to, że tego rodzaju techniki nie wiążą się z prowadzeniem żad- nych eksperymentów ani zbieraniem danych, a tylko »myśleniem« o kodzie”. 20 Rozdział 2. ♦ Wskazówki dotyczące analizy kodu Mówi się, że Archimedes — matematyk żyjący w trzecim wieku p. n. e. — po uświa- domieniu sobie faktu, że wyporność obiektu jest zależna od ciężaru cieczy, którą wy- piera, biegał nago po ulicach krzycząc „Eureka!”, co w grece oznacza „znalazłem”. Archimedes wchodząc do wanny obserwował, jak woda wylewa się poza jej brzegi w momencie zanurzania jego ciała, i właśnie wtedy przyszła mu do głowy owa genialna myśl. Znajdowanie błędów w kodzie może stanowić podobne doświadczenie. Nieocze- kiwanie możemy sobie coś uświadomić, doświadczając własnego odkrycia (bieganie nago po ulicach nie jest obowiązkowe). Niniejszy rozdział prezentuje szereg działań, które można podjąć w czasie analizy kodu. Często nie jest konieczne wykonywanie ich wszystkich — w dowolnej chwili przyczyna błędu może nagle się ujawnić, nawet jeśli nie rozpatruje się bezpośrednio zawierającego go kodu. Jednak jeśli taki przebłysk intuicji nie wystąpi w ogóle, można mieć nadzieję, że do czasu wykonania ostatniego z opisywanych działań błąd zostanie odkryty. Działania, o których mowa, to: 1. Podział kodu na sekcje o określonych celach działania. 2. Identyfikacja znaczenia każdej zmiennej. 3. Wyszukanie znanych pułapek. 4. Wybór danych wejściowych dla celów analizy działania. 5. Analiza działania każdej sekcji kodu. Poniżej działania te opisano bardziej szczegółowo. Podział kodu na sekcje o określonych celach działania Pierwszym etapem działań zmierzających do zrozumienia kodu jest dokonanie jego podziału na sekcje i zidentyfikowanie celów każdej z nich. Sekcja jest fragmentem kodu wykonującym określone zadanie. Nie da się określić kon- kretnej liczby wierszy kodu składających się na sekcję; zależy to całkowicie od cha- rakteru kodu. Sekcja może składać się z jednej instrukcji lub wywołania funkcji, albo stanowić pętlę o 30 wierszach kodu. Sekcję można zdefiniować jako dowolną sekwen- cję instrukcji programu, które wykonują wystarczająco dużo działań, by poświęcenie czasu na zdefiniowanie ich celów było uzasadnione. „Celem działania” sekcji kodu jest zbiór zmian, które ma wprowadzić dany kod w struk- turach danych używanych przez program. Jeżeli sekcja stanowi pełną funkcję, nazwa tej funkcji zwykle wskazuje ogólnie, jakie działania są wykonywane w ramach sekcji, jednak nie na tyle szczegółowo, by mogło to pomóc w analizie kodu. Jest to bardziej punkt wyjścia do dalszych analiz celów działania funkcji. Podział kodu na sekcje o określonych celach działania 21 Identyfikacja sekcji w kodzie Jeżeli znamy kod, który jest poddawany analizie, zadanie jego podziału na sekcje może okazać się proste, ponieważ wiemy, które jego części odpowiadają różnym częściom implementowanego algorytmu. Jeżeli jednak nie znamy kodu — czy to dlatego, że napisała go inna osoba, czy dlatego, że napisaliśmy go sami, ale na tyle dawno, że nie pamiętamy już szczegółów — trzeba poświęcić nieco czasu na przemyślenie kwestii podziału kodu. Podstawowym etapem działań jest zlokalizowanie głównej części algorytmu. Większość funkcji rozpoczyna się od kodu wprowadzającego, obsługującego przypadki szczególne, błędy i inne podobne elementy, zaś kończy kodem czyszczącym, który zwykle zwraca pewne wartości do funkcji wywołującej. Między nimi znajduje się kod implementują- cy algorytm główny. Algorytm główny jest tą częścią kodu, którą należałoby omówić w przypadku opisywa- nia jego działania innej osobie. Można powiedzieć: „funkcja wyszukuje klucz w słow- niku”, bez wspominania o tym, że najpierw sprawdza ona, czy słownik jest poprawny, a później zwalnia bufor tymczasowy, który przydzieliła. Oczywiście, kod wprowadzający i czyszczący również mogą zawierać błędy i muszą zostać poddane sprawdzeniu równie dokładnie, jak każdy inny fragment kodu. Jednak bez wątpienia te partie kodu zwykle są wykonywane dla dowolnych danych wejścio- wych, więc są testowane bezustannie. Ukryte błędy związane z postacią danych wej- ściowych mogą ukrywać się w algorytmie głównym. Jest to ta część kodu, która fak- tycznie odpowiada implementowanemu algorytmowi matematycznemu. Stąd przydatną rzeczą jest określenie, gdzie kończy się kod wprowadzający, a gdzie zaczyna kod czyszczący. Należy zaznaczyć obszar znajdujący się między tymi wier- szami jako lokalizację algorytmu głównego: KPVHKPFANCTIGUVAJCUJ 5VTKPIU=? ] KH UNGPIVJ ] VJTQYPGY+PXCNKF2CTCOGVGT ZEGRVKQP  _ *CUJ CNEWNCVQTJDPGY*CUJ CNEWNCVQT  KPVNCTIGUVJCUJJDJCUJ U=?  KPVPGYJCUJ HQT KPVLLUNGPIVJL ] PGYJCUJJDJCUJ U=L?  KH PGYJCUJ NCTIGUVJCUJ ] NCTIGUVJCUJPGYJCUJ _ _ JDHNWUJ  TGVWTPNCTIGUVJCUJ _ 22 Rozdział 2. ♦ Wskazówki dotyczące analizy kodu W powyższym przykładzie kod sprawdzający UNGPIVJ oraz trzy wiersze definiu- jące zmienne JD, NCTIGUVJCUJ oraz PGYJCUJ stanowią kod wprowadzający. Wywołanie metody JDHNWUJ oraz instrukcja TGVWTP to kod czyszczący. Reszta kodu stanowi algorytm główny. Powyższy przykład pokazuje również, że nie trzeba znać dokładnie kodu w celu okre- ślenia rozmieszczenia sekcji. Choć nie mamy żadnych informacji na temat klasy *CUJ å CNEWNCVQT, i tak nie mamy wątpliwości, gdzie jest ona inicjalizowana, gdzie się jej używa w algorytmie głównym oraz gdzie są wykonywane działania czyszczące. Jeżeli algorytm główny składa się z więcej niż kilku wierszy kodu, musi zostać po- dzielony na mniejsze części. Ponownie należy wziąć pod uwagę, jak można by opisać algorytm innej osobie. Każda część takiego opisu prawdopodobnie określa sekcję kodu. Gdybyśmy opisali algorytm następująco: „najpierw wczytujemy dane, potem rozmiesz- czamy je według wartości klucza, a następnie przekazujemy na wyjście”, należałoby podjąć próbę podziału kodu na adekwatne trzy sekcje. Identyfikacja celów działania każdej sekcji Po dokonaniu podziału kodu na sekcje należy zidentyfikować cele działania każdej z nich. Jakie zmienne i w jaki sposób powinny zostać zmodyfikowane na końcu każ- dej sekcji? Jakie warunki niezmiennicze powinny być spełnione? W jaki sposób należy skonfigurować struktury danych? Kiedy zakończymy etap podziału kodu na sekcje z określeniem ich celów, sprawdzamy, czy każdy z tych celów został uwzględniony: kod rozpoczynający działania związane z kolejnym celem zanim zakończy się przetwarzanie poprzedniego może być podatny na powstawanie błędów. Niektóre języki dopuszczają użycie instrukcji asercji (ang. assertion) które stanowią wyrażenia logiczne (zwykle testowane tylko w wersji uru- chomieniowej kodu), powodujące zatrzymanie programu w przypadku, gdyż ich warto- ścią okaże się fałsz. Luki między sekcjami często stanowią dobre miejsce na wstawienie instrukcji asercji, które weryfikują poprawne osiągnięcie celów stawianych danej sek- cji, co pokazuje poniższy kod: RWDNKEENCUU/[#TTC[] RWDNKEDQQNGCPKU5QTVGF ] HQT KPVLLFCVCNGPIVJL ] KH FCVC=L? FCVC=L ? ] TGVWTPHCNUG _ _ TGVWTPVTWG _ _ /[#TTC[OC // Sortowanie tablicy OCUQTV  CUUGTV OCKU5QTVGF  Podział kodu na sekcje o określonych celach działania 23 Jeżeli sekcja kodu jest pętlą, należy określić ogólny cel jej działania. Jednak należy również postarać się określić cel działania pętli w każdym jej przebiegu. Przykładowo, w przypadku pętli sortującej tablicę celem jej pierwszego przebiegu może być: „pierw- szy element w tablicy ma zawierać najmniejszą wartość”. W przypadku instrukcji KH należy postarać się określić cel samego warunku KH, na przykład: „blok KH zostanie wykonany wówczas, gdy użytkownik nie został jeszcze zweryfikowany”. Komentarze Komentarze stanowią istotny element procesu określania celów działania fragmentów kodu. Stanowią one jedyną możliwość sformułowania w języku naturalnym i zakomu- nikowania przez programistę informacji o jego działaniach. Wielu programistów pisze komentarze jako wskazówki, pomocne w momencie po- wtórnego przeglądania kodu. W wielu przypadkach komentarze, szczególnie te długie, wskazują obszary kodu, które w odczuciu autora były skomplikowane, niejasne lub w pewien inny sposób niełatwe w odbiorze w czasie późniejszej analizy. Obecność takich komentarzy zazwyczaj określa kluczowe części algorytmu. Komentarze mogą również pomóc w identyfikacji przydatnych sekcji kodu, ponieważ często wielowierszowe komentarze objaśniające poprzedzają blok kodu warty zgru- powania w ramach jednej sekcji i stanowią próbę wyjaśnienia celów działania takiego fragmentu kodu. Jednakże istotną rzeczą jest, by nie dać się zwieść komentarzom. Kompilator i (lub) interpreter ignoruje komentarze i niekiedy podobnie powinien postąpić czytający kod. Komentarze mogą nie być dostosowane do zaktualizowanej wersji kodu lub mogą w ogóle być błędne. Choć reprezentują punkt wyjścia do prób zrozumienia kodu, na- leży zweryfikować ich poprawność w stosunku do faktycznego kodu. Niektóre komentarze są wstawiane bez namysłu, w przekonaniu, że wszystkie operacje wymagają opatrzenia komentarzem, jak w poniższym przykładzie: // dodanie this_price do total VQVCN VJKUARTKEG Tego rodzaju komentarze raczej nie pozwolą na ujawnienie obszarów kodu zawierają- cych błędy. Z drugiej strony, prosty komentarz podobny do prezentowanego poniżej, który jest bez wątpienia błędny, stanowi sygnał, że w kodzie mogły być wprowadzone znaczące zmiany od czasu jego oryginalnego utworzenia: // aktualizacja współrzędnej x [AEQQTF FGNVC Najprawdopodobniej ktoś zmienił ten kod w pośpiechu, być może wklejając go z in- nego dokumentu, a następnie zmienił nazwy zmiennych przy użyciu funkcji edytora automatycznego wyszukiwania i zastępowania ciągów znaków. W procesie tym mogło zostać wypaczone znaczenie i cele działania kodu. 24 Rozdział 2. ♦ Wskazówki dotyczące analizy kodu Identyfikacja znaczenia każdej zmiennej Po zidentyfikowaniu wszystkich sekcji należy przyjrzeć się zmiennym używanym w kodzie i określić „znaczenie” każdej z nich. Znaczenie zmiennej oznacza wartość, jaką koncepcyjnie powinna ona przyjmować. Nazwy zmiennych Nazwy zmiennych, podobnie jak komentarze, mogą być albo przydatne, albo mylące. W przeciwieństwie do sekcji kodu, wszystkie zmienne posiadają nazwy, co zwykle można wykorzystać, uzyskując pewne wskazówki co do znaczenia zmiennych. Nazwa zmiennej stanowi niejako mini-komentarz programisty, wstawiany w każdym miejscu użycia zmiennej. Jednak, podobnie jak w przypadku komentarzy, należy się upewnić, że zmienne rzeczywiście są używane w sposób, jaki sugerują ich nazwy. Ponadto nie- które zmienne, nawet te o istotnym znaczeniu, posiadają nazwy jednoliterowe lub inne mało znaczące: HNQCVUTGFPKGAUCNFQ// prawidłowo UVTKPIPCYC// OK, ale nazwa czego? KPVM// niejasne, może oznaczać cokolwiek W przeciwieństwie do komentarzy, kompilator lub interpreter nie ignoruje nazw zmien- nych, ponieważ odwołują się one do określonych obszarów pamięci. Jednak kompilator czy interpreter nie stawia żadnych wymagań co do faktycznych nazw. Nazwanie zmien- nej C, UWOC lub Y[Z nie wpływa na sposób jej obsługi przez kompilator. To, co ma zna- czenie, to wymóg poprawnego zadeklarowania, zdefiniowania i używania zmiennej w programie. Jeżeli zmienna posiada niejasną nazwę lub nazwa ta nie odpowiada jej prawdziwemu znaczeniu, należy spróbować określić nową nazwę lub przynajmniej słowną definicję jej znaczenia. Przykładowo, w przypadku zmiennej o nazwie K można zapisać uwagę, że jest ona używana tylko jako licznik pętli lub że przechowuje identyfikator bieżące- go użytkownika, albo że zawiera wskaźnik na kolejny wiersz danych wejściowych. Określenie sposobów użycia każdej zmiennej W przypadku każdej zmiennej wykorzystywanej w funkcji lub w bloku kodu należy sprawdzić, gdzie jest używana. Pierwszym krokiem jest odróżnienie miejsca użycia zmiennej w wyrażeniu (gdzie nie podlega modyfikacjom) od miejsca, gdzie przyjmuje nową wartość. Nie zawsze jest to oczywiste. Niektóre zmienne, szczególnie chodzi tu o struktury danych, mogą być modyfikowane w funkcjach, do których są przekazy- wane jako argumenty. Niektóre języki zapewniają sposoby określenia, że zmienna nie podlega zmianom w ramach funkcji (na przykład poprzez użycie słowa kluczowego EQPUV w językach C i C++), ale nie zawsze są one wykorzystywane: Identyfikacja znaczenia każdej zmiennej 25 UWOC FCPG=L?// zmienna suma jest modyfikowana, zmienne dane oraz j są używane RTKPV NKEPKM // zmienna licznik jest używana WRFCVG O[UVTWEV // zmienna mystruct może być modyfikowana Po określeniu, gdzie jest modyfikowana zmienna, można przejść do etapu podjęcia próby zrozumienia, w jaki sposób jest używana. Czy jej wartość jest stała w ramach całej funkcji? Czy jest stała w pojedynczej sekcji kodu? Czy jest używana tylko w jed- nej części kodu, czy wszędzie? Jeżeli jest używana w więcej niż jednej części, czy wy- korzystuje się ją ponownie tylko w celu uniknięcia deklarowania dodatkowej zmiennej (liczniki pętli są często używane właśnie w ten sposób), czy może jej wartość określona na końcu jednej sekcji ma znaczenie na początku kolejnej? Przeglądając pętle należy przeanalizować stan każdej zmiennej po zakończeniu pętli. Zmienne należy podzielić na te, które w czasie działania pętli nie zmieniały wartości, te, które były używane tylko w pętli (na przykład zmienne przechowujące wartości tym- czasowe), oraz te, które będą używane po zakończeniu pętli z uwzględnieniem pewnych oczekiwań co do ich wartości (w oparciu o działania wykonane w pętli). Licznik pętli może należeć do jednej z dwóch ostatnich kategorii; często jest używany tylko w celu sterowania pętlą, jednak niekiedy używa się go po jej zakończeniu w celu ułatwienia określenia, co wydarzyło się w czasie jej wykonywania (szczególnie wówczas, gdy została zakończona przedwcześnie): HQT LLVQVCNATGEQTFUL ] KH GPFAQHAHKNG ] DTGCM _ _ KH LVQVCNATGEQTFU ] // pętla nie została dokończona ze względu na wystąpienie końca pliku (end_of_file) _ Ze względu na fakt, że wartość zwracana przez funkcję ma znaczenie, należy określić, czy zmienna jest używana tymczasowo w ramach funkcji, czy może stanowi część danych zwracanych do podprogramu wywołującego daną funkcję: FGHUWOACTTC[ CTT  VQV HQTLKPCTT VQVVQV L TGVWTPVQV Zmienna CTT jest używana wewnątrz funkcji, jednak nie podlega modyfikacjom. Zmienna L jest modyfikowana, ale pod koniec funkcji jest niszczona. Z kolei zmienna VQV jest mo- dyfikowana, a następnie zwracana do podprogramu wywołującego. Należy się upewnić, że wszystkie zmienne są inicjalizowane przed ich użyciem (niektóre kompilatory i interpretery ostrzegają użytkownika, jeżeli tak nie jest). Wielu zmiennym nie przypisuje się wartości początkowej w momencie ich deklarowania, więc istotne znaczenie ma to, aby przypisano im pewną wartość w ramach wszystkich możliwych ścieżek wykonania kodu, zanim zostaną użyte w wyrażeniu. 26 Rozdział 2. ♦ Wskazówki dotyczące analizy kodu Zmienne ograniczone Zmienne ograniczone (ang. restricted variables) mogą zawierać tylko określony pod- zbiór wartości, które mogłyby przechowywać w normalnej sytuacji w oparciu o swój typ. Przykładowo, w przypadku kodu symulacji toru wyścigowego o ośmiu torach moż- na zdefiniować zmienną całkowitą o nazwie NCPG. W normalnym przypadku zmienna całkowita może przyjmować wartości z dużego przedziału, jednak w tym przypadku należy ograniczyć ten zakres do przedziału od 1 do 8 lub od 0 do 7. Należy to trakto- wać jako element definicji znaczeniowej zmiennej. Niektóre języki dopuszczają jawne określanie takich ograniczeń na zmiennych, jed- nak często programiści nie wykorzystują tej możliwości nawet tam, gdzie to możliwe. Przykładowo, programista może zdefiniować zbiór wyliczeniowych stałych 10 , 691, 6*4 , (174, (+8 , 5+:, 5 8 0 i +)*6, a następnie może określić, że zmienna NCPG może przyjmować tylko wartości tych stałych. Jednak często istnieje konieczność dokona- nia wyboru między ścisłym sprawdzaniem typów (kompilator lub interpreter zapewnia wówczas, że zmiennej NCPG będzie przypisywana tylko jedna z ośmiu wymienionych wartości) a łatwością programowania (pozwalającą na przykład na wykonywanie ope- racji arytmetycznych na zmiennej NCPG, takich jak dodawanie wartości 1). W idealnej sytuacji każda zmienna ograniczona jest definiowana jako taka — przy- najmniej w komentarzu w miejscu jej definicji — być może poprzez samą nazwę zmien- nej. Zmienne ograniczone są często używane na sposoby powodujące błędy, jeżeli ich wartość okaże się nie należeć do zakładanego przedziału. Tak więc istotną rzeczą jest określenie, czy i w jaki sposób zmienna podlega ograniczeniu: EJCT IGVANCPGAPCOG NCPG ] UVCVKEEJCT NCPGAPCOGU]QPGVYQVJTGG HQWTHKXGUKZ UGXGPGKIJV_ TGVWTPNCPGAPCOGU=NCPG? _ Wykonanie powyższego kodu zakończy się niepowodzeniem, jeżeli wartość parame- tru NCPG przekazanego do funkcji IGVANCPGAPCOG nie będzie należała do przedziału od  do . Indeks tablicy (ang. array index) stanowi rodzaj zmiennej ograniczonej, ponieważ jego prawidłowe wartości determinuje rozmiar tablicy. Niektóre języki sprawdzają operacje dostępu do tablic w czasie uruchomienia i w razie potrzeby generują błędy. Inne języ- ki bez żadnych problemów pozwalają na uzyskiwanie dostępu do dowolnych obszarów pamięci, na które wskazuje indeks. Wykorzystanie błędów czasu uruchomienia jest preferowanym rozwiązaniem, ponieważ w widoczny sposób pokazuje, że coś jest nie w porządku, jednak oba błędy mogą wystąpić z tego samego powodu. Niestety, rozmiar tablicy również może podlegać dynamicznym zmianom i bywa trud- ny do określenia w danym miejscu kodu. Ponadto tablica może być indeksowana przy użyciu skomplikowanych wyrażeń. Weźmy pod uwagę poniższy przykład: KPVCTTC[=? [CTTC[=Z? Identyfikacja znaczenia każdej zmiennej 27 W tym momencie jest rzeczą oczywistą, że zmienna Z jest ograniczona do wartości z przedziału od  do  włącznie (zakładając, że wykorzystywany jest język indeksu- jący tablice od 0). Jeżeli instrukcja dostępu będzie jednak miała postać: [CTTC[=Z? to wartość zmiennej Z będzie ograniczona do przedziału od  do . W przypadku instrukcji podobnej do poniższej: [CTTC[=UQOGHWPEVKQP Z ? określenie poprawnych wartości dla zmiennej Z może okazać się trudnym zadaniem, szczególnie wówczas, gdy liczba elementów w tablicy CTTC[=? została określona w cza- sie działania programu. Warunki niezmiennicze Warunki niezmiennicze stanowią uogólnioną formę zmiennych ograniczonych. Waru- nek niezmienniczy jest wyrażeniem, uwzględniającym jedną lub więcej zmiennych, co do którego przyjmuje się, że powinno mieć wartość prawdy przez czas wykonywania programu oprócz krótkich chwil związanych z aktualizacją wartości zmiennych. Wa- runek niezmienniczy jest zwykle ustaleniem określonym przez programistę w oparciu o to, w jaki sposób chce zarządzać strukturami danych używanymi przez program. Biorąc pod uwagę zmienną będącą niebanalną strukturą danych, należy postarać się określić wszelkie warunki niezmiennicze, które zachowują wartość prawdy w przy- padku, gdy struktura danych pozostaje w spójnym stanie. Przykładowo, struktura da- nych przechowująca ciąg znaków i długość może wymagać, aby owa długość zawsze uwzględniała długość ciągu. Należy się upewnić, czy wszystkie adekwatne elementy struktury danych są inicjalizowane w razie potrzeby. Kiedy struktura danych ulega modyfikacji, należy zapewnić, aby warunki niezmiennicze wciąż były spełnione. W przedstawionym wcześniej przykładzie ze zmienną NCPG warunek niezmienniczy można by określić następująco: NCPG   NCPG Kolejny przykład, dotyczący listy jednokierunkowej, może mieć następującą postać: KH NKUVAJGCF07..  NKUVAJGCF PGZV07..  NKUVAJGCF PGZV RTGXKQWUNKUVAJGCF Warunki niezmiennicze należy określać w miejscu, w którym występują, ponieważ stanowią one niejawny cel działań przed rozpoczęciem i po zakończeniu każdego bloku kodu programu. Ze względu na fakt, że cele działań stanowią teoretyczne idee, ignoro- wane przez kompilator i interpreter, warunki niezmiennicze są również dobrymi kan- dydatami do wykorzystania instrukcji CUUGTV w przypadku języków, które ją obsługują. Występujące wcześniej zdanie, które określało, że warunki niezmiennicze zachowują wartość prawdy „oprócz krótkich chwil związanych z aktualizacją wartości zmiennych”, ma istotne znaczenie. W przypadku programów wielowątkowych trzeba pamiętać, że owe „krótkie chwile” są synchronizowane, więc inny wątek nie znajduje zmiennych w stanie, w którym warunek niezmienniczy ma wartość fałszu. 28 Rozdział 2. ♦ Wskazówki dotyczące analizy kodu Śledzenie zmian zmiennych ograniczonych Jak wcześniej wspomniano, pewne zmienne są ograniczone o tyle, że powinny zawie- rać tylko podzbiór możliwych wartości. Przykładowo, wartość całkowita używana jako wartość logiczna może zostać ograniczona do wartości 0 i 1. Ze względu na fakt, że takie ograniczenia mają zwykle charakter logiczny i nie są wymuszane przez kompi- lator lub interpreter, istotną rzeczą jest sprawdzenie modyfikacji zmiennych w celu upewnienia się, że ich wartości pozostają w ograniczonym przedziale. Modyfikacje zmiennych ograniczonych można sprawdzić w toku procesu indukcyj- nego. Oznacza to, że zanim zmienna zostanie zmodyfikowana, jeżeli założy się, że jej wartość bieżąca jest poprawnie ograniczona, istnieje możliwość udowodnienia, że owa wartość po dokonaniu modyfikacji również jest poprawnie ograniczona. Jeżeli można wykazać, że zmienna jest inicjalizowana poprawną wartością oraz że każda modyfika- cja zachowuje jej poprawne ograniczenie, o ile tylko przed jej wykonaniem tak było, stanowi to dowód na to, że zmienna jest zawsze poprawnie ograniczona. Przykładowo, jeżeli zmienna ITCFG powinna zawierać wartość z przedziału od  do , to poniższa instrukcja: ITCFG zawsze zachowuje wartość zmiennej ITCFG w ramach poprawnego ograniczenia. Jednak w przypadku zapisu takiego jak poniżej: ITCFGITCFG nie jest jasne, czy wartość zmiennej ITCFG wciąż będzie się znajdować w odpowiednim przedziale. Jeżeli jednak założymy, że zmienna ITCFG była wcześniej poprawnie ograni- czona do wartości od  do , to w tym momencie wiemy, że wyrażenie ŌITCFG za- chowuje wartość ITCFG w odpowiednim przedziale. Wyszukanie znanych pułapek Jeżeli kod podzielono na sekcje i określono ich cele oraz zidentyfikowano prawdziwe znaczenie każdej zmiennej i przy wykonywaniu tych czynności nie znaleziono żadnego błędu, można kontynuować działania wybierając pewne dane wejściowe i analizując działanie kodu. Najpierw jednak należy szybko przejrzeć kod w poszukiwaniu kilku znanych, często występujących pułapek, bez wnikania w szczegóły. Liczniki pętli Liczniki pętli (ang. loop counters) są często używane w celu indeksowania tablic. W przy- padku języków stosujących indeksowanie od zera należy sprawdzić, czy sprawdzenie warunku wyjścia z pętli wykorzystuje warunek , czy . Kod podobny do przedsta- wionego poniżej: Wyszukanie znanych pułapek 29 HQT KPFGZKPFGZ/#:A 1706KPFGZ ] LCTTC[=KPFGZ? _ może być poprawny, ale porównanie KPFGZ/#:A 1706 jest podejrzane. W normal- nej sytuacji, w przypadku tablic indeksowanych od zera, zapis ten powinien mieć postać KPFGZ/#:A 1706, tak aby pętla nie wykonała przebiegu dla licznika KPFGZ o wartości /#:A 1706. Jak wcześniej wspomniano, niektóre pętle z logicznego punktu widzenia posiadają wiele liczników, co można wyrazić w oczywisty sposób: HQT LML/#:A5+ L M  ] // treść pętli _ lub w mniej zwarty sposób: M HQT LL/#:A5+ L ] // treść pętli M  _ czy też całkowicie samodzielnie zajmując się obsługą warunków: L M YJKNG VTWG ] KH L /#:A5+ DTGCM // treść pętli L  M  _ Powyższe trzy przykłady kodu wyglądają tak samo, jednak różnica polega na tym, że w drugim i trzecim przykładzie, gdyby do sekcji oznaczonej komentarzem VTGħèRúVNK dodano instrukcję EQPVKPWG, spowodowałoby to pominięcie kodu modyfikującego war- tości liczników pętli. W drugim przykładzie wartość L zostałaby zaktualizowana, jednak wartość M — nie. W trzecim przykładzie ani L, ani M nie zostałyby zaktualizowane. W trzecim przykładzie wartość M jest zwiększana o  w każdym przebiegu pętli. Zwy- kle samo w sobie nie jest to błędem, jednak w normalnym przypadku zwiększanie jest wykonywane o , więc jeżeli ktoś wykorzystał zwiększanie o , prawdopodobnie miał w tym jakiś cel. Należy jednak zapamiętać, że wartość zmiennej M jest zwiększana w niestandardowy sposób. Należy wystrzegać się kodu modyfikującego licznik pętli w niej samej. Zwykle jest to robione celowo i (przynajmniej tak powinno być) operacja taka jest opatrywana komen- tarzem, jednak znacznie utrudnia to analizę wykonywanych operacji w trakcie działa- nia pętli — szczególnie wówczas, gdy modyfikacje są wykonywane tylko w pewnych przypadkach (w zależności od wartości danych podlegających przetwarzaniu w pętli): 30 Rozdział 2. ♦ Wskazówki dotyczące analizy kodu HQT RRDWHHGTAUKGR ] KH DWHHGT=R? ] // znak sterujący, więc pomijamy następny R  _ // treść pętli _ W powyższym przykładzie po instrukcji R nie występuje instrukcja EQPVKPWG, więc treść pętli głównej jest wykonywana bez przeszkód. Wyrażenia występujące po lewej oraz po prawej stronie instrukcji przypisania Ta sama zmienna lub wyrażenie czasem występuje po lewej, a czasem po prawej stronie instrukcji przypisania, które znajdują się blisko siebie. Może tak się zdarzyć w sytuacji, gdy wartość zmiennej jest używana w celu obliczenia wartości innej zmiennej, a na- stępnie pierwsza z tych zmiennych jest oznaczana jako pusta, usuwana itp. Zazwyczaj można wyróżnić krok, w którym zmienna jest używana, oraz krok, w którym podlega modyfikacji (w poniższym przykładzie polega to na wyzerowaniu jej wartości): VQVCN CTTC[=O? CTTC[=O? W takiej sytuacji przekazanie zmiennej do funkcji może być logicznie równoważne jej wystąpieniu po prawej stronie instrukcji przypisania — jest to krok, w którym zmien- na jest używana: FWORAEQPVGPVU EWTTGPVATGEQTF // użycie EWTTGPVATGEQTFXCNKF// wyczyszczenie Błąd występuje wówczas, gdy takie dwie instrukcje zostaną zamienione, to znaczy war- tość zmiennej zostanie wyczyszczona zanim będzie użyta: CTTC[=O? VQVCN CTTC[=O?// wartość elementu już wynosi 0!!! Inny przypadek to kod używany do zamiany wartości dwóch zmiennych, którego stan- dardowy zapis to: VGORXCT XCTXCT XCTVGOR Można z łatwością popełnić błąd w zapisie tych wierszy kodu — czy to pod względem ich kolejności, czy rozmieszczenia zmiennych. Sprawdzenie operacji sprzężonych Wiele operacji wykonujących pewne działania w programie posiada analogiczne operacje „wycofania” i muszą one być ze sobą odpowiednio sprzęgnięte. Wyszukanie znanych pułapek 31 Jednym z przykładów może tu być operacja przydzielania pamięci, a szczególnie pamięci tymczasowej przydzielanej przez funkcję. Całość takiej pamięci musi zostać zwolnio- na przed wyjściem z funkcji bez względu na warunek takiego wyjścia. Niektóre języki nie pozwalają na jawne przydzielanie i zwalnianie pamięci, ale pewne operacje i tak muszą być sprzęgane: zakładanie i zdejmowanie blokad, zwiększanie i zmniejszanie wartości liczników odwołań itd. Kod podobny do przedstawionego poniżej: RTQEGUUATGEQTF TGEQTF TGE ] CESWKTGANQEM TGE  KH UQOGVJKPICDQWV TGE ] TGVWTP // reszta kodu TGNGCUGANQEM TGE  TGVWTPTGVAXCN _ nie zawsze w poprawny sposób sprzęga wywołanie funkcji CESWKTGANQEM TGE z wy- wołaniem TGNGCUGANQEM TGE . Ogólnie rzecz biorąc, w każdym miejscu, gdzie jest wykonywana pierwsza część operacji sprzężonej, należy się upewnić, czy także druga jej część jest zawsze wykonywana bez względu na wybraną ścieżkę wykonania kodu. Wywołania funkcji Wywołania funkcji (ang. function calls) mogą być trudne do analizy, ponieważ kod zawarty w funkcji nie znajduje się bezpośrednio przed czytającym treść programu. W najlepszym przypadku ma się do niego dostęp, ale zwykle trzeba polegać tylko na dokumentacji. Poprawnie napisana funkcja modyfikuje tylko te zmienne, które powinna modyfikować. Wywołanie funkcji można traktować jako pojedynczą instrukcję przypisania, aczkol- wiek może ona modyfikować wiele zmiennych i wykonywać bardziej skomplikowane działania na tablicach i strukturach. W przypadku czytania kodu wywołania funkcji główną rzeczą wartą sprawdzenia jest to, czy parametry są do niej przekazywane w sposób poprawny. Większość kompilato- rów i interpreterów wychwytuje argumenty o błędnym typie, jednak nie błędne argu- menty o poprawnym typie. Jedną z możliwości przekazania błędnego argumentu jest przypadek indeksu do tablicy. Ze względu na fakt, że każdy element tablicy posiada ten sam typ, można przekazać błędny argument o poprawnym typie po prostu myląc wartość indeksu. Typ indeksu jest zwykle jednym z typów podstawowych (umożliwiającym przechowywanie warto- ści całkowitej), więc nietrudno popełnić taki błąd. Przykładowo, w przypadku poniż- szego kodu: ECNNAHWPE UVTWEVACRQKPVGTADCTTC[=S? istnieje prawdopodobieństwo, że jeżeli zmienne UVTWEVAC lub RQKPVGTAD mają błędny typ, kompilator zgłosi błąd. Jednak jeśli zmienna S jest wartością całkowitą, a zapis CTTC[=S? tak naprawdę miał mieć postać CTTC[=T? lub CTTC[=U?, kompilator nie zauwa- ży niczego podejrzanego. 32 Rozdział 2. ♦ Wskazówki dotyczące analizy kodu Wartości zwracane Chociaż wiele funkcji manipuluje na przekazywanych do nich strukturach, w przypadku wielu innych ważna jest jedynie wartość zwracana (ang. return value) — jedyny stały wynik wykonania funkcji. Stąd całość kodu zapisanego z dużą ostrożnością i przeanali- zowanego zda się na nic, jeżeli funkcja zwraca niepoprawną wartość. Podstawowym błędem jest zwrócenie wartości nieodpowiedniej zmiennej, na przykład zwrócenie wskaźnika tymczasowego zamiast oczekiwanego, jak w poniższym kodzie: TGEQTF HKPFANCTIGUV TGEQTFNKUV=? ] TGEQTF EWTTGPVATGEQTF TGEQTF NCTIGUVATGEQTF // kod znajdujący largest_record TGVWTPEWTTGPVATGEQTF _ Kod ten najprawdopodobniej powinien zwrócić wartość NCTIGUVATGEQTF. Ze względu na fakt, że obie zmienne mają ten sam typ, kompilator nie ma żadnej możliwości stwier- dzenia, że z semantycznego punktu widzenia kod jest niepoprawny. Niektóre funkcje posiadają wiele instrukcji TGVWTP. Powrót z funkcji w momencie, gdy znaleziono wynik, jest często o wiele łatwiejszy niż sprawdzanie, czy wciąż należy wykonywać jakieś działania, co pokazuje poniższy przykład: FGHKUAYQTF U  FQPG TGVWTPAXCNWG KHNGP U  TGVWTPAXCNWG FQPG KHFQPG # pewien kod, który może ustawiać wartość return_value na 0 lub 1 KHFQPG # pewien dodatkowy kod, który może ustawiać wartość return_value TGVWTPTGVWTPAXCNWG Bardziej przejrzystym sposobem zapisu tego kodu może być zawarcie instrukcji TGVWTP w każdym miejscu, gdzie ustawiana jest wartość TGVWTPAXCNWG, zamiast używania zmien- nej FQPG w celu uniknięcia wykonywania pozostałego kodu. Tak więc pierwsza część funkcji miałaby następującą postać: FGHKUAYQTF U  KHNGP U  TGVWTP # ciąg dalszy funkcji… Jeżeli występuje wiele instrukcji TGVWTP, należy się upewnić, czy każda ścieżka wyko- nania kodu pozwala na dotarcie do jednej z nich. Na pewno nie chcemy, aby kod miał postać podobną do poniższej: FGHECNEWNCVGACXGTCIG N  KHNGP N  TGVWTP # dodatkowy kod KHEQWPV  TGVWTPVQVCNEQWPV Wybór danych wejściowych dla celów analizy działania 33 Problem związany z powyższym kodem polega na tym, że może powodować wyjście z funkcji bez wykonania żadnej instrukcji TGVWTP. Wiele języków nie pozwala na wystę- powanie takiej sytuacji w przypadku funkcji zdefiniowanych jako zwracające określony typ danych, jednak w powyższym przykładzie, zapisanym w języku Python, funkcja zwróci wewnętrzną wartość 0QPG, która zapewne nie jest tym, czego oczekuje użytkownik. Wreszcie, należy się upewnić, czy zwracane dane są wciąż poprawne. Nie należy zwracać wskaźnika na obszar pamięci, który został już zwolniony! Kod podobny do istniejącego błędu Jeżeli znajdzie się określony błąd, co do którego można podejrzewać, że może się po- wtórzyć w innym miejscu, należy wyszukać inne lokalizacje, w których tak jest. Błędy są powtarzane i może tak być dlatego, że kod został powielony lub autor kodu miał tendencję do popełniania danego błędu, czy też opacznie zrozumiał sposób działania kodu (programista konsekwentnie próbował zrobić coś poprawnie, lecz w rzeczywisto- ści konsekwentnie popełniał błąd). Przykładowo, jeżeli napotka się kod podobny do poniższego: HQT LM/#:M należy poszukać innych pętli HQT, które pasowałyby do tego samego wzorca zapisu w celu zapewnienia, aby ten sam błąd nie był powielany gdzie indziej (szczególnie wówczas, gdy wygląda, jakby fragmenty kodu były kopiowane i wklejane w programie). Podobnie, jeżeli odkryje się, że kod wywołuje funkcję z argumentami podanymi w nie- poprawnej kolejności, należy sprawdzić inne miejsca jej wywołania. Jeżeli odkryje się błąd zakresu dla operacji dostępu do tablicy, należy sprawdzić inne miejsca, gdzie uzy- skiwany jest dostęp do tej samej tablicy. Wybór danych wejściowych dla celów analizy działania Jeżeli po wykonaniu opisanych powyżej działań wciąż nie udało się zlokalizować błędu, najprawdopodobniej konieczne jest przeanalizowanie kodu „na piechotę”. W pewnym sensie taka analiza działania kodu nie jest rozwiązaniem najlepszym. W idealnej sytu- acji pozwoliłoby to na udowodnienie, że każda sekcja wykonuje stawiane jej cele, każda zmienna jest wykorzystywana zgodnie z jej przeznaczeniem oraz że są zwracane i wy- świetlane poprawne wartości, co nie pozostawia miejsca na żadne wątpliwości co do poprawności funkcji dla wszystkich danych wejściowych. Analiza działania kodu wpro- wadza element niepewności, ponieważ bez względu na ilość wypróbowywanych zesta- wów danych wejściowych błąd może nie zostać odkryty przy wykorzystaniu żadnego z nich. 34 Rozdział 2. ♦ Wskazówki dotyczące analizy kodu Jednak w wielu przypadkach jedynym sposobem na znalezienie błędu jest właśnie do- konanie analizy działania kodu. W tym celu należy wybrać pewne dane wejściowe. Oprócz krótkich niezależnych programów, które obliczają określoną wartość (lub zbiór wartości), wszystkie sekcje kodu — czy to programu, czy funkcji, czy po prostu frag- ment większej sekcji kodu — zachowują się odmiennie w zależności od pobranych danych wejściowych. W przypadkach, w których próbuje się wyśledzić błąd zgłoszony przez inną osobę, osoba ta może niekiedy określić dane wejściowe powodujące występowanie problemu. Stanowią one wówczas pierwszy zestaw danych wybieranych do analizy. Jednak nie- kiedy należy wybrać własne serie danych w celu znalezienia trudnego do powtórzenia lub niedostatecznie opisanego błędu, sprawdzając nowy kod przed jego opublikowa- niem. Może to również dotyczyć sytuacji, w których zgłoszone dane wejściowe są zbyt skomplikowane, by z nich skorzystać. Analiza działania kodu jest czasochłonna. Nie można po prostu sprawdzić jego działania dla wszystkich danych wejściowych. Na szczęście często można wykorzystać małą próbkę takich danych, która jednak jest na tyle reprezentatywna, że pozwala na wykrycie wszystkich możliwych błędów. Określając dane wejściowe należy pamiętać o tym, że nie jest się ograniczonym do wybo- ru tylko danych wejściowych funkcji zewnętrznych czy całego niezależnego programu. W rzeczywistości często łatwiej jest podzielić kod na mniejsze grupy i przeanalizować je w pierwszej kolejności. Po uzyskaniu pewności, że owe mniejsze grupy obsługują różne dane wejściowe w sposób poprawny, można się cofnąć i przeanalizować większe partie kodu bez konieczności powtórnego analizowania szczegółów sprawdzanych sekcji. Z najłatwiejszym przypadkiem podziału kodu mamy do czynienia wówczas, gdy funk- cje mają charakter warstwowy — jedna wykorzystuje drugą. Rozpoczynamy od funkcji znajdującej się na najniższym poziomie, czyli takiej, która nie zawiera żadnych wy- wołań innych funkcji w ramach sprawdzanego kodu. Następnie przechodzimy w górę hierarchii, sprawdzając po kolei każdą funkcję zewnętrzną. Podobnie można postąpić w ramach pojedynczej funkcji po dokonaniu jej podziału na logiczne sekcje. Wybieramy sekcję, którą chcemy sprawdzić, a następnie określamy jej dane wejściowe. W tym przypadku na „dane wejściowe” składają się wartości wszystkich zmiennych, które są używane w badanej sekcji kodu. Jeżeli określiliśmy znaczenie każdej zmiennej, będziemy wiedzieć, które z nich są tu istotne. Jeżeli program przechowuje pewne dane o swoim stanie między kolejnymi uruchomie- niami kodu, należy również postarać się określić możliwe wartości takich danych. Przy- kładowo, w przypadku języków obiektowych funkcja, którą się bada, może być meto- dą klasy. Wówczas bieżący stan zmiennych składowych klasy (używanych w funkcji) z logicznego punktu widzenia stanowi część danych wejściowych takiej funkcji. Wreszcie, powinno być rzeczą oczywistą, że wybierając testowe dane wejściowe należy znać oczekiwane dane wyjściowe. W przeciwnym razie określenie, czy program działa poprawnie, stanowi ogromną trudność. Wybór danych wejściowych dla celów analizy działania 35 Pokrycie kodu Określając dane wejściowe dla znanego kodu ma się przewagę nad osobami, które prze- prowadzają testy na kodzie stanowiącym „czarną skrzynkę
Pobierz darmowy fragment (pdf)

Gdzie kupić całą publikację:

Znajdź błąd. Sztuka analizowania kodu
Autor:

Opinie na temat publikacji:


Inne popularne pozycje z tej kategorii:


Czytaj również:


Prowadzisz stronę lub blog? Wstaw link do fragmentu tej książki i współpracuj z Cyfroteką: