R05-03, ## Documents ##, C++Builder 5


Rozdział 5. Uruchamianie, śledzenie przebiegu i testowanie aplikacji

Po skompletowaniu kodu źródłowego projektu, jego skompilowaniu, skonsolidowaniu i uruchomieniu przychodzi zazwyczaj czas pierwszych rozczarowań - stworzona z takim trudem aplikacja, o ile w ogóle nie odmawia współpracy z użytkownikiem, zamiast oczekiwanych danych produkuje przeważnie komunikaty o błędach. Scenariusz taki wpisany został już na dobre w realia życia zawodowego programistów projektantów i pozostanie aktualny tak długo, dopóki w drastyczny sposób nie zmieni się technologia tworzenia oprogramowania dla komputerów.

Etymologia angielskiej nazwy debugging, określającej całokształt postępowania zmierzającego do zlokalizowania i poprawienia błędnych elementów aplikacji, nie doczekała się jednolitego wyjaśnienia. Najczęściej powtarzająca się wersja, zgodnie z którą zabłąkana pomiędzy kable komputera pluskwa (ang. bug) spowodowała jego błędne działanie, kwestionowana bywa przez autorytety powołujące się na dokumentację ikonograficzną; wiele materiałów źródłowych przypisuje autorstwo samego terminu bug pani admirał Grace Hopper, która 9 września 1945 r. odnalazła martwą ćmę (mola?) między stykami jednego z tysięcy przekaźników ówczesnego komputera Mark II. Prawdopodobnie jednak dla Czytelników książek takich jak niniejsza techniczne szczegóły „odpluskwiania” aplikacji są znacznie bardziej interesujące od samej nazwy, swoją drogą dość trafnie odzwierciedlającej charakter zmagania się programistów z oporną rzeczywistością.

Równie niejednoznaczną kwestią jest sama definicja „błędu” aplikacji - okazuje się, iż zaklasyfikowanie takich czy innych aspektów funkcjonowania aplikacji jako „błędy” zależne jest od punktu widzenia projektanta, użytkownika itp. Abstrahując w tym miejscu od ścisłej, teoretycznej definicji „błędu” ograniczymy się do stwierdzenia, iż najpowszechniej popełniane przez programistów błędy, nie pozostawiające wątpliwości co do swojego charakteru, zaliczyć można do jednej z poniższych kategorii:

W uzupełnieniu wymienić można wiele innych, rzadziej spotykanych okoliczności, jak na przykład nie nadający się do współpracy interfejs użytkownika (o czym pisaliśmy w rozdziale 3.) czy też niemożliwe do zaakceptowania ślamazarne tempo pracy aplikacji.

Jak wynika z powyższego zestawienia, natura większości błędów wyklucza możliwość efektywnego wykrywania ich w sposób automatyczny, należy więc walkę z błędami uczynić integralną częścią działań projektowych, bez złudnych nadziei, iż wyręczy nas w tym kompilator czy konsolidator.

Szanse programistów na tworzenie bezbłędnego kodu zwiększają się dzięki oferowanym przez C++Buildera narzędziom wspomagającym proces kodowania, opatrzonych wspólną nazwą Code Insight. Ułatwiają one tworzenie bezbłędnych konstrukcji dzięki m.in. funkcjom automatycznego uzupełniania kodu, podpowiedziom dotyczącym deklaracji używanej właśnie funkcji, wzorcom kodu itp.

Projektowe uwarunkowania śledzenia aplikacji

Jak już wspomnieliśmy, wobec nieuchronności popełniania błędów programistycznych walkę z tymi błędami należy uwzględnić już w początkowych stadiach tworzenia aplikacji. To enigmatyczne stwierdzenie przekłada się w praktyce na działania dwojakiego rodzaju. Po pierwsze, uciekając się do działań obarczonych znikomym ryzykiem błędu, zwiększamy szansę na bezbłędność produktu końcowego - i tak na przykład wykorzystanie gotowych, gruntownie przetestowanych bibliotek, udostępniających środki dla realizacji określonego aspektu funkcjonalnego aplikacji, jest posunięciem zdecydowanie pewniejszym niż samodzielne tworzenie takowych bibliotek ab ovo. Bowiem jakby na przekór znanemu przysłowiu, iż „toczący się kamień nie obrasta mchem”, intensywne prace projektowe - niby przysłowiowe toczenie kamienia - niosą ze sobą groźbę równie intensywnego obrastania „mchem” popełnianych błędów.

Po drugie, popełniane przez programistów błędy stają się mniej groźne, jeżeli manifestują się w aplikacji w sposób wyraźny - na przykład w postaci komunikatu, iż wartość, stanowiąca (zgodnie z algorytmem obliczeń) kwadrat liczby rzeczywistej jest ujemna, a wskaźnik do struktury zawierającej dane wejściowe jest wskaźnikiem pustym. Należy również dążyć do takiego stylu kodowania, by jak najwięcej pomyłek popełnianych przez programistów przekładało się na błędy syntaktyczne, jak wiadomo wykrywalne w stu procentach. W charakterze przykładu rozpatrzmy częsty błąd, polegający na gubieniu jednego znaku „=” w operatorze porównania. Konstrukcja:

if (x == 5)

...

zamienia się wówczas w instrukcję przypisania:

if (x=5)

...

zmieniającą niezauważalnie wartość zmiennej x, co może być naprawdę trudne do wykrycia, lecz już równoważna konstrukcja:

if (5 == x)

w przypadku opuszczenia jednego ze znaków „=” zamienia się w konstrukcję błędną syntaktycznie. Wybierając drugi ze wskazanych sposobów porównania zmiennej x z wartością 5, zwiększamy w znacznym stopniu szansę na wykrycie ewentualnej pomyłki - nosi to znamiona swoistej działalności „obronnej” przed skutkami ludzkiej niedoskonałości i z tego względu zyskało sobie powszechnie miano programowania defensywnego.

Problematyce programowania defensywnego i ogólnie całokształtowi działań programistycznych, przyczyniających się do tworzenia bezbłędnych aplikacji, poświęcona jest w całości książka Steve'a Maguire'a Writing solid code, której polskie wydanie (pod tytułem Niezawodne programowanie) przygotowywane jest właśnie w wydawnictwie Helion - przyp. tłum.

Możliwym źródłem błędu są również luki organizacyjne przy zespołowym tworzeniu projektu. Programista, przystępując do wprowadzania zmian w istniejącej koncepcji, powinien poinformować o tym swoich kolegów, którzy tym samym uniknąć mogą błędów, polegających na wykorzystywaniu nowych konstrukcji na starą modłę. Może się wówczas także okazać, iż jakiś nowy mechanizm, implementowany właśnie przez jednego z programistów, został już wcześniej zrealizowany przez kogoś innego; utrzymywanie dwóch jego różnych implementacji nie sprzyja bynajmniej redukcji popełnianych błędów (nie wspominając już o zwykłej stracie czasu programisty).

Naturalnym sojusznikiem programistów w walce z błędami jest przejrzysty i czytelny styl kodowania, któremu poświęciliśmy rozdział 2. książki. Tworzony kod powinien być w miarę możności kodem samodokumentującym, wszelkie „nieoczywiste” konstrukcje powinny być więc w sposób zrozumiały skomentowane. Należy przy tym tak dobierać sposób kodowania i treść komentarzy, by były one zrozumiałe przez innych programistów, a nie tylko przez programistę kodującego (który, w przeciwnym razie, czytając swój własny produkt po upływie np. pół roku, mógłby sam mieć poważne kłopoty z jego zrozumieniem).

Szczególnie istotnymi dla poprawności tworzonego kodu są te elementy, których w nim po prostu nie ma - mowa tu o przyjętych milcząco specyficznych założeniach, przy których działać ma aplikacja, a także o sytuacjach, które zdaniem programistów nie mają prawa wystąpić. Tak się jednak składa, iż uwarunkowania zewnętrzne szybko się zmieniają (przekonali się o tym niedawno programiści Turbo Pascala, gdy procesor Pentium II okazał się zbyt szybki dla funkcji skalującej czasomierz programowy, powodując w efekcie błąd wykonania 200 - przyp. tłum.), zaś sytuacje uznane dotąd za niemożliwe stają się całkiem realne. Programista utrzymujący aplikację powinien więc być świadom w każdym czasie wspomnianych założeń, które tym samym powinny być wyraźnie udokumentowane w formie komentarzy, a najlepiej dodatkowo weryfikowane za pomocą stosownych testów. Należy również liczyć się z tym, iż domyślne ustawienia opcji, związanych z różnymi aspektami projektu (kompilacją, konsolidacją itp.), mogą ulec zmianie w przyszłych wersjach wykorzystywanego języka programowania.

Poprawiając dostrzeżone błędy, należy jednocześnie uważać, by przy tej okazji nie wprowadzić nowych - wieszając kolejną bombkę na choince, należy uważać, by przy okazji nie stłuc pięciu już wiszących; aktualna staje się w tym momencie zasada, zgodnie z którą „jeżeli coś działa, to nie należy tego poprawiać”.

Programistyczne uwarunkowania śledzenia aplikacji

Zgodnie z tym, co przed chwilą powiedzieliśmy, walka z błędami, które pojawią się w tworzonym projekcie, powinna być organicznie wpisana w sam proces projektowania aplikacji; błędy programistyczne są bowiem zjawiskiem na tyle powszechnym, iż niedopuszczalne jest traktowanie ich jako jedynie swego rodzaju „wypadków przy pracy”, których skutki zniwelować można za pomocą li tylko debuggera. Najdalej nawet posunięta staranność i „defensywność” przy tworzeniu projektów nie jest jednak w stanie całkowicie zapobiec wszystkim błędom (ani nawet ich większości) - wtedy właśnie debuggery stają się najważniejszymi narzędziami, ułatwiającymi zlokalizowanie błędu i jego usunięcie.

Podobnie, jak nie należy pozostawiać zagadnienia błędów programistycznych „na uboczu” zasadniczych prac projektowych, podobnie nie należy odkładać „na później” poprawiania błędów już znalezionych - świadomość, iż po ukończeniu tworzenia aplikacji pozostaje jeszcze usunięcie kilku błędów, utrudnia określenie chociażby terminu ukończenia projektu, trudno bowiem a priori określić, ile czasu zajmie walka z błędami.

Należy ponadto dokumentować wszelkie stwierdzone błędy, zarówno w formie odrębnej listy, jak i innych środków bezpośrednio odnoszących się do kodu źródłowego, jak np. znaczników „To-Do”. Lista zauważonych błędów nie zawsze musi być postrzegana jako „lista złych wiadomości”, szczególnie jeżeli względnie szybko przekształca się ona w listę błędów poprawionych i jeżeli (zgodnie z wytycznymi poprzedniego akapitu) nie będzie nigdy listą zbyt długą.

Przed przystąpieniem do modyfikacji kodu należy bezwzględnie sporządzić kopie zapasowe modyfikowanych modułów - w przypadku totalnego zagubienia się w kodowaniu będziemy mieli wówczas przynajmniej możliwość powrotu do stanu wyjściowego. Zadanie to ułatwiają różnorodne systemy kontrolowania wersji.

I jeszcze jedno: człowiek z reguły uczy się na własnych błędach, należy więc starać się unikać powtórnego popełniania tych samych błędów w tworzonych aplikacjach.

W tym miejscu Autor oryginału poleca ulubioną przez siebie książkę Steve'a McConnella Code Complete wydaną przez w 1993 przez Microsoft Press (ISBN 1-55615-484-4), nazywając ją wprost „biblią dla programistów”. Jego zdaniem książka ta porusza wszelkie aspekty konstruowania kodu źródłowego i jest bardzo przyjemna w czytaniu.

Podstawowe techniki usuwania błędów aplikacji

Zajmiemy się teraz najczęściej stosowanymi metodami lokalizacji błędów w kodzie programu. W dalszej części rozdziału zilustrujemy ich zastosowanie na przykładzie zintegrowanego debuggera C++Buildera 5, zaprezentujemy również kilka technik nieco bardziej zaawansowanych.

Przed zagłębieniem się w złożoną i niekiedy czasochłonną misję poszukiwania błędów należy zastanowić się, czy źródłem błędu nie są zastosowane komponenty niezależnych wytwórców (third party), dokładniej - czy inni użytkownicy tych komponentów nie doświadczyli podobnych błędów lub też czy błędy te znane są samym producentom. Jeżeli tak, to najprawdopodobniej dostępna jest nowa wersja rzeczonych komponentów lub też łata (patch) usuwająca błąd; jeżeli nie, to przyczyna błędu leży najprawdopodobniej w samej aplikacji i czas zabrać się do dzieła. Mamy do dyspozycji cztery zasadnicze metody lokalizowania błędu:

Dwie pierwsze z wymienionych metod mogą wymagać zastosowania pracy krokowej, co oczywiście umożliwia zintegrowany debugger C++Buildera. Należy zdawać sobie sprawę z faktu, iż miejsce wystąpienia błędu (w kodzie źródłowym) zazwyczaj różni się od miejsca zawierającego przyczynę błędu - i tak na przykład niespełnienie którejś asercji może być konsekwencją niespełnienia innych zakładanych warunków we wcześniejszej fazie obliczeń. Dotarcie do faktycznej przyczyny błędu może być dokonane z użyciem trzech następujących metod:

Każdy, kto w swym życiu zetknął się z programowaniem, doskonale wie, iż najtrudniejszymi do zlokalizowania są błędy pojawiające się od czasu do czasu. Minimalizowanie tego zjawiska - jeżeli już trafiają się błędy, powinny one występować w sposób powtarzalny - jest jednym z elementów wspomnianego wcześniej programowania defensywnego.

Przyjrzyjmy się teraz zastosowaniu opisanych technik na gruncie C++Buildera.

Wyprowadzanie informacji testowych

Najprostszą metodą śledzenia przebiegu programu i zmiany zawartości wybranych zmiennych i obiektów jest wyprowadzanie na zewnątrz czytelnych informacji dotyczących problemu. W przypadku aplikacji konsolowych wyprowadzenie to może być realizowane np. za pomocą funkcji printf() lub instrukcji cout <<, czego przykład przedstawia wydruk 5.1. Zwróć uwagę, iż konieczne jest dołączenie plików nagłówkowych <stdio.h> i <iostream.h>.

Wydruk 5.1. Wyprowadzanie informacji testowej w aplikacji konsolowej

#include <stdio.h>

#include <iostream.h>

void MyDebugOutput(AnsiString OutputMessage)

{

// pierwsza metoda - użycie funkcji printf()

printf("Debug: %s\n", OutputMessage.c_str());

// druga metoda - użycie instrukcji cout <<.

cout << "Debug: " << OutputMessage.c_str() << endl;

}

void NormalFunc(int MaxLines)

{

....

MyDebugOutput("Przed pętlą, MaxLines=" + IntToStr(MaxLines));

for (int i = 0 ; i < MaxLines ; i++)

{

MyDebugOutput("In loop, i=" + IntToStr(i));

.......

}

}

W przypadku aplikacji graficznej (GUI) wyprowadzany tekst może być prezentowany w ramach jakiegoś komponentu tekstowego - etykiety, kontrolki edycyjnej, memo itp. - albo wyświetlany w postaci komunikatów produkowanych przez funkcje ShowMessage() lub MessageDlg(). Jeżeli informacja testowa ma być jedynie świadectwem obecności sterowania w określonym miejscu kodu, można nawet zrezygnować z postaci tekstowej i nadać tej informacji formę dźwięku produkowanego przez funkcję MessageBeep(). Wszystkie te metody zilustrowaliśmy na wydruku 5.2.

Wydruk 5.2. Wyprowadzanie informacji testowej w aplikacji GUI

void MyDebugOutput(AnsiString OutputMessage)

{

// pierwsza metoda - wyświetlenie tekstu za pośrednictwem etykiety,

// kontrolki edycyjnej i memo

MainForm->ErrorLabel->Caption = OutputMessage;

MainForm->ErrorEdit->Text = OutputMessage;

MainForm->ErrorMemo->Text = OutputMessage;

// druga metoda - wyświetlenie komunikatu za pomocą ShowMessage()

ShowMessage(OutputMessage);

// trzecia metoda - wyświetlenie komunikatu za pomocą MessageDlg()

MessageDlg(OutputMessage, mtInformation, TMsgDlgButtons() << mbOK, 0);

}

void MyDebugBeep()

{

// czwarta metoda - emisja standardowego sygnału dźwiękowego

MessageBeep(0xFFFFFFFF);

}

W aplikacji wielowątkowej użyteczne jest włączanie do treści komunikatu informacji o identyfikatorze wątku generującego ów komunikat - identyfikator ten otrzymać można za pomocą funkcji GetCurrentThreadId().

Po zakończeniu śledzenia aplikacji komunikaty produkujące informację testową stają się po prostu niepożądane, dlatego jedynym rozsądnym sposobem ich użycia jest uzależnienie ich od jakiegoś symbolu kompilacji warunkowej, zdefiniowanego w trybie testowania aplikacji i nieobecnego w trybie jej normalnego uruchomienia. Symbole kompilacji warunkowej obowiązujące dla całości kodu źródłowego wpisuje się w pole Conditional defines na karcie Directories/Conditionals opcji projektu (należy pamiętać, iż po zmianie listy symboli warunkowych konieczne jest ponowne skompilowanie projektu w trybie Build). W poprzednich wersjach C++Buildera programiści musieli wykonywać tę czynność ręcznie, w wersji 5. została ona nieco zautomatyzowana dzięki przyciskom Full debug i Release na karcie Compiler. Kliknięcie pierwszego z wymienionych przycisków powoduje dodanie do wspomnianej listy symbolu _DEBUG (o ile nie jest on tam już obecny), natomiast przy kliknięciu drugiego przycisku symbol ten jest z listy usuwany (dokładniej: usuwane jest tylko jego ostatnie wystąpienie na tej liście). Jeżeli komuś nie odpowiada nazwa _DEBUG, może ją zmienić stosownie do własnych upodobań, dokonując odpowiednich ustawień w rejestrze systemu - należy mianowicie przypisać żądany symbol do łańcucha o nazwie DebugDefine w kluczu HKEY_CURRENT_USER\Software\Borland\C++ Builder\5.0\Debugging. W tym rozdziale zakładamy, iż w trybie śledzenia zdefiniowany jest symbol _DEBUG.

Zaprezentowane na wydruku 5.2 metody wyświetlania tekstu nie sprawdzają się jednak w przypadku śledzenia metody Paint() któregoś komponentu, bowiem w trakcie obsługi „właściwego” komunikatu WM_PAINT pojawia się kolejny taki komunikat związany z wyświetlaniem informacji testowej; w efekcie wyświetlanie zasadniczego komponentu zostanie poważnie zakłócone. Znacznie wygodniejszym sposobem produkowania komunikatów testowych jest użycie funkcji OutputDebugString(), wypisującej łańcuch (przekazany jako argument) do dziennika zdarzeń (View|Debug Windows|Event Log), ale tylko wówczas, gdy aplikacja uruchamiana jest z poziomu IDE - przy uruchomieniu aplikacji w normalnym trybie funkcja OutputDebugString() nie daje żadnych zewnętrznych oznak swego działania, co z pewnością jest jej dodatkową zaletą.

Poza możliwością bezpośredniego wykorzystania funkcja OutputDebugString() dostępna jest również w „obudowanej” formie, mianowicie w postaci makr o nazwach TRACE i WARN; uzupełniają one komunikat przekazany przez użytkownika nazwą pliku i numerem wiersza, w którym występują. Makro TRACE wymaga pojedynczego argumentu, stanowiącego przedmiotowy komunikat, makro WARN wymaga ponadto wyrażenia testowego - komunikat (podany jako drugi parametr) wyświetlany jest jedynie wówczas, gdy wynikiem wartościowania wyrażenia testowego (podanego jako pierwszy parametr) jest true. Aby wspomniane makra były dostępne, należy zdefiniować (za pomocą dyrektywy #define) symbole __TRACE i __WARN oraz dołączyć plik nagłówkowy <checks.h>.

Przykład wykorzystania makr TRACE i WARN prezentujemy na wydruku 5.3. Zauważ, iż ich dostępność uwarunkowana jest zdefiniowaniem symbolu _DEBUG, tylko wówczas są bowiem definiowane symbole __TRACE i __WARN. Mimo iż funkcja OutputDebugString() nie manifestuje się zewnętrznie w czasie normalnego wykonania, to jednak jej wywołania z oczywistych względów wpływają ujemnie na efektywność. Wykorzystane makro TRACEF podobne jest do makra TRACE z tą różnicą, iż dodatkowo dołącza do komunikatu nazwę funkcji, w której zostało wywołane.

Wydruk 5.4. Wykorzystanie makr TRACE i WARN

#ifdef _DEBUG

#define __TRACE

#define __WARN

#endif

#include <checks.h>

#pragma option -w-ccc

#define TRACEF(s) TRACE(__FUNC__ ": " << s)

void MyFunc(AnsiString Title, int *MyArray, int Max)

{

int i,

Sum = 0;

TRACE("Proste śledzenie ");

TRACEF("Śledzenie wraz z nazwą funkcji");

for (i = 0 ; i < 10 ; i++)

{

Sum += A[i];

Sum += MyArray[i];

}

TRACE("The sum is " << Sum);

WARN(Sum > Max,

"Sum jest zbyt duże! Maksymalna dopuszczalna wartość to " << Max);

OutputDebugString(Title.c_str());

}

Pewnego wyjaśnienia wymaga dyrektywa preprocesora #pragma option -w-ccc. Otóż makra TRACE i WARN zdefiniowane są w formie pętli do { } while (0), na co inteligentny kompilator reaguje ostrzeżeniem, iż pętla być może nigdy się nie skończy, bo warunek jej kontynuacji zawsze będzie prawdziwy („Condition is always true”); wspomniana dyrektywa #pragma blokuje generowanie tego ostrzeżenia. Ten sam efekt można osiągnąć za pomocą przycisku Warnings na karcie Compiler opcji projektu, likwidując zaznaczenie ostrzeżenia o numerze W8008.

Zazwyczaj pożądanym elementem informacji testowej jest informacja o wartości wybranej zmiennej (w formie nazwa=wartość) czy nawet prostego wyrażenia (np. A+B=13). Jakkolwiek zawsze można to wykonać „na piechotę”, to jednak podejście takie jest uciążliwe i mało elastyczne. Z pomocą przychodzi w tym momencie dyrektywa #, udostępniająca tekstową formę przekazanego do makra parametru; jej wykorzystanie ilustrujemy na wydruku 5.4.

Wydruk 5.4. Wykorzystanie tekstowej postaci parametru makra

#ifdef _DEBUG

#define __TRACE

#define __WARN

#endif

#include <checks.h>

#pragma option -w-ccc

#define DEBUGEXP(x) TRACE(__FUNC__ ": " #x "=" << (x))

void MyFunc()

{

int A = 6,

B = 7;

DEBUGEXP(A);

DEBUGEXP(A+B);

}

Zdefiniowane makro DEBUGEXP formuje łańcuch, zawierający nazwę funkcji, tekstową postać wyrażenia testowego i wynik jego wartościowania, a następnie przekazuje ów łańcuch jako argument do makra TRACE.

Posługując się opisanymi tutaj makrami, należy zachować szczególną ostrożność w sytuacji, gdy wyrażenia testowe przekazywane jako ich parametry powodują efekty uboczne, na przykład autoinkrementację (autodekrementację) zmiennej. Mianowicie jeżeli instrukcja DEBUGEXP(++a) wystąpi w obszarze uwarunkowanym zdefiniowaniem symbolu _DEBUG, to symbol ten będzie miał jednocześnie wpływ na wartość zmiennej a - przy jego braku autoinkrementacja zmiennej (związana z wywołaniem DEBUGEXP) po prostu nie nastąpi!

Pewną interesującą propozycję wykorzystania makra TRACE przedstawia wydruk 5.5. Definiowane są tutaj dwa podobne makra, otrzymujące jako argument wywołania kompletną instrukcję, która jest przez nie wykonywana oraz dokumentowana za pomocą makra TRACE (kolejność wykonania obydwu tych czynności stanowi jedyną różnicę pomiędzy obydwoma definiowanymi makrami).

Wydruk 5.5. Dokumentowanie wykonania instrukcji kodu źródłowego

#ifdef _DEBUG

#define __TRACE

#define __WARN

#endif

#include <checks.h>

#pragma option -w-ccc

#define DEBUGTRCN(s) TRACE("CMD: " #s); s

#define DEBUGTRCC(s) s; TRACE("CMD: " #s)

void MyFunc()

{

int i,

Sum;

DEBUGTRCN(Sum = 0);

DEBUGTRCN(for (int i = 0 ; i < 10 ; i++) {);

DEBUGTRCN(Sum += i);

DEBUGTRCC(});

DEBUGTRCN(if (Sum > 20) {);

DEBUGTRCN(ShowMessage("Suma jest duża"));

DEBUGTRCC(} else {);

DEBUGTRCN(ShowMessage("Suma jest mała"));

DEBUGTRCC(});

}

Dla większości dokumentowanych instrukcji odpowiednie jest makro DEBUGTRCN, jednak dla instrukcji zawierającej zamykający nawias klamrowy (}) konieczne jest użycie makra DEBUGTRCC, co zresztą wyraźnie widać na powyższym wydruku. Ponadto, ponieważ makra te produkują dwie instrukcje w jednym wywołaniu, nie mogą być użyte tam, gdzie dopuszczalna jest tylko jedna instrukcja, na przykład w instrukcji if lub for, nie używającej nawiasów grupujących { }. W poniższym przykładzie:

DEBUGTRCN(for (int i = 0 ; i < 10 ; i++) );

DEBUGTRCN(Sum += i);

instrukcja Sum += i wykona się tylko raz, a nie dziesięć razy, jak można by przypuszczać na pierwszy rzut oka.

Wykorzystanie asercji

Asercje są wygodnym środkiem weryfikowania prawdziwości założonych warunków. W swej naturze asercja podobna jest do makra WARN z tą różnicą, iż reaguje na fałszywość wyrażenia testowego przekazanego jako jedyny parametr i może powodować wygenerowanie wyjątku lub przerwanie pracy programu.

Właściwe umiejscowienie asercji w kodzie programu nie jest rzeczą prostą; wskazane jest ich tworzenie „na gorąco”, w momencie kodowania, kiedy to właśnie uwaga programisty skupiona jest na odnośnym fragmencie kodu.

Mimo iż asercje stanowią jeden ze środków przydatnych na etapie uruchamiania i testowania aplikacji, może okazać się sensowne pozostawienie niektórych z nich w ostatecznej wersji programu. Owa „ostateczna” wersja zazwyczaj ostateczną tak naprawdę nie jest, znakomita większość programów podlega bowiem okresowym unowocześnieniom i procesom przystosowywania do nowych warunków; jeżeli w tych nowych warunkach niektóre założenia przestaną być prawdziwe, fakt ten stanie się oczywisty, jeżeli będą one sprawdzane odpowiednią asercją.

Pod względem składniowym asercje są makrami wywoływanymi w następujący sposób:

assert(wyrażenie boolowskie)

zaś ich wykorzystywanie wymaga dołączenia pliku nagłówkowego <assert.h>. Jeszcze raz przypominamy, iż asercje sygnalizują fałszywość przekazanego wyrażenia, powinny więc mieć formę warunku, którego spełnienie zakładamy w danym miejscu kodu.

Wydruk 5.6 ilustruje użycie asercji do badania poprawności parametrów wywołania funkcji.

Wydruk 5.6. Przykład użycia asercji

....

#include <assert.h>

void MyFunc(int Width)

{

int Length;

// Width musi być różne od zera

assert(Width != 0);

Length = Area/Width;

// Length musi być mniejsze niż MaxLength

assert(Length < MaxLength);

....

}

void DisplayNode(TNode *Node)

{

// wskaźnik do węzła nie może być pusty

assert(Node != (TNode *)NULL);

....

}

Niespełnienie asercji powoduje wypisanie komunikatu Assertion failed: zawierającego znakową reprezentację testowanego wyrażenia, nazwę modułu źródłowego i numer wiersza (w ramach pliku), w którym znajduje się niespełniona asercja. Rysunek 5.1 przedstawia komunikat informujący o zerowej (a więc niezgodnej z założeniami) wartości parametru Width funkcji prezentowanej na wydruku 5.6.

Rysunek 5.1. Sygnalizacja niespełnionej asercji w aplikacji GUI

Istnieje bardzo prosty sposób zablokowania wszystkich asercji w danym module - należy mianowicie zdefiniować symbol NDEBUG przed dyrektywą dołączającą plik <assert.h>. Plik ten zawiera następującą definicję makra assert:

#ifdef NDEBUG

#define assert(p) ((void)0)

#else

#define assert(p) ((p) ? (void)0 : _assert(#p, __FILE__, __LINE__))

#endif

zatem w obecności zdefiniowanego symbolu NDEBUG asercja nie wykonuje żadnej akcji niezależnie od prawdziwości testowanego wyrażenia.

Podobnie jak w przypadku makra WARN, należy unikać używania w charakterze parametrów asercji wyrażeń powodujących efekty uboczne; zignorowanie tej zasady może spowodować, iż usunięcie z kodu źródłowego którejś asercji zmieni zasadniczą treść tego kodu.

Globalna obsługa wyjątków

Niektórzy programiści preferują używanie - w uzupełnieniu do asercji lub zamiast nich -globalnych funkcji obsługi wyjątków nie obsłużonych na niższych poziomach wywołania. Znacznie prostszym rozwiązaniem jest skompilowanie aplikacji z ustawionymi opcjami Enable Exception i Location Information na karcie C++ opcji projektu (lub z przełącznikiem -xp przy kompilowaniu z wiersza poleceń), dzięki czemu możliwe będzie użycie trzech następujących funkcji (należy ponadto dołączyć plik nagłówkowy <except.h>):

Oto elementarny przykład wspomnianej obsługi z wykorzystaniem wymienionych funkcji - blok catch powinien znajdować się na najwyższym poziomie kodu, a więc w ramach funkcji głównej:

#include <except.h>

....

try

{

...

}

catch ( Exception &e )

{

ShowMessage(e.Message + "\nTyp wyjątku: " + __ThrowExceptionName()+

"\nPlik: " + __ThrowFileName() +

"\nLinia nr:" + AnsiString(__ThrowLineNumber()));

}

Specyficzne uwarunkowania semantyczne

Każdy język programowania charakteryzuje się swoistymi właściwościami składni, czyniącymi niektóre błędy bardziej prawdopodobnymi i trudniejszymi do wykrycia od innych. W przypadku języka C++ istnieje kilka takich uwarunkowań, z których najważniejsze wymieniamy poniżej:

Zintegrowany debugger C++Buildera

Zintegrowany debugger nie ustępuje swą jakością innym elementom C++Buildera, udostępniając programistom w czytelnej postaci wiele aspektów wykonywanego programu - wartościowanie wyrażeń testowych, podgląd wartości zmiennych, punkty przerwań (breakpoints), deasemblację kodu (w kontekście instrukcji źródłowych), pracę krokową itp., lecz także możliwości zdalnego śledzenia (remote debugging) oraz śledzenie w obrębie kilku procesów jednocześnie (cross-process debugging). Jak więc widać, debugger ten nie jest jedynie narzędziem do znajdowania błędów; jest on pełnoprawnym narzędziem projektowym, udostępniającym wiele cennych informacji na temat różnorodnych cech stworzonego kodu.

Automatyczna optymalizacja kodu, o której pisaliśmy w poprzednim rozdziale, jest w przypadku śledzenia aplikacji czynnością wysoce niepożądaną. Wszelkiego rodzaju zabiegi usprawniające - grupowanie i zmiana kolejności instrukcji, eliminacja „martwego” kodu - zachowują oczywiście funkcjonalną odpowiedniość generowanych binariów z kodem źródłowym, jednocześnie jednak zatracając zazwyczaj odpowiedniość leksykalną, czyli możliwość wskazania w wygenerowanym kodzie fragmentu ściśle związanego z konkretną instrukcją. Odpowiedniość taka niezbędna jest w sytuacji, gdy prowadzący proces śledzenia programista (lub tester) postrzega kod źródłowy w kontekście jego pojedynczych instrukcji, zwłaszcza w momencie ustanawiania punktu przerwania na którejś z nich.

Mnogość funkcji zintegrowanego debuggera przejawia się także w bogactwie jego różnorodnych okien, co dla początkującego programisty może stwarzać pewne problemy związane z układem graficznym ekranu. Wielce pomocny jest w tej sytuacji fakt, iż C++Builder 5 umożliwia zdefiniowanie dwóch odrębnych układów pulpitu - dla śledzenia programu i dla jego normalnego wykonywania. Zalecany przez Autora oryginału układ pulpitu dla debuggera przedstawiony jest na rysunku 5.2. Widzimy tam okna podglądu zmiennych lokalnych, stosu wywołania i wyrażeń testowych zdokowane w jedną całość (A), okno edytora kodu (B) i okno CPU (C). W razie potrzeby wyświetlane jest (bez dokowania) okno inspektora śledzenia (Debug Inspector), przykrywając częściowo okna w części A oraz okno dziennika zdarzeń (Event Log) dokowane w dolnej części rozciągniętego w dół okna edytora kodu (nie ma już wówczas miejsca na okno CPU). Aby to wszystko pomieścić na ekranie, pożądana jest duża rozdzielczość, np. 1280×1024 pikseli.

Rysunek 5.2. Przykładowy układ pulpitu C++Buildera podczas śledzenia aplikacji

W dalszej części rozdziału zakładamy, iż Czytelnik posiada podstawową wiedzę na temat podstawowych czynności wykonywanych za pomocą debuggera - pracy krokowej (z wchodzeniem do wnętrza wywoływanych funkcji lub nie), ustanawiania punktów przerwań (z użyciem licznika przejść lub bez) i podglądania wartości wyrażeń za pomocą podpowiedzi kontekstowych ToolTips (uzyskiwanych przy zatrzymaniu kursora myszki na nazwie zmiennej) - skoncentrujemy się więc na zagadnieniach bardziej zaawansowanych.

Zaawansowane wykorzystanie punktów przerwań

Oprócz możliwości wstrzymywania wykonania programu na określonych wierszach kodu źródłowego (source breakpoints) możliwe jest uwarunkowanie tego wstrzymywania innymi okolicznościami przydatnymi w różnych przypadkach śledzenia; za chwilę przedstawimy kilka nowości wersji 5. C++Buildera w zakresie punktów przerwań.

Wstrzymanie z powodu ładowania modułu (module load breakpoint) jest szczególnie użyteczne w przypadku ładowania biblioteki DLL, umożliwiając precyzyjne ustalenie punktu wejścia do niej lub do pakietu. Ustawienia tego rodzaju punktu przerwania można dokonać na dwa sposoby.

Pierwszy sposób odnosi się do uruchomionej z poziomu IDE aplikacji. Należy wybrać z menu głównego opcję View|Debug Windows|Modules, w wyniku czego wyświetlone zostanie okno pokazane na rysunku 5.3. W lewej części tego okna należy zlokalizować i podświetlić żądany moduł - jeżeli nie ma go na liście, to oznacza, iż nie został jeszcze załadowany i należy go dodać do tej listy w sposób jawny za pomocą opcji Add Module z menu kontekstowego (uruchamianego prawym kliknięciem w dowolnym miejscu listy). Z menu kontekstowego podświetlonego modułu należy następnie wybrać opcję Break On Load (lub nacisnąć klawisz F5). Jeżeli dany moduł został już załadowany, ustanowiony punkt przerwania stanie się skuteczny dopiero w momencie ponownego ładowania tegoż modułu - czy to po jego dynamicznym rozładowaniu, czy też w wyniku restartu aplikacji.

Rysunek 5.3 Ustanawianie punktu przerwania związanego z ładowaniem modułu

Drugi sposób ma zastosowanie do aplikacji jeszcze nie uruchomionej: po wybraniu opcji Run|Add Breakpoint|Module Load Breakpoint wyświetlone zostanie okno umożliwiające określenie nazwy modułu, z którego ładowaniem (w uruchomionej aplikacji) związany ma zostać punkt przerwania.

Dwa kolejne rodzaje punktów przerwania związane są ze sprzętowymi mechanizmami ustanawiania pułapek na adresie kodu lub danych. Pułapka na adresie instrukcji (address breakpoint) podobna jest w swej naturze do zwykłego punktu przerwania, ustanawianego na określonym wierszu kodu źródłowego, lecz w przeciwieństwie do niej ustanawiana jest na konkretnym zakresie adresów liniowych wykonywanego kodu. Jest to szczególnie użyteczne w przypadku śledzenia kodu binarnego zewnętrznych modułów (w oknie CPU). W przypadku, gdy określony adres odpowiada wprost konkretnemu wierszowi kodu źródłowego aplikacji, na wierszu tym zostanie ustanowiony „normalny” punkt przerwania. Funkcjonowanie tego mechanizmu można zaobserwować na przykładzie projektu BreakpointProj.bpr, znajdującego się na załączonej płycie CD-ROM. Po jego skompilowaniu i uruchomieniu należy poczekać, aż wyświetli się formularz główny i wybrać z menu głównego opcję Run|Program Pause. Wykonanie programu zostanie wówczas zatrzymane i wyświetli się okno CPU, wskazujące instrukcję kodu maszynowego, która właśnie miała zostać wykonana. Kierując się adresami liniowymi, znajdującymi się z lewej strony poszczególnych instrukcji, można zastawić punkt przerwania (pułapkę) na którejkolwiek z nich. Można to uczynić na kilka sposobów, na przykład podświetlając żądany rozkaz maszynowy i naciskając klawisz F5, bądź wybierając opcję Run|Add Breakpoint|Address Breakpoint i określając żądany adres.

Aby skojarzyć konkretną instrukcję kodu źródłowego z konkretnym rozkazem maszynowym w oknie CPU, należy załadować żądany moduł do okna edytora kodu (za pomocą klawiszy Ctrl+F12), zlokalizować żądaną instrukcję, kliknąć ją prawym przyciskiem myszy i z wyświetlonego menu kontekstowego wybrać opcję Debug|View CPU; na rozkazie maszynowym rozpoczynającym przekład wybranej instrukcji źródłowej zostanie wówczas ustanowiony punkt przerwania.

Pułapka na danych oddaje nieocenione usługi w przypadku, gdy konieczne jest zlokalizowanie instrukcji zmieniającej zawartość wskazanej zmiennej lub obiektu. Aby ustawić ten rodzaj punktu przerwania, należy użyć opcji Run|Add Breakpoint|Data Breakpoint i w polu Address wyświetlonego okna wskazać żądany element danych - w przypadku naszego projektu BreakpointProj może to być np. prywatne pole Form1->FClickCount, zliczające kliknięcia przycisku DataBreakpointButton. Za każdym razem, gdy pole to zostanie zmodyfikowane (czyli po każdym kliknięciu wspomnianego przycisku) wykonanie aplikacji zostanie zatrzymane; podświetlony zostanie wówczas ten rozkaz w oknie CPU, który modyfikuje odnośny element danych, a także ewentualna instrukcja kodu źródłowego (w edytorze kodu), mająca odniesienie do tego adresu, o ile oczywiście takowa istnieje.

Oczywiście adres pułapki na danych może być również wyrażony w formie bezpośredniej (w postaci liczby szesnastkowej, rozpoczynającej się sekwencją 0x) - adres liniowy odpowiadający konkretnemu elementowi danych uzyskać można za pomocą opcji Run|Inspect.

Pułapkę na danych można również ustanowić za pomocą okna Watch List (View|Debug Windows|Watches), wybierając z menu kontekstowego określonej danej opcję Break When Changed.

Mechanizm pułapki na danych ustanawiany jest na podstawie adresu liniowego odnośnej danej, może więc być stosowany do tych elementów danych C++, które wprost przekładają się na konkretny obszar pamięci wirtualnej - czyli np. zmiennych i pól obiektu, lecz już nie właściwości modyfikowanych za pomocą funkcji dostępowych Setxxx. Modyfikacja każdej takiej właściwości wiąże się z wywołaniem stowarzyszonej z nią funkcji dostępowej, a więc dla wychwycenia takiej modyfikacji należy ustawić odpowiedni punkt przerwania wewnątrz tejże funkcji.
Jeżeli dla danej funkcji dostępowej nie jest dostępny kod źródłowy, bądź jest on trudny do zlokalizowania, można zamiast źródłowego punktu przerwania ustawić odpowiednią pułapkę na adresie instrukcji. Dla przykładu rozpatrzmy tytuł (Caption) etykiety ClickCountLabel naszego projektu BreakpointProj. Do okna wyświetlonego w wyniku wybrania opcji (Run|Inspect) wpisujemy Form1->ClickCountLabel i klikamy przycisk OK. W wyświetlonym oknie inspektora śledzenia przechodzimy na kartę Properties i odszukujemy właściwość Caption; stwierdzamy wówczas, iż funkcją modyfikującą właściwość Caption jest SetText(). Przechodzimy więc na kartę Methods i odczytujemy adres funkcji SetText() - jest to właśnie adres, na którym zastawić należy rzeczoną pułapkę.

Aby wychwycić modyfikację obiektu typu AnsiString (na przykład właściwości tego typu nie korzystającej z metody dostępowej), należy ustawić pułapkę (data breakpoint) na polu .Data tego obiektu - można to sprawdzić np. na polu Form1->FSomeString naszego projektu BreakpointProj.

Pole Length w oknie Add Data Breakpoint powinno być wypełniane tylko dla danych nieskalarnych, jak np. struktury i tablice. Wychwytywana będzie wówczas każda próba modyfikacji któregokolwiek bajta w zaznaczonym zakresie.

Ze względu na to, iż adresy liniowe poszczególnych elementów aplikacji różne są na ogół przy różnych jej uruchomieniach, adekwatność wszelkich pułapek na adresach instrukcji i danych gwarantowana jest jedynie do zakończenia aktualnego wykonywania aplikacji.

Nowości C++Buildera 5 związane z punktami przerwań

W obecnej wersji C++Buildera punkty przerwań mogą być organizowane w grupy, można im też przypisywać określone akcje; można też spowodować, by w przypadku natrafienia na określony punkt przerwania został dokonany wpis do dziennika zdarzeń (Event Log), będący komunikatem tekstowym lub wynikiem wartościowania podanego wyrażenia testowego. Umożliwia to zaprogramowanie zróżnicowanych możliwości punktów przerwań, na przykład ich skuteczności wyłącznie podczas wykonywania określonej sekcji kodu.

Sterowanie aktywnością obsługi wyjątków (Ignore/Handle subsequent exceptions) umożliwia selektywne śledzenie określonych fragmentów kodu, związanych z określonymi problemami, bez zaprzątania uwagi wyjątkami pochodzącymi z innych obszarów.

Okno właściwości punktu przerwania dostępne jest zarówno za pośrednictwem opcji Run|Add Breakpoint, jak i z menu kontekstowych poszczególnych punktów przerwań w oknie Breakpoint List oraz z menu kontekstowego oznacznika punktu przerwania na gutterze w oknie edytora kodu.

Okna zintegrowanego debuggera

Zintegrowany debugger umożliwia wgląd w wiele aspektów wykonywanej aplikacji - listę załadowanych modułów, stos wywołania, wartości zmiennych i wyrażeń, wygenerowany kod maszynowy itp. Dostęp do poszczególnych okien, wyświetlających żądaną informację, możliwy jest poprzez opcję View|Debug Windows. W dalszej części przedstawimy kilka zaawansowanych możliwości, związanych z wykorzystaniem niektórych z tych okien.

W wersji 5. C++Buildera lista okien zintegrowanego debuggera wzbogaciła się o okno FPU, pokazujące zawartość rejestrów zmiennoprzecinkowych lub (zamiennie) rejestrów MMX oraz znaczników związanych z arytmetyką zmiennoprzecinkową.

Okno CPU

Okno CPU ukazuje niskopoziomowe aspekty wykonywanej aplikacji - wygenerowany kod maszynowy, zawartość rejestrów ogólnego przeznaczenia i segmentowych, stan znaczników a także zawartość stosu w pobliżu jego wierzchołka oraz zawartość wybranego obszaru pamięci wirtualnej. Przykładowe okno CPU wyświetlone w czasie wykonania aplikacji BreakpointProj przedstawia rysunek 5.4.

Rysunek 5.4. Okno CPU zintegrowanego debuggera

Największą część okna CPU zajmuje panel deasemblacji. Ukazuje on kod maszynowy, powstały w wyniku tłumaczenia kodu źródłowego na rozkazy procesora, wraz z aktualnymi adresami liniowymi tych rozkazów. Powyżej tego panelu wyświetlany jest efektywny adres operandu pamięciowego bieżąco wykonywanego rozkazu (o ile rozkaz ten w ogóle ma odniesienie do pamięci) i aktualną wartość tego operandu, jak również identyfikator śledzonego właśnie wątku. W oknie na rysunku 5.4 bieżąco wykonywany rozkaz (dec dword ptr [ebp-0x0c]) odwołuje się do 32-bitowego słowa pod adresem określonym przez odjęcie od zawartości rejestru EBP wartości 0x0c, czyli 0x63f378 - 0x0c = 63f36c; jak łatwo zauważyć, słowo to ma obecnie wartość 1.

Jeżeli projekt skompilowany zostanie z ustawioną opcją Debug Information na karcie Compiler, panel deasemblacji zawierać będzie także przyporządkowanie poszczególnych fragmentów kodu wynikowego do instrukcji kodu źródłowego, w wyniku translacji których fragmenty te powstały - jak na rysunku 5.4.

Panel deasemblacji umożliwia prowadzenie pracy krokowej na poziomie poszczególnych rozkazów; aktualny rozkaz do wykonania wskazywany jest przez małą zieloną strzałkę. Możliwe jest również ustanawianie punktów przerwań na poszczególnych rozkazach - w taki sam sposób, jak czyni się to w oknie edytora kodu. Wyświetlane w wyniku prawego kliknięcia menu kontekstowe umożliwia wykonywanie kilku bardzo pomocnych operacji, ma przykład lokalizację bieżąco wykonywanego rozkazu, skok do wskazanego adresu, zmianę wątku itp.

Bezpośrednio na prawo od panelu deasemblacji w górnej części okna CPU znajduje się panel ukazujący zawartość rejestrów ogólnego przeznaczenia i rejestrów segmentowych; zawartość zmieniona przez ostatnio wykonywany rozkaz wyświetlana jest w kolorze czerwonym. Zawartość każdego z rejestrów może być doraźnie modyfikowana za pomocą menu kontekstowego (uruchamianego prawym kliknięciem odnośnej pozycji).

Z panelem rejestrów sąsiaduje panel znaczników, ukazujący stan znaczników w 32-bitowym rejestrze EFLAGS; każdy ze znaczników może być doraźnie zmieniany za pomocą menu kontekstowego.

Panel poniżej panelu deasemblacji - zwany panelem pamięci (memory dump pane) - służy do wyświetlania zawartości wskazanego obszaru pamięci. Adres żądanego obszaru oraz postać jego wyświetlania ustalić można za pomocą menu kontekstowego; poszczególne bajty obszaru wyświetlane są ponadto (w miarę możności) w postaci znaków ASCII.

Ostatni z paneli okna CPU - panel stosu (stack pane) - zlokalizowany jest w prawej dolnej części okna i odzwierciedla zawartość stosu w pobliżu jego wierzchołka, określonego przez zawartość rejestru ESP (dokładniej - przez parę rejestrów SS:ESP - przyp. tłum.).

Okno stosu wywołania

Okno stosu wywołania (call stack view) wskazuje zagnieżdżenie wywołań funkcji prowadzących do bieżąco wykonywanego rozkazu. Kolejne pozycje w oknie, poczynając góry, odpowiadają coraz bardziej „zewnętrznym” poziomom wywołania - tak więc najwyższa pozycja odpowiada wywołaniu funkcji bieżąco realizowanej. Znajomość stosu wywołań jest szczególnie użyteczna w sytuacji, gdy diagnozowany błąd związany jest nie tyle z samą funkcją, w czasie realizacji której występuje, lecz raczej z wywołaniem tej funkcji w określonym miejscu kodu.

Podwójne kliknięcie którejś z pozycji okna spowoduje wyświetlenie tego wiersza w kodzie źródłowym, w którym przekładzie znajduje się wywołanie związane z tą pozycją; jeżeli dana pozycja nie posiada odniesienia do (dostępnego) kodu źródłowego, wyświetlane jest okno CPU, podświetlające odnośny rozkaz w kodzie wynikowym.

Nowością w wersji 5. C++Buildera jest wyświetlanie informacji o zmiennych lokalnych i parametrach związanych z danym wywołaniem, jeżeli taka informacja jest dostępna.

Okno wątków

Testowanie i śledzenie aplikacji wielowątkowej jest zadaniem dość złożonym. Wątki wykonywane są asynchronicznie, niezależnie od siebie, czasami jednak przesyłają sobie nawzajem komunikaty (za pomocą funkcji PostThreadMessage()) lub korzystają z różnorodnych mechanizmów synchronizacji, jak np. mutexy.

Podczas gdy jeden z wątków zatrzyma się na punkcie przerwania, może to sparaliżować pracę pozostałych wątków ze względu na uwarunkowania czasowe lub synchronizacyjne; w pewnych sytuacjach nawet spowolnienie wykonania spowodowane śledzeniem może być katastrofalne dla całej aplikacji wielowątkowej - choć z drugiej strony, wobec nieprzewidywalności środowiska, w którym się ona wykonuje, poleganie na „wyśrubowanych” reżimach czasowych z pewnością nie jest dobrą praktyką programistyczną.

Dużym ułatwieniem w tej sytuacji jest więc dostarczane przez zintegrowany debugger okno wątków (threads view), wyświetlające status każdego z procesów biorących udział w aplikacji oraz poszczególnych wątków każdego z tych procesów. Procesy wyświetlane są w oknie zgodnie z ich hierarchiczną zależnością - proces uruchomiony jako pierwszy występuje w liście najwcześniej - podobnie jak wątki każdego z procesów (jako pierwszy wyświetlany jest wątek główny). Dla procesów pobocznych (secondary) status określony jest jako Spawned, Attached albo Cross-Process Attach. Status wątku może być określony jako Runnable, Stopped, Blocked lub None. Okno wątków z przykładową zawartością przedstawia rysunek 5.5.

Rysunek 5.5. Przykładowe okno wątków

Niezależnie od liczby procesów i wątków składających się na aplikację, czas procesora przydzielony jest w danym momencie konkretnemu wątkowi, zwanemu wątkiem bieżącym (current thread); proces, do którego ów wątek przynależy, określany jest mianem procesu bieżącego (current process). Zarówno bieżący proces, jak i bieżący wątek oznaczone są w oknie wątków zieloną strzałką, co zresztą widać na rysunku 5.5. Menu kontekstowe każdego z procesów i wątków umożliwia uczynienie danego procesu (wątku) bieżącym (Make Current), a także odnalezienie (w miarę możliwości) kodu źródłowego związanego z danym procesem (wątkiem) (View Source i Go to Source).

Okno modułów

Okno modułów udostępnia informację na temat wszystkich załadowanych bibliotek DLL i pakietów, umożliwiając ustanowienie punktu przerwania zatrzymującego pracę aplikacji w momencie ładowania konkretnego modułu - pisaliśmy o tym nieco wcześniej w tym rozdziale. Rysunek 5.6 przedstawia okno modułów związanych z pewnym etapem wykonania aplikacji „krzyżówkowej” CrozzleProj, opisywanej w rozdziale 4.

Rysunek 5.6. Okno modułów aplikacji „krzyżówkowej” z rozdziału 4.

Okno modułów składa się z trzech paneli. Lewy górny panel zawiera listę wszystkich modułów wraz z ich adresami bazowymi i kompletną lokalizacją dyskową. należy zwrócić uwagę, iż wyświetlane adresy bazowe są faktycznymi adresami ładowania modułów, niekoniecznie zgodnymi z domyślnymi ustawieniami podczas konsolidacji (Image base). Podświetlając dowolny z modułów i naciskając klawisz F5, można ustanowić związany z tym modułem punkt przerwania.

Lewy dolny panel zawiera informację o hierarchicznej zależności modułów źródłowych projektu. Podświetlając żądaną pozycję i wybierając opcję View Source z jej menu kontekstowego, spowodujemy załadowanie reprezentowanego przez nią modułu do okna edytora kodu.

Zawartość trzeciego panelu stanowi lista punktów wejścia (entry points) do modułu aktualnie podświetlonego w pierwszym panelu. Podświetlając dowolną pozycję i naciskając klawisz Enter, spowodujemy wyświetlenie odnośnego punktu wejścia w edytorze kodu lub oknie CPU, zależnie od dostępności kodu źródłowego.

Okno FPU

Okno to jest nowością wersji 5. C++Buildera i ukazuje zmiennoprzecinkowe aspekty śledzonej aplikacji. Tak więc widzimy w nim (rys. 5.7) status i zawartość rejestrów danych, rejestru stanu i znaczników jednostki zmiennoprzecinkowej oraz adres i kod ostatnio wykonywanej instrukcji zmiennoprzecinkowej; zawartość rejestrów zmodyfikowanych przez tę instrukcję wyświetlana jest w kolorze czerwonym.

Rysunek 5.7. Okno FPU z przykładową zawartością

Ze względu na zamienne traktowanie przez systemy operacyjne rejestrów zmiennoprzecinkowych i rejestrów MMX zawartość tych ostatnich może być wyświetlana zamiennie z rejestrami danych w lewym panelu - „przełączanie” zawartości panelu następuje poprzez użycie klawiszy Ctrl+F lub Ctrl+X albo za pomocą opcji Show z menu kontekstowego.

Podgląd i modyfikacja wyrażeń testowych

Podgląd zmiennych i wyrażeń (watch) stanowi naturalny sposób testowania aktualnej zawartości zmiennych i obiektów oraz wartości wyrażeń tworzonych z ich udziałem. Listę śledzonych zmiennych (watch list) udostępnia opcja View|Debug Windows|Watches menu głównego. Dodanie nowej pozycji do tej listy możliwe jest poprzez naciśnięcie klawisza Ins albo Ctrl+A bądź wybranie opcji Add Watch z menu kontekstowego (jeżeli lista nie jest widoczna, można użyć w tym celu opcji Run|Add Watch menu głównego lub klawiszy Ctrl+F5).

Okno Watch Properties, służące do określania właściwości dodawanej (lub istniejącej) pozycji listy, umożliwia określenie jej najistotniejszych cech, jak format i precyzja wyświetlania. Jeżeli obliczenie wartości wyrażenia lub jego części jest niemożliwe - na przykład ze względu na błąd w jego zapisie lub położenie niektórych jego komponentów poza zakresem widoczności), zamiast wyniku wyświetlony zostanie komunikat undefined symbol. Debugger odmówi także wartościowania wyrażenia powodującego efekty uboczne, jeżeli w oknie właściwości testowanego wyrażenia nie jest zaznaczona opcja Allow Side Effects - zamiast wyniku otrzymamy wówczas komunikat Side effects not allowed. Gdy dana pozycja w liście przestaje być potrzebna, można ją usunąć przez podświetlenie i naciśnięcie klawisza Del.

Poza wyświetlaniem zawartości zmiennych możliwa jest także ich doraźna modyfikacja - służy do tego inne okno o nazwie Evaluate/Modify otwierane za pomocą klawiszy Ctrl+F7 lub opcji Run|Evaluate/Modify menu głównego bądź kliknięcie prawym klawiszem myszki nazwy żądanego obiektu w oknie edytora kodu i wybranie z menu kontekstowego opcji Debug|Evaluate/Modify. Aby otwarcie to było możliwe, aplikacja musi znajdować się w stanie zatrzymania (Stopped).

Kliknięcie przycisku Evaluate na pasku narzędziowym okna Evaluate/Modify spowoduje wpisanie bieżącej wartości zmiennej (określonej w polu Expression) do pól Result i New value; aby nadać zmiennej inną wartość, należy wpisać ją w drugie z wymienionych pól i kliknąć przycisk Modify na pasku narzędziowym. Należy zachować szczególną ostrożność w sytuacji, gdy użyte przy tej okazji wyrażenia (np. funkcje) produkują efekty uboczne - efekty te nie pozostaną bowiem bez wpływu na dalszy bieg aplikacji.

W przeciwieństwie do okna Watch List wyświetlane wartości zmiennych użytych w oknie Evaluate/Modify nie są automatycznie aktualizowane stosownie do ich bieżącej zawartości (wyświetlenie aktualnej wartości wyrażenia wpisanego w pole Expression następuje wyłącznie w wyniku kliknięcia przycisku Evaluate).

W pole Expression może być wpisane dowolne wyrażenie, niekoniecznie pojedyncza zmienna lub obiekt - kliknięcie przycisku Evaluate (na pasku narzędziowym) spowoduje wyświetlenie jego aktualnej wartości. Jeżeli jednak wyrażenie to nie będzie L-wyrażeniem - czyli wielkością dopuszczalną po lewej stronie operatora przypisania - przycisk Modify będzie niedostępny, wyrażenie nie identyfikuje bowiem żadnego obiektu, któremu można by przypisać wartość.

Inspektor śledzenia

Inspektor śledzenia (Debug Inspector) podobny jest do inspektora obiektów. Wyświetla on w skondensowanej formie informacje o polach, metodach i właściwościach określonego obiektu - ściślej: o ich aktualnych wartościach. Przykładowe okno inspektora śledzenia przedstawia rysunek 5.8.

Rysunek 5.8 Okno inspektora śledzenia z przykładową zawartością

Otwarcie okna inspektora śledzenia następuje w wyniku wybrania opcji Run|Inspect z menu głównego (bądź Debug|Inspect z menu kontekstowego obiektu w edytorze kodu) i określeniu obiektu, którego dotyczyć ma wyświetlana informacja. Na pasku tytułowym okna wyświetlany jest identyfikator bieżącego wątku, zaś w polu combo poniżej - nazwa, typ i adres śledzonego elementu. Zależnie od rodzaju śledzonego obiektu (zmienna prosta, egzemplarz klasy) okno zawiera od jednej do trzech kart, zawierających informacje o danych (Data) metodach (Methods) i właściwościach (Properties). Dla tych elementów danych, które mogą być modyfikowane, z prawej strony odnośnego wiersza (na karcie Data) znajduje się przycisk z wielokropkiem (ellipsis) - kliknięcie go spowoduje wyświetlenie okna dialogowego, umożliwiającego wpisanie nowej wartości.

Nowością wersji 5. C++Buildera są trzy dodatkowe mechanizmy, dostępne z menu kontekstowego inspektora śledzenia. Pierwszy z nich - Show Inherited - powoduje przełączanie pomiędzy dwoma trybami wyświetlania elementów definicji obiektu: wyświetlane mogą być jedynie elementy definiowane jawnie w śledzonej klasie, bądź dodatkowo elementy odziedziczone z klas bazowych; przełączanie pomiędzy trybami odbywa się za pomocą klawiszy Ctrl+S. Drugi mechanizm - o nazwie Show Fully Qualified Names - dostępny jest tylko wówczas, gdy włączono wyświetlanie informacji odziedziczonych i umożliwia opcjonalne poprzedzanie nazw wyświetlanych elementów nazwami klas, z których elementy te się wywodzą: do przełączania pomiędzy trybami (prefiksowanie lub jego brak) służą klawisze Ctrl+Q. Trzecią nowością jest możliwość wyboru sortowania wyświetlanych pozycji w oparciu o dwa kryteria: nazwę albo kolejność deklaracji w klasie; nie określono tutaj klawiszy przełączających, wybór kryterium odbywa się wyłącznie za pomocą menu kontekstowego. Domyślne ustawienia wymienionych mechanizmów są przedmiotem sekcji Inspector Defaults karty General opcji debuggera (Tools|Debugger Options).

CodeGuard

CodeGuard jest nowym narzędziem C++Buildera 5, występującym w jego wersjach Professional i Enterprise; występował już wcześniej w Borland C++, obecnie zaadaptowany został również na gruncie C++Buildera. Służy on do kontroli gospodarowania pamięcią i innymi zasobami w czasie wykonywania programu, jak również do weryfikowania wywołań funkcji.

Ujmując rzecz nieco dokładniej, CodeGuard potrafi wykrywać następujące typy błędów wykonania:

Przykładem niepoprawnego gospodarowania pamięcią jest próba powtórnego zwolnienia już zwolnionego obszaru oraz odwoływanie się do obszaru zwolnionego (na podstawie wskaźnika wskazującego uprzednio ów obszar). Podobnymi błędami zajmiemy się szczegółowo w dalszej części rozdziału.

CodeGuard wpisuje raporty o stwierdzonych błędach do specjalnego dziennika, którego zawartość oglądać można za pośrednictwem opcji View|Debug Windows|CodeGuard Log; umożliwia on również odnalezienie tego wiersza w kodzie źródłowym, który był przyczyną danego błędu.

Włączanie do aplikacji i konfigurowanie CodeGuarda

Aby CodeGuard mógł wykonywać swe zadania na rzecz aplikacji, musi zostać do niej dołączony na etapie kompilacji. W tym celu należy przejść na kartę CodeGuard opcji projektu, zaznaczyć pole CodeGuard Validation i ustalić zestaw wykonywanych walidacji, zaznaczając odpowiednie pola poniżej. Dodatkowo należy włączyć opcje Debug information i Line number information w sekcji Debugging na karcie Compiler. Po dokonaniu tych ustawień należy koniecznie wykonać kompletną rekompilację projektu za pomocą opcji Build lub Build All Projects.

Wspomniane trzy opcje, określające zakres walidacji wykonywanych przez CodeGuarda, przedstawione są na rysunku 5.9. Pierwsza z nich uruchamia kontrolę poprawności wskaźników odnoszących się do lokalnych, globalnych i statycznych danych, a także wykrywanie „nadpisywania” tych danych (ang. overrun). Druga włącza kontrolę legalności obiektów, na rzecz których wywoływane są metody - umożliwia to wykrycie wywołania metody obiektu nieistniejącego lub usuniętego. Przedmiotem weryfikacji uruchamianych przez trzecią opcję jest generalna kontrola używanych wskaźników - niestety za cenę 5-10-krotnego spowolnienia tempa wykonywania programu.

Rysunek 5.9. Opcje regulujące funkcjonalny zakres działalności CodeGuarda

Dodatkowe możliwości konfigurowania CodeGuarda kryją się w programie CGCONFIG.EXE dostępnym również za pośrednictwem opcji Tools|CodeGuard Configuration menu głównego; jego okno przedstawia rysunek 5.10. Okno to składa się z czterech kart, z których pierwsza - Preferences - zawiera ustawienie o charakterze ogólnym. Pole Enable umożliwia łatwe wyłączanie i przywracanie funkcjonowania CodeGuarda w aplikacji bez potrzeby jej rekompilowania. Wyłączenie funkcjonowania nie oznacza jednak całkowitego usunięcia CodeGuarda z aplikacji - w tym celu należałoby usunąć zaznaczenie pola CodeGuard Validation na karcie CodeGuard opcji projektu i zrekompilować aplikację w trybie Build.

Rysunek 5.10. Okno programu CGCONFIG.EXE

Druga z opcji - Stack fill frequency - ma związek z kontrolowaniem wykorzystania stosu przez aplikację. CodeGuard dokonuje w tym celu okresowego wypełniania niewykorzystanej części stosu charakterystycznym wzorcem bajtowym, zaś omawiana opcja określa częstotliwość tego wypełniania - poszczególne wartości oznaczają wypełnianie w następujących okolicznościach:

Nietrudno zauważyć, iż wartość ta określa pewien kompromis pomiędzy dokładnością kontroli (zalecane jak najczęstsze wypełnianie) a szybkością wykonania (jak najrzadsze); jak pokazuje doświadczenie, najczęściej opłaca się pozostać przy domyślnej wartości 2.

Sekcje CodeGuard Report i Error Message Box określają sposób raportowania błędów przez CodeGuarda. Zaznaczenie opcji Statistic powoduje produkowanie przez CodeGuarda statystyki dotyczącej przydziału i zwalniania pamięci, odwołań do wybranych funkcji Win32 API i wykorzystania zasobów; statystyka ta uzupełniana jest listą wykorzystywanych modułów na końcu dziennika. Opcja Resource Leaks odpowiedzialna jest za sygnalizowanie - po zakończeniu programu - ewentualnego „wycieku” zasobów. Opcje sekcji Error Message Box określają tytuł i treść (ewentualnego) komunikatu wyświetlanego przez CodeGuarda niezależnie od wpisu do dziennika.

Karty Resource Options i Function Options zawierają opcje określające szczegóły nadzorowania przez CodeGuarda zasobów, uchwytów plikowych i wywołań funkcji. W większości przypadków zadowalające są ustawienia domyślne tych opcji; jedną z opcji, której zaznaczenie może być niekiedy użyteczne, jest opcja Log each call na karcie Function Options, powodująca raportowanie każdego wywołania określonej funkcji (odnosi się ona do funkcji aktualnie podświetlonej).

Za pomocą karty Ignore Modules możliwe jest określenie listy modułów, co do których CodeGuard powinien pozostawać bezczynny.

Wykorzystanie CodeGuarda

W zasadzie większość fatygi związanej z wykorzystaniem funkcji CodeGuarda zamyka się w dołączeniu go do aplikacji i odpowiednim skonfigurowaniu. W uruchomionej aplikacji pozostaje tylko przyglądanie się wpisom do dziennika; po zakończeniu pracy aplikacji dziennik ten dostępny jest w pliku tekstowym NazwaProjektu.cgl, który można oglądać za pomocą dowolnego edytora tekstowego, np. Notatnika lub WordPada. Przeglądanie dziennika z poziomu IDE - inicjowane za pomocą opcji View|Debug Windows|CodeGuard Log lub klawiszy Ctrl+Alt+O - jest jednak bardziej przyjazne dla użytkownika, bowiem C++Builder interpretuje informację zawartą w dzienniku i formatuje ją w czytelny sposób, m.in. grupując poszczególne wpisy według rodzaju zaistniałego błędu. Przykład okna z tak sformatowaną informacją przedstawia rysunek 5.11.

Rysunek 5.11. Przykładowy raport w dzienniku CodeGuarda

Na pasku narzędziowym widoczne są dwa przyciski - Stop i Clear; są one tak naprawdę przełącznikami. Włączenie pierwszego z nich spowoduje wstrzymanie wykonywania aplikacji każdorazowo przy stwierdzeniu błędu przez CodeGuarda; gdy będzie on wyłączony, aplikacja wykonywać się będzie bez zatrzymywania. Włączenie drugiego z przycisków spowoduje czyszczenie dziennika każdorazowo przy starcie aplikacji.

Podwójne kliknięcie którejś z pozycji w oknie CodeGuard Log spowoduje wskazanie wiersza kodu źródłowego (w oknie edytora kodu) odpowiedzialnego za błąd raportowany w tej pozycji; jeżeli kod źródłowy odnośnego fragmentu nie jest dostępny, zaznaczany jest odpowiedni rozkaz maszynowy w oknie CPU. Podobny efekt daje użycie którejś z opcji View Source i Edit Source menu kontekstowego danej pozycji - opcje te różnią się od siebie tylko tym, iż w przypadku pierwszej z nich okno edytora kodu nie staje się oknem aktywnym.

Przykład zastosowania

Na dołączonej do książki płycie CD-ROM znajduje się projekt o nazwie CodeguardProj, zawierający wiele konstrukcji wywołujących typowe błędy wychwytywane przez CodeGuarda i oczywiście typowe jego reakcje na te błędy. Poza oczywistą okazją zobaczenia CodeGuarda w akcji projekt ten stanowi również swoistą wskazówkę odnośnie tego, jak owe błędy przekładają się na konstrukcje programistyczne w C++, a to znakomicie ułatwia programistom zrozumienie istoty tego, co kryje się po postacią lakonicznych komunikatów. Formularz główny projektu (zobacz rys. 5.12) umożliwia selektywne prowokowanie wspomnianych sytuacji - wystarczy jedynie kliknąć przycisk znajdujący się obok stosownego komunikatu. Przyjrzyjmy się więc dokładniej odpowiedzialnym za to fragmentom kodu źródłowego projektu - obejmują one większość, lecz nie wszystkie błędne sytuacje obsługiwane przez CodeGuarda; kompletną ich listę znaleźć można w pliku cg.hlp.

Rysunek 5.12. Formularz główny projektu ilustrującego funkcjonowanie CodeGuarda

Access In Freed Memory - dostęp do zwolnionej pamięci

Po zwolnieniu przydzielonego uprzednio obszaru pamięci wskaźniki wskazujące dotychczas na ten obszar pozostają niezmienione, a to stwarza okazję do (najczęściej niezamierzonego) dalszego korzystania z tego obszaru, czego rezultat oczywiście trudny jest do określenia. W poniższym fragmencie obiekt MyClass jest najpierw zwalniany, po czym następuje odwołanie do jego pola PubVal:

#include <stdio.h>

#include <dir.h>

class TSomeClass

{

int FNumber;

public:

int GetNumber() { return FNumber; }

void SetNumber(int NewNumber) { FNumber = NewNumber; }

int Double(int Val) { return Val*2; }

int PubVal;

};

void MyFunc()

{

TSomeClass *MyClass = new TSomeClass;

delete MyClass;

MyClass->PubVal = 10;

CodeGuard raportując błąd dostępu do zwolnionej pamięci, nie ogranicza się przy tym do wskazania samego faktu jego zaistnienia, lecz w czytelnej postaci informuje o tym, w którym momencie nastąpiło przydzielenie, zwolnienie i nieuprawniony dostęp do przedmiotowego obszaru - to wszystko oczywiście w kategoriach kodu źródłowego.

Method Called On Freed Object - wywołanie metody na rzecz zwolnionego obiektu

Ten błąd jest podobny do poprzedniego - korzysta się ze zwolnionego obiektu - nie jest to jednak próba odczytu (zapisu) w zwolnionym fragmencie pamięci, lecz próba aktywowania metody na rzecz czegoś, co faktycznie nie istnieje, jak w poniższym przykładzie:

TSomeClass *MyClass = new TSomeClass;

int Answer;

delete MyClass;

Answer = MyClass->Double(5);

CodeGuard informuje w tym przypadku o miejscu utworzenia i zwolnienia obiektu oraz miejscu niedozwolonego wywołania metody.

Reference To Freed Resource - odwołanie do zwolnionego zasobu

Błąd ten jest kolejną odmianą wykorzystywania zwolnionego obszaru pamięci, jednak nie w celu dokonywania w nim zapisów (odczytów), lecz w celu powtórnego jego zwolnienia. W poniższym przykładzie błąd ten występuje dwukrotnie:

int *MyIntList = (int *)malloc(100);

free(MyIntList);

free(MyIntList);

TSomeClass *MyClass = new TSomeClass[10];

delete[] MyClass;

delete[] MyClass;

Informacja generowana przez CodeGuarda zawiera w tym przypadku wskazanie miejsc utworzenia i pierwszego zwolnienia zasobu, jak również miejsca, w którym próbuje się zwolnić ów obszar po raz drugi.

Method Called On Illegally Casted Object - wywołanie metody na rzecz błędnie określonego obiektu

Egzemplarz obiektu, na rzecz którego wywołana została metoda jego klasy, dostępny jest w treści tej metody pod postacią wskaźnika this, przekazywanego jako niejawny, dodatkowy parametr. Jeżeli CodeGuard wykryje, iż obszar pamięci wskazywany przez ten wskaźnik został zwolniony, sygnalizowany jest błąd Method Called On Freed Object; błąd opisywany w tym miejscu sygnalizowany jest natomiast w przypadku, gdy wskaźnik this w momencie wywołania metody wskazuje poza przydzielony obszar pamięci. W poniższym przykładzie tworzona jest dwuelementowa tablica obiektów, po czym podejmuje się próbę wywołania metody na rzecz obiektu przechowywanego w (nieistniejącym) trzecim elemencie:

TSomeClass *MyClass = new TSomeClass[2];

int Answer;

Answer = MyClass[2].Double(5);

delete[] MyClass;

CodeGuard informuje w tym przypadku o obszarze pamięci, na który wskazuje wskaźnik this, wskazuje także miejsca definicji i wywołania przedmiotowej metody.

Resource Type Mismatch - niewłaściwy typ zasobu

Błąd ten sygnalizowany jest w przypadku, gdy zwolnienie zasobu następuje w sposób nieadekwatny do sposobu jego utworzenia - czego przykładem jest zwalnianie za pomocą operatora free obiektu utworzonego za pomocą operatora new. Poniższy przykład prezentuje cztery odmiany tego błędu:

// nie dopasowane operatory new i free

TSomeClass *MyClass1 = new TSomeClass;

free(MyClass1);

// zwalnianie skalarnego obiektu za pomocą operatora tablicowego

TSomeClass *MyClass2 = new TSomeClass;

delete[] MyClass2;

// zwalnianie tablicy obiektów za pomocą operatora skalarnego

TSomeClass *MyClass3 = new TSomeClass[2];

delete MyClass3;

// niedobrana para - funkcja malloc() i operator delete

int *IntList = (int *)malloc(2);

delete IntList;

CodeGuard wskazuje w tym przypadku miejsca utworzenia zasobu i jego niewłaściwego zwalniania.

Access Overrun - wykroczenie poza dozwolony zakres adresów

Ten dość powszechny błąd polega na wykroczeniu „w przód” poza dozwolony region pamięci - adres, do którego odwołuje się kod programu, jest większy niż największy adres tego regionu. Oto dwa typowe przykłady tego zjawiska:

TSomeClass *MyClass = new TSomeClass[2];

MyClass[2].PubVal = 10; // Nie istnieje element MyClass[2]

delete[] MyClass;

char *CharList = new char[10];

strcpy(CharList, "1234567890"); // W tablicy CharList brak jest miejsca na

// zerowy ogranicznik

delete[] CharList;

CodeGuard wskazuje w tym przypadku miejsce definicji zasobu i instrukcję dokonującą niedozwolonego dostępu.

Access underrun - wykroczenie przed dozwolony zakres adresów

Błąd ten różni się od błędu Access Overrun jedynie tym, iż adres, do którego odwołuje się kod programu, wykracza przed dozwolony zakres - jak w poniższym przykładzie, zawierającym próbę odwołania do elementu tablicy o indeksie -1:

int *IntList = new int[2];

IntList[-1] = 10;

delete[] IntList;

Podobnie ja przypadku błędu Access Overrun, CodeGuard wskazuje tu miejsce definicji zasobu i instrukcję dokonującą niedozwolonego dostępu.

Access In Uninitialized Stack - dostęp do niezainicjowanego fragmentu stosu

Błąd ten powstaje przy próbie dostępu do niezainicjowanego obszaru, leżącego na stosie. Jedną z odmian tego błędu jest odwoływanie się do zmiennej lokalnej, której czas życia zakończył się - w poniższym przykładzie funkcja LocFunc() przekazuje na zewnątrz adres swojej zmiennej lokalnej; w momencie, gdy funkcja MyFunc() próbuje ów adres wykorzystać, zmienna ta już nie istnieje, a pod wskazywanym adresem (na stosie) znajduje się prawdopodobnie coś zupełnie innego:

void LocFunc(int **LocPtr)

{

int LocalVar;

*LocPtr = &LocalVar;

}

void MyFunc()

{

int *LocPtr;

LocFunc(&LocPtr);

*LocPtr = 10;

}

CodeGuard ogranicza się w tym przypadku do wskazania obszaru stanowiącego cel błędnego odwołania.

Access an Invalid Stack - próba dostępu poza wierzchołkiem stosu

Błąd ten oznacza próbę dostępu do obszaru leżącego poza wierzchołkiem stosu (czyli pod adresem mniejszym niż zawartość rejestru ESP) i tym różni się od błędu Access Underrun, iż odwołanie to jest legalne z punktu widzenia granic przydzielonego obszaru, jakim jest w tym przypadku cały stos. Jedną z przyczyn wykraczania poza wierzchołek stosu może być wykraczanie przed początek zmiennej lokalnej funkcji, jak w poniższym przykładzie:

void MyFunc()

{

char Name[20];

strcpy(&Name[-1], "Someone");

}

Podobnie jak w przypadku błędu Access In Uninitialized Stack, CodeGuard wskazuje przedmiotowy fragment stosu.

Bad Parameter - błędny argument wywołania funkcji

W tym przypadku argument jest „błędny” w tym sensie, iż reprezentowany przez niego obiekt nie spełnia określonych warunków wymaganych przez funkcję RTL lub Win32 API. Poniższy fragment przedstawia jedną z takich sytuacji - próbę zamknięcia nie otwartego pliku:

void MyFunc()

{

FILE *Stream;

fclose(Stream);

}

CodeGuard wskazuje w tym przypadku miejsce wywołania z błędnym parametrem.

Function Failure - niedozwolony wynik funkcji

CodeGuard monitoruje wartości zwracane przez większość funkcji RTL i Win32; zwrócenie przez funkcję z tej grupy wartości -1 oznacza, iż podczas jej realizacji wystąpił błąd i jest sygnalizowane przez CodeGuard. Jedną z wielu możliwych przyczyn takiego stanu rzeczy może być próba przejścia do nieistniejącego katalogu, jak w poniższym przykładzie:

void MyFunc()

{

chdir ("Z:\ZXCVBN");

}

Informacja produkowana przez CodeGuarda zawiera wskazanie miejsca wywołania, w wyniku którego funkcja zwróciła wartość sygnalizującą błąd.

Resource Leak - „wyciek” zasobu

Racjonalna gospodarka zasobami systemu - np. pamięcią operacyjną - nakazuje ich zwalnianie, gdy okazują się już niepotrzebne. Lekceważenie - lub po prostu przeoczenie - tej prostej zasady prowadzi do systematycznego ubożenia środowiska operacyjnego w zasoby na skutek uruchamiania aplikacji, które (mówiąc po prostu) więcej biorą niż oddają. W poniższym fragmencie kodu tworzony jest egzemplarz klasy TSomeClass:

void MyFunc()

{

TSomeClass *MyClass = new TSomeClass;

}

Celowo jednak w naszej aplikacji nie ma instrukcji zwalniającej ów obiekt, wskutek czego powstaje namiastka opisanego „wyciekania” zasobów. CodeGuard wykrywa nie zwolnione zasoby po zakończeniu aplikacji, wskazując te wiersze kodu źródłowego, w których zostały one utworzone.

Zaawansowane techniki śledzenia

Jak już pisaliśmy wcześniej - i jak wiedzą zaawansowani programiści - śledzenie aplikacji samo w sobie jest czynnością na wskroś skomplikowaną, pod wieloma względami uwarunkowaną specyfiką konkretnej aplikacji. Tym niemniej można wymienić kilka istotnych czynników, których uwzględnienie może w różnym stopniu tę czynność ułatwić.

Przede wszystkim dla profesjonalnego tworzenia i śledzenia aplikacji systemy operacyjne Windows NT i Windows 2000 stanowią środowisko bardziej stabilne niż Windows 95/98, zwłaszcza dla aplikacji „najeżonych” błędami. Załamanie się takiej aplikacji w środowisku NT lub 2000 ma zwykle dla systemu operacyjnego skutki daleko łagodniejsze iż w 95/98, gdzie prawdopodobne jest załamanie się samego C++Buildera czy nawet całego systemu, szczególnie podczas wymuszonego zatrzymywania aplikacji (Run|Program Reset), którego tym samym należy w miarę możliwości unikać na rzecz „normalnego” zamykania. Pewien wyjątek od tej zasady stanowi sytuacja, kiedy to aplikacja wykona niedozwoloną operację (illegal operation) lub doświadczy ogólnego błędu ochrony (access violation), wyświetlając stosowny komunikat - w środowisku Windows 9x należy wówczas najpierw „zresetować” program, a dopiero później zamknąć okno komunikatu, co jest na ogół bezpieczniejsze od zamykania go a priori.

W pewnych szczególnych sytuacjach, na przykład przy tworzeniu aplikacji systemowych dla Windows, wskazane jest postarać się o wersje systemu Windows ukierunkowane na funkcje śledzenia - wersje te noszą nazwy „debug binary” dla Windows 9x i „checked/debug build” dla Windows NT/2000. Są one wzbogacone o mechanizmy sprawdzania błędów, weryfikacji argumentów i śledzenia systemowego na użytek Win32 API i systemu operacyjnego w ogóle, głównie w postaci asercji - mechanizmów tych brak jest w „zwyczajnych”, sprzedawanych detalicznie wersjach Windows. Wersje „wzbogacone” można otrzymać w ramach niektórych subskrypcji MSDN (Microsoft Developer Network).

Tomku, jeżeli będziesz czytał powyższy akapit, to zweryfikuj jego prawdziwość, bo ja naprawdę nie mam pojęcia, o co tu chodzi.

Dla śledzonej aplikacji może być ponadto istotna informacja, czy aplikacja ta wykonywana jest pod kontrolą jakiegoś debuggera; informacji takiej dostarcza funkcja IsDebuggerPresent(), zwracająca w takiej sytuacji wartość true. Aplikacja może wykorzystać tę informację do zróżnicowania pewnych elementów swego zachowania, na przykład wypisując w trybie śledzenia dodatkowe komunikaty, ułatwiające zlokalizowanie błędu.

Zajmiemy się teraz kilkoma wybranymi zagadnieniami śledzenia o wyższym stopniu zaawansowania.

Znajdowanie przyczyny ogólnego błędu ochrony

Ogólny błąd ochrony (AV - Access Violation) stanowi sytuację znacznie mniej klarowną od błędów dotychczas opisywanych. Może on być spowodowany niektórymi błędnymi sytuacjami wychwytywanymi zawczasu przez CodeGuarda, ten ostatni nie potrafi jednak wychwycić wszystkich takich sytuacji, poza tym nie każda aplikacja wykonywana jest pod jego kontrolą.

Zewnętrznym przejawem błędu AV jest komunikat o treści „Access violation at address YYYYYYYY. Read of address ZZZZZZZZ”. Komunikaty o „konkretnych” błędach aplikacji mają zazwyczaj inną formę, np. „The instruction at 0xYYYYYYYY referenced memory at 0xZZZZZZZZ”, gdzie YYYYYYYY jest adresem instrukcji, która próbowała uzyskać niedozwolony dostęp do danych pod adresem ZZZZZZZZ.

Jedną z metod przechwycenia błędu AV jest zaimplementowanie odpowiedniej funkcji obsługi wyjątku, o czym pisaliśmy już wcześniej w tym rozdziale. Można także użyć w tym celu debuggera systemowego (JIT - Just-In-Time debugger) i przejść do adresu YYYYYYYY, albo po prostu uruchomić aplikację w środowisku IDE . Jeżeli nie jest możliwe powtórne doprowadzenie do wystąpienia błędu AV, należy przerwać wykonywanie aplikacji za pomocą opcji Run|Program Pause i spowodować zatrzymanie na adresie YYYYYYYY za pomocą opcji Goto Address (z menu kontekstowego panelu deasemblacji w oknie CPU). Nie jest to metoda specjalnie skuteczna, jednak analiza kodu w pobliżu adresu zatrzymania może niekiedy podsunąć jakiś pomysł co do (przypuszczalnej) przyczyny błędu.

Gdy adres ZZZZZZZZ w treści komunikatu ma wartość bliską zeru (na przykład 00000089), prawdopodobną przyczyną błędu jest dostęp do pola nie utworzonego obiektu, dokładniej -wskazywanego przez niezainicjowany wskaźnik, jak w poniższym przykładzie:

TButton *MyButton;

MyButton->Height = 10;

Deklarowany wskaźnik MyButton inicjowany jest automatycznie wartością zerową; wartość 00000089 jest offsetem pola związanego z właściwością Height w ramach klasy TButton, czyli adresem tego pola w „egzemplarzu” klasy zlokalizowanym pod adresem 0 - kod wygenerowany przez kompilator sumuje bowiem wspomniany offset z wartością wskaźnika. Aby łatwiej można było rozpoznawać takie sytuacje, zalecane jest zerowanie wskaźników w momencie zwalniania wskazywanego przez nie obszaru.

Podłączanie się do uruchomionego procesu

Możliwości debuggera C++Buildera nie ograniczają się do projektu załadowanego aktualnie do IDE - możliwe jest „podłączenie się” do innego, uruchomionego na zewnątrz IDE procesu i śledzenie go w taki sam sposób, jak śledzi się „normalnie” uruchomione aplikacje. Jedyną niedogodnością tego procesu jest fakt, iż system Windows nie udostępnia żadnych środków umożliwiających „odłączenie się” od zewnętrznego procesu przed jego zakończeniem.

W celu przyłączenia się do zewnętrznego procesu należy wybrać opcję Run|Attach to Process z menu głównego IDE. Wyświetlone zostanie okno (patrz rys. 5.13), zawierające listę procesów uruchomionych na komputerze lokalnym (tj. tym samym, na którym uruchomiono C++Buildera); należy podświetlić w tej liście interesujący proces i kliknąć przycisk Attach. W wyniku tej operacji wskazany proces zostanie wstrzymany (paused) i wyświetlone zostanie okno CPU ze wskazaniem bieżącego punktu jego wykonania. Można odtąd ustanawiać punkty przerwań, kontrolować wartości danych, a nawet kojarzyć wyświetlany kod binarny z kodem źródłowym (jeżeli jest dostępny) za pomocą opcji View Source.

Rysunek 5.13. Okno dialogowe umożliwiające podłączenie debuggera do zewnętrznego procesu

Możliwość podłączenia się do zewnętrznych procesów nie kończy się na lokalnym komputerze - w oknie opcji Run|Attach to Process istnieje możliwość wpisania nazwy zdalnego komputera; powrócimy do tej kwestii już za chwilę.

Należy zachować ostrożność w przypadku podłączania się do starszych procesów, gdyż może to spowodować zawieszenie Windows wskutek podłączenia się do procesu systemowego. Bezpieczne jest za to podłączanie się jedynie do procesów stworzonych we własnym zakresie.

Debugger systemowy

Debugger systemowy (JIT - Just-In-Time debugger) obecny jest w systemach Windows NT i Windows 2000; nie jest dostępny w środowisku Windows 9x. Umożliwia on kontynuowanie wykonania procesu zgłaszającego błąd drogą śledzenia tegoż procesu.

Jeżeli posługiwałeś się wcześniej Windows NT i Windows 2000, z pewnością słyszałeś o programie Dr. Watson; jest on właśnie narzędziem typu JIT dostarczanym przez Windows w celu ułatwienia identyfikacji przyczyny błędnego zachowania się programu. Program pełniący aktualnie rolę debuggera typu JIT określony jest przez odpowiednie wpisy w rejestrze (HKEY_CURRENT_USER\Software\Borland\Debugging\5.0\JIT Debuggers) i oczywiście może być zmieniany. Nowością C++Buildera 5 jest program BORDBG51.EXE, który może być uruchamiany zamiast programu Dr. Watson; umożliwia on uruchomienie w charakterze JIT dowolnego dostępnego debuggera - C++Buildera, Delphi, a nawet Turbo Debuggera.

W poprzednich wersjach C++Buildera odwołanie do programu Dr. Watson zastępowane było po prostu odwołaniem do debuggera C++Buildera; nie był możliwy wybór spośród dostępnych debuggerów.

Więcej informacji na temat debuggerów JIT znaleźć można w systemie pomocy C++Buildera 5 pod hasłem „Just in time debuggers”.

Śledzenie zdalne

Debugger C++Buildera 5 umożliwia także śledzenie procesów uruchomionych na zdalnych komputerach (remote computers) z poziomu komputera lokalnego, co jest o tyle wygodne, iż nie wymaga instalowania na zdalnych komputerach C++Buildera. Zdalne śledzenie (remote debugging) jest szczególnie użyteczne w przypadku aplikacji rozproszonych typu DCOM lub CORBA i może być prowadzone z dowolnego komputera, dla którego sieć zapewnia odpowiednią przepustowość.

Przedmiotem zdalnego śledzenia mogą być pliki wykonywalne, biblioteki DLL lub pakiety. Śledzona aplikacja musi być skompilowana z ustawionymi opcjami informacji o śledzeniu, musi też posiadać plik symboli .tds. Najłatwiejszym sposobem osiągnięcia takiego stanu rzeczy jest załadowanie projektu (do C++Buildera) na komputerze lokalnym i skierowanie generowanych plików wynikowych do odpowiedniego folderu zdalnego komputera za pomocą pola Final output na karcie Directories/Conditionals opcji projektu.

Od momentu, gdy sesja zdalnego śledzenia zostanie nawiązana, śledzenie to nie będzie się w odczuwalny sposób różnić od śledzenia w trybie lokalnym.

Konfigurowanie zdalnego śledzenia

Pierwszym etapem konfigurowania zdalnego śledzenia jest zainstalowanie na zdalnym komputerze serwera śledzenia (debug server), którym jest program BORDBG51.EXE - ten sam, o którym pisaliśmy przy okazji debuggerów JIT; program ten wymaga dodatkowo kilku bibliotek DLL, umożliwiających komunikację z C++Builderem.

Na komputerach zdalnych wyposażonych w systemy Windows NT lub Windows 2000 serwer śledzenia instalowany jest zazwyczaj jako usługa identyfikowana w ramach apletu „Services” Panelu sterowania pod nazwą „Borland Remote Debugging Service”; usługa ta może być uruchamiana i zatrzymywana z poziomu apletu, może też być uruchamiana automatycznie przy starcie systemu. Do jej instalacji i odinstalowania służą opcje -install i -remove przy uruchomieniu programu BORDBG51.EXE z wiersza poleceń.

Na zdalnych komputerach wyposażonych w Windows 9x serwer śledzenia instalowany jest jako niezależny proces (możliwe jest to również na komputerach z Windows NT/2000); należy go wówczas uruchamiać z opcją -listen.

Instalację serwera śledzenia wraz z niezbędnymi bibliotekami DLL przeprowadza się z instalacyjnej płyty CD-ROM C++Buildera za pomocą standardowego dialogu instalacyjnego bądź przez „ręczne” uruchomienie programu SETUP.EXE z katalogu RDEBUG tej płyty CD-ROM. Serwer śledzenia komunikuje się z lokalną instancją C++Buildera za pomocą protokołu TCP/IP, toteż obydwa komputery - lokalny i zdalny - powinny mieć poprawnie zainstalowane i skonfigurowane składniki sieciowe związane z tym protokołem.

Prowadzenie zdalnego śledzenia

Aby rozpocząć zdalne śledzenie aplikacji, należy na lokalnym komputerze załadować do C++Buildera związany z nią projekt. Wybierając opcję Run|Parameters z menu głównego IDE, należy przejść na kartę Remote wyświetlonego okna i w polu Remote Path wpisać kompletną ścieżkę sieciową pliku wykonywalnego śledzonej aplikacji (jeżeli śledzona ma być biblioteka DLL, należy podać lokalizację modułu aplikacji-hosta), a w polu Remote Host wpisać nazwę lub adres IP komputera zdalnego. W polu Parameters podać można ewentualne parametry wywołania aplikacji.

Aby natychmiast rozpocząć zdalne śledzenie, należy zaznaczyć opcję Debug project on remote machine i kliknąć przycisk OK. Kiedy tylko w lokalnej instancji C++Buildera wykonane zostanie jakiekolwiek polecenie związane ze śledzeniem, nawiązane zostanie połączenie z serwerem śledzenia na zdalnym komputerze; dalszy przebieg śledzenia jest identyczny jak przy śledzeniu lokalnym.

W przypadku otrzymania komunikatu Unable to connect to remote host należy przede wszystkim sprawdzić, czy serwer śledzenia jest uruchomiony. Następnie należy sprawdzić poprawność wpisanej nazwy (adresu) komputera-hosta; w dalszej kolejności należy sprawdzić połączenie pomiędzy komputerami, np. za pomocą programu ping lub innego narzędzia sieciowego.

Przyczyną komunikatu Could not find program `xxxxxxx' jest błędne określenie ścieżki sieciowej w polu Remote Path.

Jak już wcześniej wspominaliśmy, zdalne śledzenie dotyczyć może również zewnętrznego procesu uruchomionego na zdalnym komputerze; proces ten przebiega podobnie jak w przypadku podłączania się do procesu lokalnego z tą różnicą, iż w pole Remote Machine okna Attach to Process należy wpisać nazwę lub adres IP komputera zdalnego i uprzednio uruchomić na tym komputerze serwer śledzenia. Należy pamiętać, iż nie jest możliwe odłączenie debuggera od zewnętrznego procesu przez zakończeniem tego ostatniego.

Śledzenie bibliotek DLL

Śledzenie biblioteki DLL tym różni się od śledzenia „zwykłej” aplikacji, iż biblioteka DLL nie da się samodzielnie uruchomić, lecz może być załadowana przez inną aplikację, zwaną tutaj aplikacją-hostem. W celu przetestowania poszczególnych funkcji biblioteki można taką aplikację stworzyć ad hoc, można jednak w tym celu równie dobrze wykorzystać „prawdziwą” aplikację, z którą biblioteka ta zwykle współpracuje.

Aby rozpocząć śledzenie biblioteki, należy załadować jej projekt do C++Buildera, skompilować i ustawić punkt przerwania w miejscu, w którym chcemy rozpocząć śledzenie. Następnie w polu Host Application na karcie Local okna Run Parameters należy wpisać ścieżkę aplikacji-hosta (lub zlokalizować tę aplikację za pomocą przycisku Browse); w polu Parameters można określić ewentualne parametry jej wywołania.

Uruchomienie aplikacji-hosta następuje w momencie kliknięcia przycisku Load (lub kliknięcia przycisku OK i późniejszego wybrania opcji Run|Run z menu IDE). Od momentu zatrzymania wykonania na ustanowionym wewnątrz biblioteki punkcie przetrwania dalsze śledzenie przebiega tak samo, jak w przypadku zwykłej aplikacji. Można w ten sposób kontrolować działanie obiektów COM i komponentów ActiveX, jeżeli jednak te ostatnie są samodzielnymi procesami, to śledzenie ich możliwe jest jedynie w Windows NT/2000 - Windows 9x nie dopuszczają śledzenia międzyprocesowego.

Inne narzędzia śledzenia

Jeżeli już mowa o śledzeniu i usuwaniu błędów z aplikacji, kilka dodatkowych narzędzi wspomagających unikanie błędów wartych jest przynajmniej krótkiego komentarza.

Jak już wcześniej pisaliśmy, wiele błędów popełnionych zostaje w momencie modyfikowania kodu źródłowego aplikacji. W procesie budowania aplikacji należy zatem korzystać z jakiegokolwiek systemu kontroli wersji lub zapewnić równoważną kontrolę we własnym zakresie - nawet jeżeli oznaczałoby to okresowe kopiowanie całych folderów. Konieczne jest także jakieś narzędzie do porównywania różnych wersji kodu - popularnym freeware'owym narzędziem tego typu jest program diff i jego mniej znana odmiana windiff obecna na niektórych dyskach instalacyjnych Windows. Wizualną odmianę tego programu (Visual Diff) ściągnąć można z witryny http://www.starbase.com.

Standardowym wyposażeniem C++Buildera 5 w wersji Enterprise jest interfejs czołowy TeamSource; może być on także odrębnie zakupiony przez posiadaczy wersji Professional. Szczegółowy opis TeamSource jest przedmiotem rozdziału 18.

Niedrogim, lecz użytecznym narzędziem jest program CodeSite firmy Raize Software (http://www.raize.com). Umożliwia on wysyłanie do centralnej przeglądarki różnorodnych informacji o danej aplikacji, opatrzonych znacznikami czasowymi, co dodatkowo stanowić może pewną informację na temat efektywności aplikacji. Podobnym narzędziem jest program Overseer Debugger - darmowy program osiągalny pod adresem http://delphree.clexpert.com/pages/projects/nexus/overseer_debugger.htm.

Innym darmowym programem jest GExperts, tworzący dziennik śledzenia przeznaczony do późniejszej analizy za pomocą programu Gdebug.exe. Program ten osiągalny jest pod adresem http://www.gexperts.org, znajduje się również na instalacyjnej płycie CD-ROM wersji Enterprise i Professional C++Buildera 5.

ClassExplorer Pro firmy ToolsFactory (http://www.toolsfactory.com) jest integrowalnym (add-on) narzędziem, wspomagającym tworzenie i śledzenie aplikacji w środowisku Delphi i C++Buildera. Rozszerza on znacznie możliwości nawigowania po plikach i klasach, udostępnia również pełnowartościowe środki do automatyzacji tworzenia klas, włączając w to szablony generowanego kodu i tworzenie dokumentacji. Dla zastosowań niekomercyjnych program jest ten bezpłatny.

W rozdziale 4. opisaliśmy pokrótce pakiet Sleuth QA Suite firmy TurboPower Software (http://turbopower.com). Pakiet ten zawiera między innymi dwa użyteczne programy - CodeWatch, podobny pod względem funkcjonalnym do CodeGuarda, oraz profilator StopWatch.

W wykrywaniu wycieków pamięci pomocny może okazać się darmowy program MemProof dostępny pod adresem http://www.totalqa.com. Integruje się on z C++Builderem, może również śledzić wykorzystanie zasobów przez BDE i InterBase, nadzorować wywoływanie funkcji API itp.

Istnieje wiele niezależnych narzędzi do śledzenia błędów wykonania; Autor książki poleca trzy następujące:

Doc-o-Matic (http://www.doc-o-matic.com) firmy ToolsFactory jest systemem dokumentowania aplikacji, produkującym pliki w formacie .hlp i HTML na podstawie komentarzy w kodzie źródłowym.

WinSight dostarczany jest wraz z wersjami Professional i Enterprise C++Buildera 5. Udostępnia on szczegółowe informacje dotyczące klas, okien i komunikatów w czasie wykonania aplikacji.

Program OleView.exe jest przeglądarką obiektów OLE/COM, dostępną w witrynie Microsoftu.

Testowanie aplikacji

Testowanie aplikacji nierozerwalnie splata się ze śledzeniem jej wykonywania. Zadaniem testowania jest wykrywanie błędów aplikacji, jak również dostarczanie dodatkowych informacji co do ich rodzaju i lokalizacji.

Testowanie stanowi integralną część projektowania - im aplikacja jest bardziej gruntownie przetestowana, tym jest bardziej wiarygodna. Intensywność testowania zależna jest od wymogów stawianych aplikacji, konsekwencji jej ewentualnego błędnego działania, a także dostępnego budżetu i terminów - wszelkie wynikające stąd ograniczenia powinny jednak dotyczyć raczej repertuaru funkcji spełnianych przez aplikację niż jej niezawodności.

Testowanie jest tematem niezmiernie rozległym -napisano już o nim mnóstwo książek, artykułów itp.; w tym podrozdziale ograniczymy się więc jedynie do najważniejszych jego aspektów.

Etapy i techniki testowania

Testowanie tworzonego projektu przeprowadzane jest na różnych etapach jego tworzenia. Niektóre z tych etapów ujęliśmy w poniższej liście, każdy z nich może ponadto dzielić się na pewną liczbę podetapów:

Wszelkie techniki testowania podzielić można na dwie kategorie; kryterium tego podziału jest odniesienie czynności testowania do kodu źródłowego aplikacji.

Testowanie strukturalne, zwane skrótowo testem białej skrzynki (ang. whitebox), odbywa się w ścisłym odniesieniu do kodu źródłowego, jego rozgałęzień i ścieżek przepływu sterowania. Dla małych aplikacji wystarczającym do tego celu narzędziem jest zintegrowany debugger, dla większych projektów wymagane są narzędzia bardziej specjalizowane, uwzględniające czynnik tzw. pokrycia kodu (ang. code coverage), określający kompletność przetestowania wszelkich ścieżek, jakimi może podążać sterowanie programu.

Testowanie behawioralne, zwane potocznie testem czarnej skrzynki (ang. blackbox), odbywa się w oderwaniu od kodu źródłowego; przedmiotem testów są wyniki produkowane na podstawie określonych danych lub na podstawie takich a nie innych zachowań użytkownika aplikacji. Skuteczność testowania behawioralnego zależna jest w dużym stopniu od czynnika tzw. pokrycia danych (ang. data coverage), określającego relację danych użytych do testów z rzeczywistymi danymi (ściślej - różnymi kategoriami tych danych), z jakimi przyjdzie aplikacji pracować w mniej lub bardziej ekstremalnych warunkach. Podstawowym mankamentem tego rodzaju testów jest trudność określenia, jaka część kodu została tak naprawdę przetestowana, tak więc rzadko kiedy test behawioralny okazuje się wystarczający.

Wskazówki dotyczące testowania

Zawsze testuj stworzony przez siebie kod! Tworzenie oprogramowania dla komputerów jest działalnością wybitnie podatną na błędy, nawet więc najlepszym programistom rzadko kiedy zdarza się tworzyć programy od początku bezbłędne. Zawsze lepiej mieć do czynienia z małą ilością oprogramowania przetestowanego niż z ogromem oprogramowania naszpikowanego błędami.

Nie odkładaj na później problematyki testowania tworzonego kodu; wszelkie testy powinny być przeprowadzane „na gorąco”, gdy mentalne odniesienie programisty do tworzonego przezeń kodu jest największe.

Przy tworzeniu nowej wersji istniejącego systemu zajmij się możliwie wcześnie problematyką konwersji danych obsługiwanych przez dotychczasową wersję. Pozwala to nie tylko na upewnienie się, iż żadne aspekty funkcjonowania dotychczasowej wersji nie pozostają niejasne, lecz także na dokładniejsze określenie terminu ukończenia projektu.

Upewnij się co do adekwatności wykonywanych testów; dotyczy to szczególnie testowania automatycznego. Jeżeli poprawność i wystarczalność przeprowadzanych testów stoi pod znakiem zapytania, całe testowanie staje się pracą syzyfową.

I na koniec uwaga natury ogólnej - testy, którym poddawana jest aplikacja, są co najwyżej tak dobre, jak kwalifikacje testerów w zakresie dziedziny zastosowań aplikacji. Aby bowiem wiedzieć, czy aplikacja zachowuje się bezbłędnie, należy przede wszystkim wiedzieć, jak powinna się ona zachowywać.

Grace Hopper (1906-1992), komandor amerykańskiej marynarki, matematyk, absolwentka Vassar College i Yale. Jedna z pierwszych programistek współczesnych komputerów (Mark I, II i III), w latach sześćdziesiątych XX w. kierowała rządowym (USA) zespołem normalizacyjnym języka Cobol (przyp. tłum. na podstawie Wielkiej Internetowej Encyklopedii Multimedialnej http://wiem.onet.pl).

Interesujące wywody na ten temat znaleźć można w książce G.J.Myersa „Projektowanie niezawodnego oprogramowania” wyd. pol. WNT 1980, ISBN 83-204-0224-7 (przyp. tłum.)

Rysunek 7.2 został jednak sporządzony przez autora przy rozdzielczości 800×600 pikseli (przyp. tłum.)

Pod pojęciem adresu liniowego rozumiemy adres wygenerowany przez moduł segmentacji procesora; podlega on następnie przekształceniu na adres fizyczny przez jednostkę stronicowania. W przypadku aplikacji korzystającej z „płaskiego” (flat) modelu pamięci adres liniowy tożsamy jest z adresem w jej pamięci wirtualnej (przyp. tłum.)

Tak naprawdę adres ten jest offsetem w segmencie określonym przez zawartość rejestru segmentowego DS (przyp. tłum.)

Szczegółowe informacje na ten temat znajdują się w książce „Anatomia PC”, wyd. V, HELION 1999, strony 214÷216 (przyp. tłum.)

Część I Podstawy obsługi systemu WhizBang (Nagłówek strony)

1

2 D:\helion\C++Builder 5\R05-03.DOC



Wyszukiwarka

Podobne podstrony:
R14-03, ## Documents ##, C++Builder 5
R17-03, ## Documents ##, C++Builder 5
R18-03, ## Documents ##, C++Builder 5
R04-03, ## Documents ##, C++Builder 5
R13-03, ## Documents ##, C++Builder 5
R08-03, ## Documents ##, C++Builder 5
R09-03, ## Documents ##, C++Builder 5
R07-03, ## Documents ##, C++Builder 5
R03-03, ## Documents ##, C++Builder 5
R15-03, ## Documents ##, C++Builder 5
R16-03, ## Documents ##, C++Builder 5
R02-03, ## Documents ##, C++Builder 5
R11-03, ## Documents ##, C++Builder 5
r05-01, ## Documents ##, XML Vademecum profesjonalisty
r-13-00, ## Documents ##, C++Builder 5
r-12-00, ## Documents ##, C++Builder 5
2011 03 03 Document 001
r05-03(1), Informacje dot. kompa
R05-05, ## Documents ##, WIN 2000 Professional -W praktyce

więcej podobnych podstron