Debugowanie

ŁF

1 Debugowanie programów w Delphi
     1.1 Wstęp
     1.2 Debuger
     1.3 Jak tego używać
          1.3.1 Podstawowe narzędzia
          1.3.2 Narzędzia trochę bardziej zaawansowane
          1.3.3 Narzędzia zaawansowane
          1.3.4 Pogląd zmiennych (Watches)
          1.3.5 Stos (Call Stack)
          1.3.6 Run Until Return
          1.3.7 Local Variables
          1.3.8 Thread Status
          1.3.9 Modules
          1.3.10 CPU/FPU/MMX Window
2 Debugowanie programów w Delphi part 2
     2.4 Czym są wycieki pamięci?
          2.4.11 Prosty przykład wycieku pamięci
          2.4.12 Prosty przykład Resource Leak
     2.5 Jak walczyć z wyciekami pamięci?
          2.5.13 ReportMemoryLeaksOnShutdown
     2.6 Programy firm trzecich
          2.6.14 Krótki opis FastMM

Debugowanie programów w Delphi

Wstęp

Artykuł został napisany na bazie Delphi 7 Enterprise, ale opisane narzędzia istnieją i działają tak samo (albo bardzo podobnie) także w innych wersjach tego środowiska.

Jeśli masz już za sobą stworzenie większego kawałka kodu, to na pewno wiesz też, że jego pomyślne skompilowanie to dopiero początek walki o jego poprawne działanie. Na forum jest pełno tematów typu "co jest źle w tym kodzie", "czemu to nie działa" itp. Kod się kompiluje, uruchamia, często nawet nie wyskakują błędy - ale nie działa prawidłowo. Coś jest nie tak, właściwie to wszystko jest nie tak, ale co to powoduje i jak to sprawdzić?

Debuger

Dawno, dawno temu, za siedmioma górami i siedmioma lasami budowano olbrzymie komputery, lampowe pierwowzory malutkich pecetów. Owe komputery składały się z olbrzymiej ilości lamp (tranzystory nie były wtedy jeszcze w powszechnym użyciu), a lampy niestety miały (i mają) dwie wady: spore zyżycie mocy, i krótki czas życia. Krótki czas działania, skrócony jeszcze przez istnienie wielu lamp, powodował że dawne komputery częściej nie działały niż wykonywały obliczenia. Ponadto ich rozmiar powodował spore problemy z namierzeniem usterki. Któregoś razu okazało się, że kolejny błąd w działaniu systemu spowodował robak, którego śmiertelnie popieścił prąd w trakcie wędrówki po płycie. Robak, konkretnie pluskwa, po angielsku bug, nadał nazwę całemu procesowi wyszukiwania i usuwania błędów w programie (poczucie humoru programistów...) - debugowaniu, po polsku "odpluskwianiu".

Początkujący programiści zanim nauczą się posługiwać debuggerem, czasem korzystają z tricku polegającego na umieszczanie w kodzie programu linijki (bądź linijek) wypisujących dokąd to kod się wykonał zanim się wykrzaczył (ShowMessage - brzmi znajomo?...). To jednak wymaga wielu kompilacji programu, i celowania na chybił/trafił w podejrzane miejsca, a co za tym idzie jest to czaso- i pracochłonne. Dlatego Delphi od zarania dziejów zawiera pakiet narzędzi ułatwiających, czy też może raczej w ogóle umożliwiających wyłapywanie błędów w kodzie - debugger.

Debugger (odpluskwiacz ;-)) Delphi umożliwia uruchomienie programu w trybie pracy krokowej, przejście do pracy ciągłej, zatrzymanie warunkowe bądź bezwarunkowe, sprawdzenie wartości zmiennych, rejestrów, pamięci, stosu, stanu wątków, a także umożliwia podgląd kodu programu z poziomu instrukcji procesora (czyli asemblera).

Jak tego używać

Najważniejszym elementem debuggera są tzw. breakpoints (punkty zatrzymania - tu lepiej chyba też jest używać angielskiego słowa). Breakpoint to specjalnie oznaczona linijka kodu, która spowoduje zatrzymanie PRZED NIĄ wykonywania programu - program przejdzie w tryb pracy krokowej, co pozwala na sprawdzenie rzeczy wymienionych akapit wyżej.

No właśnie, a co to jest praca krokowa? Otóż jest to wykonywanie programu pod kontrolą debuggera - uruchamianie np.: jednej linijki kodu, jednej instrukcji procesora, bloku kodu... W trakcie zatrzymania kodu (ale nie zamknięcia programu, zatrzymanie a zamknięcie to dwie różne rzeczy) masz do dyspozycji pewne fantastyczne narzędzie o nazwie Evaluate/Modify. Pozwala ono na podgląd wartości zmiennych i stałych, można nim także zmieniać wartości zmiennych, oraz sprawdzać wyniki wykonania funkcji (!). Inne narzędzia pozwalające testować zatrzymany program są w menu View->Debug Windows. Od razu mówię, że do skutecznego debugowania programu konieczne jest przekompilowanie, a jeszcze lepiej zbudowanie (Project->Build) całego projektu od nowa z odpowiednio ustawionymi opcjami kompilatora:

  • trzeba wyłączyć optymalizację kodu, bo inaczej nie będziesz mógł sprawdzić wartości większości zmiennych (Project->Options->Compiler->Code generation->Optimization);
  • warto na wszelki wypadek włączyć wyłapywanie błędów przekroczenia wartości (Project->Options->Compiler->Runtime errors->Overflow checking) i zakresu (Project->Options->Compiler->Runtime errors->Range checking);
  • jeśli jakimś cudem nie są włączone, to trzeba je zaznaczyć - dodawanie do kodu informacji dla debuggera (Project->Options->Compiler->Debugging->wszystko oprócz Use Debug DCUs).

Całość powinna wyglądać mniej więcej tak (oczywiście pole "Debug information" musi być zaznaczone):

rysunek01.jpg

Pamiętaj, żeby na koniec skompilować cały program.

Podstawowe narzędzia

W menu Run masz kilka interesujących pozycji:

Step Over, Trace Into, Trace to Next Source Line, Run to Cursor, Evaluate/Modify, Add watch (w Delphi 7.0 i wyższych są jeszcze dodatkowe opcje, jednak raczej Ci się nie przydadzą, dlatego na razie nie omówię ich).
Co robi Step over? Po pierwsze - skrót klawiszowy - F8 - zapamiętaj go. Po drugie - załaduj do Delphi jakiś program (chociażby File->New->Application) i wciśnij F8. Program został skompilowany (o ile wcześniej nie był), uruchomiony, a potem zatrzymany (ale nie zakończony). W edytorze jedna linijka kodu została podświetlona - w tym miejscu program został spauzowany. Ponieważ plikiem, w którym zaczyna się wykonywanie kodu, jest plik dpr, masz przed swoim oczami najprawdopodobniej zawartość pliku Project1.dpr, i linijkę ze słowem begin

rysunek02.jpg

Jeśli znowu wciśniesz F8, wykona się kod w tej linijce (tutaj Application.Initialize;), i zostanie podświetlona następna linijka. W ten sposób możesz wykonać cały program - to jest właśnie praca krokowa. W międzyczasie możesz obserwować, która część kodu jest wykonywana, a jeśli kod nie wykonuje się tak, jak według Ciebie powinien, to znaczy że namierzyłeś błąd (co nie znaczy, że łatwo znajdziesz jego powód i go usuniesz).

Jeśli chcesz skończyć debugowanie, po prostu wciśnij F9 (skrót do Run) - program wykona się do końca tak, jakbyś go wcale nie debugował.

Step Over wykonuje od razu całą zawartość linijki, nieważne, czy kryje się tam jedna instrukcja, kilka, przypisanie wartości do zmiennej, czy wywołanie Twojej funkcji albo procedury. Krótko mówiąc Step Over wykonuje operacje hurtowo - wszystko co stoi na drodze do następnej linijki. Jeśli program nigdy nie dotrze do kolejnej linijki (np.: zawiesi się wskutek źle skonstruowanej pętli), efekt będzie taki, jakbyś po prostu uruchomił program. Co zrobić, jeśli chcesz wykonać linijkę po linijcę wywoływaną funkcję/procedurę? Ano, po pierwsze musisz mieć do niej kod źródłowy, po drugie kod modułu, w którym znajduje się ta funkcja, musi być skompilowany wedle instrukcji podanych w części "Jak tego używać". Jeśli tak jest, zerknij na pozycję Run->Trace Into - skrót klawiszowy F7 (też go zapamiętaj).

Trace Into pozwala na wykonanie kodu rzeczywiście linijka po linijce - z "wchodzeniem" do wnętrza wszystkich metod/funkcji/procedur. Jest to bardzo przydatne, aczkolwiek na dłuższą metę dość męczące (bo przeważnie nie interesuje nas wykonanie linijka po linijce wszystkich 153 funkcji siedzących w debugowanym kawałku programu, tylko kilka konkretnych, które są podejrzane o powodowanie błędów. Dlatego opcji Trace Into używa się przeważnie w kombinacji ze Step Over.

Różnicę pomiędzy Trace Into a Step Over doskonale zauważysz po wykonaniu w trybie krokowym poniższego kodu (raz przejedź przez kod programu wciskając F8, a drugi raz - F7):

program Project1;

function a : integer;
begin
  result := 1;
end;

var
  i : integer;
begin
  i := a;
end.

Weź pod uwagę, że jest to zawartość pliku .dpr, a nie .pas.

Jeżeli już masz dość wykonywania programu w trybie krokowym, ale nie chcesz, żeby się wykonał do końca, możesz go zresetować - służy do tego opcja Program reset (Run->Program reset) - skrót Ctrl+F2. Może ona zatrzymać program nie tylko w trybie pracy krokowej, ale także program uruchomiony poprzez Run, co bardzo przydaje się, gdy program się zawiesi (czyli wpadnie w nieskończoną pętlę).

W trakcie szukania błędów przydaje się (o ile nie jest najważniejszy) podgląd wartości zmiennych - bo przecież to właśnie nieprawidłowe wartości tych zmiennych (albo nieprawidłowe przyjęcie założeń co do ich wartości) najczęściej powodują błędy. Do podglądu zawartości zmiennych tylko w trakcie pracy krokowej programu służy narzędzie Evaluate/Modify - skrót Ctrl+F7 (koniecznie zapamiętaj).

Uruchom podany wyżej kod w trybie pracy krokowej, najedź kursorem klawiatury na zmienną "i" i wciśnij Ctrl+F7. Powinieneś zobaczyć coś takiego:

rysunek03.jpg

Jeśli pole evaluate jest puste, wpisz tam nazwę zmiennej (w tym przypadku "i") i wciśnij enter. Jak widzisz, bezpośrednio po uruchomieniu programu zmienne globalne są zerowane - w polu result znajduje się liczba 0, tj. wartość zmiennej wpisanej w polu Expression. Zasad wpisywania zmiennych są takie, jak przy pisaniu kodu - jeśli "i" jest zmienną globalną, to będzie widoczna w każdym miejscu modułu, w którym została zdefiniowana (oraz w modułach używających tego z definicją); jeśli w funkcji/metodzie istnieje zmienna o nazwie takiej samej, jak zmienna globalna, to ta druga zostanie "przykryta" tą pierwszą - dlatego musisz się orientować co wpisujesz w pole Expression.

Jeśli zamkniesz okno Evaluate/Modify i wykonasz linijkę i := a; i znowu otworzysz to okno, to przekonasz się, że wartość zmiennej i wynosi 1.

Wartość zmiennej i możesz modyfikować - w polu New value możesz wpisać dowolną wartość (dowolną, ale prawidłową dla zmiennej - do np.: liczby nie przypiszesz stringa), zmiana zostanie zatwierdzona enterem.
Narzędzie Evaluate/Modify posiada możliwość sprawdzania wartości nie tylko zmiennych (i stałych), ale także funkcji. Oczywiście funkcji nie można przypisać nowej wartości ;-) Pamiętaj, że jeśli funkcja wykonuje się długo, to i długo będziesz czekać na pokazanie wyniku, a jeśli funkcja się zapętli, to okno Evaluate/Modify
się zawiesi (jednak Ctrl-F2 przywróci Delphi do stanu używalności, resetując wykonywanie programu).

Narzędzia trochę bardziej zaawansowane

Dotarcie do interesującej nas linijki kodu za pomocą narzędzi Step Over/Trace Into może trwać wieki, zwłaszcza, jeśli interesuje Cię linijka o numerze 58624739 ;] Poza tym kod programów napisanych w Delphi z użyciem VCL w zasadzie uniemożliwia dotarcie przez F7/F8 do kodu metody TForm1.Create. Więc co wtedy?
Otóż masz do dyspozycji narzędzia, które powodują wykonanie kodu aż do momentu dotarcia do interesującej Cię linijki. Konkretnie Run to Cursor (skrót F4) i breakpointy.
Run to Cursor działa następująco: ustawiasz się kursorem na interesującej Cię linijce, wciskasz F4, a program uruchomi się i będzie działać aż natrafi na zaznaczoną wcześniej linijkę - tam się zatrzyma, i będziesz mógł użyć wszystkich omówionych wyżej narzędzi - pojechać dalej po kodzie przez Step Over/Trace Into, sprawdzić/zmienić interesujące Cię zmienne przez Evaluate/Modify, albo znowu użyć Run to Cursor (zakładając, że to ma sens, czyli np.: jesteś wewnątrz pętli, albo jesteś w kodzie obsługującym jakieś zdarzenie, choćby kliknięcie na TButton).
W ramach ćwiczeń wstaw na formatkę przycisk, w jego zdarzeniu OnClick wklep jakiś kod (np.: exit) i wciśnij F4. Program będzie działać normalnie aż do momentu, gdy wciśniesz ten przycisk; po jego wciśnięciu zatrzyma się, i zostanie podświetlona linijka wcześniej wybrana przez F4. Jeśli ponownie wciśniesz F4, program znowu będzie działać normalnie aż do następnego naciśnięcia przycisku.

Jeśli jednak musisz "pilnować" kodu w kilku odległych miejscach naraz, to ciągłe skakanie kursorem kilkadziesiąt linijek w górę lub w dół tylko po to, żeby wcisnąć F4, robi się męczące. Jeszcze gorzej jest, gdy debugujesz aplikację wielowątkową i nie masz pewności, który z fragmentów strategicznego kodu wykona się najpierw. Przydało by się mieć możliwość uruchomienia programu w trybie Run to cursor, ale żeby kursor stał w wielu miejscach naraz. Otóż da się coś takiego zrobić, nazywa się to breakpoint.

Breakpoint stawia się albo poprzez Run->Add Breakpoint->Source breakpoint... albo przez kliknięcie myszą maksymalnie po lewej stronie kodu (zaznaczona w ten sposób linijka powinna się podświetlić):

rysunek04.jpg

W ten sposób możesz zaznaczyć dowolną ilość linijek; po uruchomieniu program zatrzyma się, gdy dotrze do któregokolwiek z postawionych breakpointów - możesz go uruchomić dalej (F9) - zatrzyma się wtedy na kolejnym breakpoincie, albo skorzystać z dowolnego ze wcześniej omówionych narzędzi.

Jeśli okaże się, że błąd siedzi gdzieś w pętli, i wychodzi dopiero po jej 238107 iteracji, to zajedziesz się na śmierć stawiając w pętli breakpoint, a potem 238108 razy wciskając F9. I na to jest rada - na powyższym obrazku widzisz pole condition; możesz tam wpisać warunek, który musi zostać spełniony, żeby breakpoint zadziałał (np.: i = 238107).

Breakpoint możesz skasować klikając na czerwonym znaczku z lewej strony kodu.
Lista breakpointów dostępna jest w menu View->Debug Windows->Breakpoints.

Narzędzia zaawansowane

To jeszcze nie koniec narzędzi do debugowania oferowanych przez Delphi...

Pogląd zmiennych (Watches)

Watches (Add watch... - Ctrl+F5) pozwala na śledzenie wielu zmiennych naraz. Używanie Evaluate/modify jest uciążliwe, gdy często musisz sprawdzać jak zmieniają się wartości zmiennych. W oknie Add watch możesz zdefiniować, które zmienne chcesz śledzić, zostaną one wyświetlone w okienku takim jak to dolne na poniższym obrazku:

debuger_imgs_5.gif

Nazwę zmiennej (albo także funkcji - o ile zaznaczony jest checkbox Allow Function Calls) wpisujesz do pola Expression. Jeśli śledzisz dane z tablicy, możesz w polu Repeat count podać ilość pól, która ma być wyświetlona; jeśli śledzisz zwykłe dane, ustawienie Repeat count spowoduje kilkukrotne wyświetlenie zawartości zmiennej (ciekaw jestem po co to komu ;]). Na sam koniec Enter, zmienna zostaje dodana do listy (Watch list) , i już. Oczywiście zawartość zmiennych można podglądać tylko w trakcie pracy krokowej (zasady takie same jak dla Evaluate/modify).
Dostęp do Watches możliwy jest też poprzez View->Debug Windows->Watches.

Stos (Call Stack)

Call Stack (View->Debug Windows->Call Stack) pozwala na sprawdzenie, co zostało odłożone na stosie przy wywoływaniu funkcji (metody, procedury...) - a konkretnie pozwala sprawdzić, która funkcja wywołała funkcję, która wywołała funkcję (...), która wywołała naszą funkcję ;-) - czyli lista nazw funkcji, z których niższa wywołała wyższą.

debuger_imgs_6.gif

Run Until Return

Run Until Return - Shift+F8 - wykonuje program aż do momentu, gdy skończy się bieżąca funkcja (procedura, metoda...). Przydatne zwłaszcza w połączeniu z breakpointami.

Local Variables

Narzędzie Local Variables (View->Debug Windows->Local Variables) wyświetla listę zawierającą nazwy wszystkich dostępnych zmiennych lokalnych.
debuger_imgs_7.gif

Thread Status

Narzędzie Threads (View->Debug Windows->Threads) wyświetla listę procesów i wątków utworzonych przez Twój program. Po kliknięciu na procesie prawym przyciskiem myszy i wybraniu Process properties otwiera się okno Temporary Process Options For Process ..., w którym można ustawić opcje współpracy procesu z systemem i z IDE. Dodatkowo każdy z procesów programu można ręcznie zakończyć korzystając z polecenia Terminate Process, również dostępnego pod prawym przyciskiem myszy.

debuger_imgs_8.gif

Modules

Czasem przydaje się sprawdzić z jakich bibliotek pas/dcu i dll korzysta program - umożliwia to narzędzie Modules (View->Debug Windows->Modules). Dzięki niemu możesz też zobaczyć, jakie funkcje zawierają biblioteki, a nawet - za pośrednictwem narzędzia CPU - zobaczyć ich kod (oczywiście mowa o kodzie assemblerowym). Jednocześnie możesz zobaczyć, jak wygląda drzewko modułów, i otwierać kod źródłowy tych modułów (o ile go mają). Wszystkie opcje tego narzędzia dostępne są jedynie podczas pracy krokowej.

debuger_imgs_9.gif

CPU/FPU/MMX Window

Na koniec najbardziej zaawansowane narzędzie, czyli okno z kodem assemblerowym Twojego programu - CPU Window (View->Debug Windows->CPU). Zawiera okna:

Disassembly z kodem programu (pokazane adresy komórek pamięci i ich zawartość, zarówno jako liczba, jak i jako instrukcja procesora); CPU Registers z rejestrami - możesz podejrzeć i zmodyfikować zawartości wszystkich podstawowych rejestrów procesora (EAX..EDX, ESI..EFL, CS..GS); na czerwono zaznaczone są te rejestry, które w trakcie ostatniego kroku programu zmieniły wartość; Flags z flagami procesora - każdą flagę możesz podejrzeć i przełączyć Memory Dump z zawartością pamięci Twojego programu (możesz zmieniać jej zawartość); Machine Stack - wyświetla zawartość stosu; na zielono zaznaczony jest wierzchołek stosu.

Dla programów wielowątkowych możesz przełączyć kontekst (menu kontekstowe->Change Thread), aby zobaczyć stan procesora dla innych wątków. Oczywiście cały czas działają wszystkie omówione wyżej narzędzia - możesz sobie nawet otworzyć wszystkie okna z menu View->Debug Windows (o ile wystarczy Ci miejsca na ekranie :]) i wędrować po programie krok po kroku, i to z ustawionymi breakpointami. Okno CPU Window pozwala na wykonywanie kodu nie linijka po linijce, ale instrukcja po instrukcji - zarówno w trybie Trace Over, Step Into, jak i Run To Cursor; możesz nawet stawiać breakpointy na konkretne instrukcje procesora. Dla prostoty użytkowania skróty klawiszowe do wędrowania po instrukcjach są takie same, jak te do wędrowania po linijkach kodu źródłowego, a wielkość kroku, który się wykona, zależy od tego które okno jest aktywne (to z kodem źródłowym, czy CPU Window).

debuger_imgs_10.gif

Z menu podręcznego możesz wybrać narzędzie uzupełniające CPU Window, a mianowicie FPU Window. Dostępne jest ono także z menu View->Debug Windows->FPU. Pozwala ono na podgląd i edycję zawartości rejestrów jednostek FPU (menu podręczne->Show->Floating Point Registers) i MMX (menu podręczne->Show->MMX Registers) procesora.

Polecam dość dokładne przebadanie zawartości menu kontekstowego zarówno CPU Window, jak i FPU Window - jest tam wiele opcji ułatwiających życie programiście. Opis wszystkich - F1 :-P

Debugowanie programów w Delphi part 2

Powyżej poznaliśmy podstawowe narzędzia zawarte w IDE Delphi7.
Czasami jednak zdarzają się sytuacje, gdy nasza aplikacja powoduje błędy w systemie dopiero po dłuższym czasie, lub przestaje działać w nieoczekiwanym miejscu, powodując u nas przyrost siwych włosów na głowie.
Powodem tego mogą być wycieki pamięci (memory leaks).

Czym są wycieki pamięci?

Programowanie obiektowe w Delphi pozwala na pisanie dużych aplikacji, jednakże im więcej zaawansowanych klas i komponentów, tym większe szanse na powstanie błędów.

Wycieki pamięci występują, gdy program traci możliwość zwolnienia pamięci, którą zajął. Powtarzane wycieki powodują, że proces zajmuje coraz większy obszar i jeśli program działa dość długo przestanie odpowiadać.

Kiedykolwiek tworzysz nowy obiekt w Delphi, musisz potem zwolnić pamięć, którą zajmował! Pierwszym krokiem w zapobieganiu "memory leaks", jest zrozumienie jak powstają.
W większości prostych programów, gdzie używasz komponenty (Button, Memo, Edit, itd.); poprzez zrzucanie ich na formę, nie musisz zbytnio przejmować się zarządzaniem pamięci. Gdy komponent zostanie umieszczony na formularzu, forma staje się jego opiekunem (owner) i zwolni pamięć tego komponentu, gdy będzie zamknięta (closed, destroyed). Inaczej można powiedzieć, że komponenty na formie są tworzone i usuwane automatycznie.

Prosty przykład wycieku pamięci

Często w aplikacji zachodzi potrzeba tworzenia własnych klas. Powiedzmy, że masz klasę TDeveloper, która ma metodę DoProgram().
Teraz, gdy chcesz użyć klasę TDeveloper, tworzysz jej instancję poprzez wywołanie metody Create (constructor). Metoda Create, alokuje pamięć dla nowego obiektu i zwraca dla niego referencję.

var
   zarko : TDeveloper
begin
   zarko := TMyObject.Create();
   zarko.DoProgram();
end;

I oto mamy prosty wyciek pamięci! Obiekt został utworzony, użyty, następnie zakończyła się metoda, wewnątrz której obiekt był zmienną lokalną. Po zakończeniu działania metody tracimy zmienną lokalną i nie mamy jak się dostać do utworzonego obiektu, ale on nadal siedzi w pamięci; w językach z tzw. Garbage Collectorem - GC - w takiej sytuacji obiekt zostanie automatycznie usunięty, tzn. GC automatycznie zwolni pamięć po każdym obiekcie, do którego nie ma referencji wewnątrz programu. GC znajduje się głównie w językach zarządzanych - np.: Java, C#, C++/CLI, ale ma go także każda przeglądarka www do "sprzątania" po javascripcie.
Jednak programy pisane w Delphi (poza środowiskiem .NET) nie posiadają GC, dlatego kiedykolwiek utworzysz obiekt, musisz pamiętać o zwolnieniu pamięci, którą zajął, poprzez wywołanie metody Free (oczywiście robisz to dopiero w momencie, kiedy obiekt nie będzie już potrzebny). Aby być pewien jej zadziałania, powinieneś też użyć bloku try / finally.

var
   zarko : TDeveloper;
begin
   zarko := TMyObject.Create();
   try
    zarko.DoProgram();
   finally
     zarko.Free();
   end;
end;

To jest przykład bezpiecznego alokowania i zwalniania pamięci.
Uwaga: Jeśli chcesz dynamicznie tworzyć i zwalniać komponenty, przekazuj im Nil jako owner.

Prosty przykład Resource Leak

Oprócz tworzenia i usuwania obiektów (metody Create i Free), musisz być także ostrożny, gdy korzystasz z zewnętrznych zasobów (pliki, kontrolki (zarówno winapi, jak i używające go vcl), bazy danych, gniazda, większość obiektów IPC, itd, ogólnie wszystko, co posiada tzw. uchwyt - handle).
Powiedzmy, że potrzebujesz operacji na jakimś pliku tekstowym. W prostym przykładzie metoda AssignFile jest użyta do przypisania pliku na dysku, gdy skończysz musisz użyć CloseFile by zwolnić uchwyt (handle) do pliku.

var
   F: TextFile;
   S: string;
begin
   AssignFile(F, 'c:\somefile.txt') ;
   try
     Readln(F, S) ;
   finally
     CloseFile(F) ;
   end;
end;

Inny przykład zawiera użycie zewnętrznych bibliotek. Kiedykolwiek użyjesz LoadLibrary, musisz wywołać FreeLibrary:

var
   dllHandle : THandle;
begin
   dllHandle := Loadlibrary('MyLibrary.DLL') ;
   // Trochę niepotrzebnego kodu...
   if dllHandle <> 0 then FreeLibrary(dllHandle) ;
end;

Jak walczyć z wyciekami pamięci?

Oprócz pisania z rozwagą kodu, zapobiegać wyciekom można używając aplikacji firm trzecich lub narzędzi zawartych w samym Delphi.
Wszystkie wersje Delphi od 2006 wzwyż posiadają ulepszony menadżer pamięci, który jest szybszy i posiada więcej możliwości. Jednym z miłych dodatków jest to, iż menadżer pamięci pozwala aplikacji zarejestrować i wyrejestrować oczekiwane wycieki, oraz opcjonalnie raportować nieoczekiwane wycieki po zamknięciu programu.

ReportMemoryLeaksOnShutdown

Raportowanie wycieków pamięci jest domyślnie wyłączone. By go włączyć, musisz ustawić globalną wartość ReportMemoryLeaksOnShutdown na TRUE. Gdy aplikacje zostanie zamknięta, przy wystąpieniu wycieku zostanie wyświetlone okno dialogowe: "Unexpected Memory Leak".
Najlepszym miejscem dla ReportMemoryLeaksOnShutdown, jest plik projektu (dpr).

    begin
      ReportMemoryLeaksOnShutdown := DebugHook <> 0;

      Application.Initialize();
      Application.MainFormOnTaskbar := True;
      Application.CreateForm(TMainForm, MainForm);
      Application.Run();
    end.

Uwaga: w tym przykładzie zmienna DebugHook jest użyta, by pokazywać memory leaks jedynie, gdy aplikacja jest uruchamiana w trybie debug mode - gdy naciśniesz F9 w Delphi.

Wpisz ten kawałek kodu do aplikacji, by przetestować ReportMemoryLeaksOnShutdown

    
var
  sl : TStringList;
begin
  sl := TStringList.Create();
  sl.Add('Memory leak!') ;
end;

Uruchom aplikację i zamknij, a zobaczysz dialog box informujący o wycieku pamięci.

unexpectedmemoryleak.jpg

Programy firm trzecich

Bardziej zaawansowane narzędzia i to dla różnych wersji Delphi oferują inne firmy.

  • Płatny i posiadający wiele możliwości jest pakiet EurekaLog (http://www.eurekalog.com/), integrujący się z IDE Delphi. Pakiet ten dodaje do aplikacji własny menadżer pamięci i oprócz wycieków potrafi wyłapać większość wyjątków.
  • Memproof (http://www.onlythebestfreeware.com/program.asp?program_id=138) - osobna aplikacja, do której ładujemy nasz skompilowany program exe. Po zakończeniu działania pokaże ewentualne wycieki. Uwaga program czasami pokazuje MemLeaks, na które nie mamy wpływu, są to błędy zawarte w plikach systemowych Delphi.
  • FastMM (http://sourceforge.net/projects/fastmm/) - moim zdaniem najlepszy i najskuteczniejszy pakiet, który wykryje większość błędów w naszej aplikacji.

Krótki opis FastMM

Jego dodanie do programu polega na dopisaniu jednej linijki do dpr:

program JakisProgram;

uses
  FastMM4,
  Forms,

No i definiujemy sobie w projekcie, czy chcemy używać dodatkowo biblioteki FullDebugMode DLL
Przykładowe Conditional defines:

DEBUG; FullDebugMode; LogErrorsToFile; LogMemoryLeakDetailToFile;

Gdy nie potrzebujemy raportowania, wystarczy wpisać Conditional defines:

RELEASE;

Raport o wyciekach będzie w pliku tekstowym, w miejscu uruchamiania aplikacji.
JakisProgram_MemoryManager_EventLog.txt

Zobaczmy przykładowy log:

--------------------------------2009/3/4 19:32:23--------------------------------
A memory block has been leaked. The size is: 68

Stack trace of when this block was allocated (return addresses):
 402A9B [system.pas][System][@GetMem][2447]
 6098CE [Forms\Main.pas][Main][TForm1.SpeedRecClick][4219]
(...)

Jak widać program pobrał pamięć, ale jej nie zwolnił, bezpośrednio pod funkcją getmem jest:
6098CE [Forms\Main.pas][Main][TForm1.SpeedRecClick][4219]

Otwieramy ten plik w delphi (Main.pas) i w menu "Search/Go to line number" - skrót Alt + G
idziemy do linii numer 4219 gdzie przykładowo widać:

SpeedRec.Glyph:=TBitmap.Create();

Aha i już wiem, że został utworzony obiekt TBitmap, który nie został potem zwolniony. Przy okazji dodam, iż ten błąd występuje w wielu przykładach na tym forum.
Prawidłowy kod w tym przypadku będzie taki:

var
  obrazek1 :TBitmap;
begin
  Obrazek1:= TBitmap.Create();
try
  SpeedRec.Glyph.Assign(Obrazek1);
finally
  Obrazek1.Free;
end;

Oprócz raportowania do pliku, można włączyć inne opcje - opis w pliku FastMM4Options.inc

Sukcesów w debugowaniu.

9 komentarzy

NAPRAWDE SUPER artykuł!! przydałoby się WIĘCEJ takich rzeczy o różnych narzedziach dostępnych w Delphi i ich możliwościach :) ZACHĘCAM do pracy :) wszystko przeczytam.. :)

Bardzo dobry artykuł. Wspomnial bym jeszcze o takim narzedziu jak Debug Inspector. Pozwala na podglad zlozonych struktur danych(klasy).Mnie sie kiedys bardzo przydalo.(W Delphi 6 po zaznaczeniu podglądanego obiektu w trakcie debugowania Alt+F5 lub wybranie z menu kontekstowego Inspect).Ocena 6.

Mimo wszystko ShowMessage() i Beep() to dobry sposób w prostszych programach lub, gdy niewiele jest do sprawdzenia - nie ma bałaganu z ustawianiem punktów przerwań etc.

"Któregoś razu okazało się, że kolejny błąd w działaniu systemu spowodował robak, którego smiertelnie popieścił prąd w trakcie wędrówki po płycie."

Pomyślałem sobie - początki microsoftu? :D

he he ShowMessage - ciągle na tym jade teraz trzeba przeczytaćtego 'giganta' i zmienić taktykę :)

ode mnie masz 6

tabelka przesunięta w lewo... hmmm.... :>

Tabelka na górze jest przesunięta w lewo [rotfl]

Nie no... zaraz się Kapustka wkurzy i przyśle mi National Geographic [green]

Art spoko :]

nawet nie wiedziałem, że jest tyle narzędzi do debugowania :)

Jestem na TAK :). Dobre, ale już o tym wiem. Mimo wszystko dobrze, że ktoś o tym napisal, bo naprawdę są ludzie typu "ShowMessage". Ja też taki bylem, ale w wersji "Dopisz linię do pliku tekstowego". Też ciekawe :).