czwartek, 15 października 2009

VC++ i wykrywanie wycieków pamięci

Ostatnio bawiąc się Visual C++ 2008 Express dowiedziałem się o możliwości wykrywania przez VC++ wycieków pamięci w natywnym kodzie. Nie ma konieczności instalowania dodatkowych bibliotek, wystarczy tylko <crtdbg.h>. Czasami można wyczytać, że wersja Express jest pozbawiona tej możliwości - cóż, u mnie w 2008 działa ;) Ale oczywiście nie ma lekko :P Więc zobaczmy co można zrobić, aby pamięć nie przeciekała nam przez palce ;)

Najlepszym sposobem na wykrywanie wycieków (wg. mnie) jest zrobienie sobie pliku nagłówkowego memdbg.h o następującej zawartości

#ifndef MEMDBG_H
#define MEMDBG_H

#ifdef _DEBUG
//memory leak detection
#include <stdlib.h>
#include <crtdbg.h>
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif /* _DEBUG */

#endif /* MEMDBG_H */


Dyrektywę #include "memdbg.h" trzeba umieszczać po nagłówkach standardowych (<iostream>, <cstdio> itd.), inaczej będzie dużo błędów z powodu przedefiniowanego operatora new.

Dodatkowo we właściwościach projektu VC++ należy dodać definicję preprocesora (Properties -> Configuration properties -> C/C++ -> Preprocessor -> Preprocessor definitions -> (...))

_CRTDBG_MAP_ALLOC

Napisanie zwykłego #define _CRTDBG_MAP_ALLOC w memdbg.h przynajmniej u mnie nie działało - tzn. funkcja malloc nie była mapowana na swoją wersję debug ;)


Ok, a teraz trzeba to wykorzystać :) Do alokacji pamięci dalej normalnie wykorzystujemy new i malloc. Natomiast do prezentacji wyników wykrywania wycieków pamięci służy parę funkcji, z czego najpopularniejsze to

1. _CrtDumpMemoryLeaks(); standardowo wypisuje na panel "Debug" okna Output informacje o odnalezionych wyciekach pamięci. Funckja zwraca również wartość int (bool w stylu C) czy coś odnalazła. Miejsce wypisywania raportu można oczywiście zmienić za pomocą funkcji _CrtSetReportMode ;)

2. _CrtSetDbgFlag (_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); - umieszczenie tego na początku programu sprawi, że zawsze przed jego zakończeniem będzie wywoływane _CrtDumpMemoryLeaks();

Przykładowy raport wygląda tak:


Detected memory leaks!
Dumping objects ->
c:\c++\leak\leak.cpp(21) : {126} normal block at 0x00036A58, 400 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.


Jest tu mnóstwo przydatnych informacji
1. Plik i numer linii, w której jest dokonywana podejrzana alokacja - wystarczy kliknąć dwa razy w oknie Output, aby VC++ przerzucił nas tam automatycznie ;)
2. Numer w nawiasach klamrowych to numer alokacji (nadawane one są kolejno), która spowodowała wyciek
3. Typ niezwolnionego bloku pamięci i jego adres w pamięci.
4. Rozmiar wycieku.
5. Pierwsze 16 bajtów danych w bloku.

Rozwijając punkt 3, można wyróżnić następujące rodzaje bloków:
1. normal - pamięć alokowana przez nas
2. client - wykorzystywane przez programy MFC
3. CRT - bloki alokowane przez bibliotekę CRT (C Runtime)
4. free - zwolniony blok pamięci
5. ignore - blok, który użytkownik oznaczył jako ignorowany

W raportach najczęściej będzie się widzieć pierwsze 2 rodzaje (normal i client). Bloki CRT pojawiające się w raporcie o wyciekach pamięci oznaczają błędy w samej bibliotece i nie powinny się zdarzać ;) Ostatnie 2 rodzaje na pewno się w raporcie nie pojawią.


Numer alokacji (ten z nawiasów klamrowych) można dodatkowo wykorzystać przy poszukiwaniu wycieków - ustawiając breakpointa na konkretny numer alokacji. Można to zrobić na parę sposobów:

1. instrukcją _CrtSetBreakAlloc(126); (wkompilujemy to na stałe)
2. wykorzystując debugger - trzeba w oknie Watch na samym początku debuggowania ustawić wartość {,,msvcr90d.dll}_crtBreakAlloc na pożądany numer alokacji (np. 126).

Program teraz sam przerwie wykonanie przy odpowiedniej alokacji.

Oczywiście, kiedy porządek alokacji nie jest deterministyczny, to mamy problem ;)


Jeszcze innym sposobem walki z wyciekami z użyciem <crtdbg.h> jest porównywanie stanów pamięci odczytanych w kluczowych miejscach programu. Stan pamięci to liczba i rozmiar zaalokowanych bloków z podziałem na ich typy. Jest on reprezentowany przez strukturę _CrtMemState, a zapisywany jest z pomocą funkcji
void _CrtMemCheckpoint(_CrtMemState* state);.

Do porównania dwóch stanów służy funkcja
int _CrtMemDifference(_CrtMemState* stateDiff, const _CrtMemState* oldState, const _CrtMemState* newState);
Zwraca ona TRUE (int jako bool, witamy w C), jeżeli stany są różne.

Do wypisania stanu pamięci na ekran można użyć funkcji
void _CrtMemDumpStatistics(const _CrtMemState* state);

Znając funkcje do obsługi stanów pamięci można w taki sposób "osaczyć" podejrzane alokacje:

_CrtMemState s1, s2, diff;

_CrtMemCheckpoint(&s1);

float* f = new float[100];

_CrtMemCheckpoint(&s2);

if (_CrtMemDifference(&diff, &s1, &s2))
{
    _CrtMemDumpStatistics(&diff);
}


Wynik wyświetlony przez _CrtMemDumpStatistics wygląda mniej więcej tak:

0 bytes in 0 Free Blocks.
400 bytes in 1 Normal Blocks.
0 bytes in 0 CRT Blocks.
0 bytes in 0 Ignore Blocks.
0 bytes in 0 Client Blocks.
Largest number used: 0 bytes.
Total allocations: 400 bytes.


Jak widać wymienione są wszystkie wspomniane wcześniej typy bloków pamięci wraz z rozmiarami i liczbą. I można kombinować gdzie mamy przeciek ;)


Szkoda, że pisząc silniczek na MIBD nie znałem tego ficzeru, bo w jednym przypadku miałem naprawdę brzydki wyciek pamięci ;)

Aha, jeżeli masz własne operatory new i delete, to niestety <crtdbg.h> nie da rady, ponieważ działa przez podmienianie tych operatorów - ale jest to dość logiczne ;)

Więcej na MSDNie: http://msdn.microsoft.com/en-us/library/x98tx3cf.aspx

Brak komentarzy:

Prześlij komentarz