Znajdowanie przecieków pamięci za pomocą biblioteki CRT

Przecieki pamięci należą do najbardziej subtelnych i trudnych do wykrycia błędów w aplikacjach C/C++. Przecieki pamięci wynikają z niepowodzenia poprawnego cofnięcia przydziału pamięci, która została wcześniej przydzielona. Na początku może nie zostać zauważony mały przeciek pamięci, ale z upływem czasu może powodować objawy, od niskiej wydajności do awarii, gdy aplikacja zabraknie pamięci. Wyciek aplikacji, która używa całej dostępnej pamięci, może spowodować awarię innych aplikacji, co powoduje zamieszanie co do tego, która aplikacja jest odpowiedzialna. Nawet nieszkodliwe przecieki pamięci mogą wskazywać na inne problemy, które należy rozwiązać.

Debuger programu Visual Studio i biblioteka czasu wykonywania języka C (CRT) mogą pomóc w wykrywaniu i identyfikowaniu przecieków pamięci.

Włączanie wykrywania przecieków pamięci

Podstawowe narzędzia do wykrywania przecieków pamięci to debuger C/C++ i funkcje sterty debugowania CRT.

Aby włączyć wszystkie funkcje stert debugowania, dołącz następujące instrukcje w programie C++, w następującej kolejności:

#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

Instrukcja #define mapuje podstawową wersję funkcji sterty CRT na odpowiednią wersję debugowania. Jeśli pominiesz instrukcję #define , zrzut wycieku pamięci będzie mniej szczegółowy.

Dołączenie pliku crtdbg.h mapuje malloc funkcje i free na ich wersje debugowania oraz _malloc_dbg _free_dbg, które śledzą alokację pamięci i cofanie alokacji. To mapowanie odbywa się tylko w kompilacjach debugowania, które mają wartość _DEBUG. Kompilacje wydania używają zwykłych malloc funkcji i free .

Po włączeniu funkcji stert debugowania przy użyciu powyższych instrukcji umieść wywołanie _CrtDumpMemoryLeaks przed punktem wyjścia aplikacji, aby wyświetlić raport przecieku pamięci po zakończeniu działania aplikacji.

_CrtDumpMemoryLeaks();

Jeśli aplikacja ma kilka zakończeń, nie musisz ręcznie umieszczać _CrtDumpMemoryLeaks w każdym punkcie wyjścia. Aby wywołać _CrtDumpMemoryLeaks wywołanie automatyczne do każdego punktu wyjścia, umieść wywołanie na _CrtSetDbgFlag początku aplikacji przy użyciu pól bitowych pokazanych tutaj:

_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );

Domyślnie _CrtDumpMemoryLeaks generuje raport dotyczący przecieku pamięci do okienka Debugowanie w oknie Dane wyjściowe. Jeśli używasz biblioteki, biblioteka może zresetować dane wyjściowe do innej lokalizacji.

Możesz użyć _CrtSetReportMode polecenia , aby przekierować raport do innej lokalizacji lub wrócić do okna Dane wyjściowe , jak pokazano poniżej:

_CrtSetReportMode( _CRT_WARN, _CRTDBG_MODE_DEBUG );

W poniższym przykładzie przedstawiono prosty wyciek pamięci i wyświetla informacje o wycieku pamięci przy użyciu polecenia _CrtDumpMemoryLeaks();.

// debug_malloc.cpp
// compile by using: cl /EHsc /W4 /D_DEBUG /MDd debug_malloc.cpp
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#include <iostream>

int main()
{
    std::cout << "Hello World!\n";

    int* x = (int*)malloc(sizeof(int));

    *x = 7;

    printf("%d\n", *x);

    x = (int*)calloc(3, sizeof(int));
    x[0] = 7;
    x[1] = 77;
    x[2] = 777;

    printf("%d %d %d\n", x[0], x[1], x[2]);

    _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG); 
    _CrtDumpMemoryLeaks();
}

Interpretowanie raportu przecieku pamięci

Jeśli aplikacja nie definiuje _CRTDBG_MAP_ALLOCelementu , _CrtDumpMemoryLeaks wyświetla raport dotyczący przecieku pamięci, który wygląda następująco:

Detected memory leaks!
Dumping objects ->
{18} normal block at 0x00780E80, 64 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.

Jeśli aplikacja definiuje _CRTDBG_MAP_ALLOCelement , raport przecieku pamięci wygląda następująco:

Detected memory leaks!
Dumping objects ->
c:\users\username\documents\projects\leaktest\leaktest.cpp(20) : {18}
normal block at 0x00780E80, 64 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.

Drugi raport przedstawia nazwę pliku i numer wiersza, w którym po raz pierwszy przydzielono wyciek pamięci.

Niezależnie od tego, czy zdefiniujesz _CRTDBG_MAP_ALLOCpolecenie , zostanie wyświetlony raport dotyczący przecieku pamięci:

  • Numer alokacji pamięci, który znajduje się 18 w przykładzie
  • Typ normal bloku w przykładzie.
  • W przykładzie lokalizacja 0x00780E80 pamięci szesnastkowej.
  • Rozmiar bloku 64 bytes w przykładzie.
  • Pierwsze 16 bajtów danych w bloku w postaci szesnastkowej.

Typy bloków pamięci są normalne, klienckie lub CRT. Normalny blok to zwykła pamięć przydzielona przez program. Blok klienta to specjalny typ bloku pamięci używanego przez programy MFC dla obiektów, które wymagają destruktora. Operator MFC new tworzy normalny blok lub blok klienta, odpowiednio do tworzonego obiektu.

Blok CRT jest przydzielany przez bibliotekę CRT do użytku własnego. Biblioteka CRT obsługuje cofnięcie przydziału dla tych bloków, więc bloki CRT nie będą wyświetlane w raporcie przecieku pamięci, chyba że występują poważne problemy z biblioteką CRT.

Istnieją dwa inne typy bloków pamięci, które nigdy nie pojawiają się w raportach przecieków pamięci. Wolny blok to pamięć, która została zwolniona, więc z definicji nie wycieka. Blok ignoruj to pamięć, którą jawnie oznaczono do wykluczenia z raportu przecieku pamięci.

Powyższe techniki identyfikują przecieki pamięci dla pamięci przydzielonej przy użyciu standardowej funkcji CRT malloc . Jeśli jednak program przydziela pamięć przy użyciu operatora języka C++ new , może być widoczny tylko nazwa pliku i numer wiersza, w którym operator new wywołania są wywoływane _malloc_dbg w raporcie dotyczącym przecieku pamięci. Aby utworzyć bardziej przydatny raport dotyczący przecieku pamięci, możesz napisać makro podobne do poniższego, aby zgłosić wiersz, który dokonał alokacji:

#ifdef _DEBUG
    #define DBG_NEW new ( _NORMAL_BLOCK , __FILE__ , __LINE__ )
    // Replace _NORMAL_BLOCK with _CLIENT_BLOCK if you want the
    // allocations to be of _CLIENT_BLOCK type
#else
    #define DBG_NEW new
#endif

Teraz możesz zastąpić new operator za pomocą DBG_NEW makra w kodzie. W kompilacjach debugowania używa przeciążenia globalnegooperator new, DBG_NEW które przyjmuje dodatkowe parametry dla typu bloku, pliku i numeru wiersza. Przeciążenie wywołań _malloc_dbg do rejestrowania new dodatkowych informacji. Raporty dotyczące wycieku pamięci pokazują nazwę pliku i numer wiersza, w którym przydzielono wyciekły obiekty. Kompilacje wersji nadal używają domyślnego newelementu . Oto przykład techniki:

// debug_new.cpp
// compile by using: cl /EHsc /W4 /D_DEBUG /MDd debug_new.cpp
#define _CRTDBG_MAP_ALLOC
#include <cstdlib>
#include <crtdbg.h>

#ifdef _DEBUG
    #define DBG_NEW new ( _NORMAL_BLOCK , __FILE__ , __LINE__ )
    // Replace _NORMAL_BLOCK with _CLIENT_BLOCK if you want the
    // allocations to be of _CLIENT_BLOCK type
#else
    #define DBG_NEW new
#endif

struct Pod {
    int x;
};

void main() {
    Pod* pPod = DBG_NEW Pod;
    pPod = DBG_NEW Pod; // Oops, leaked the original pPod!
    delete pPod;

    _CrtDumpMemoryLeaks();
}

Po uruchomieniu tego kodu w debugerze programu Visual Studio wywołanie w celu _CrtDumpMemoryLeaks wygenerowania raportu w oknie Dane wyjściowe wygląda następująco:

Detected memory leaks!
Dumping objects ->
c:\users\username\documents\projects\debug_new\debug_new.cpp(20) : {75}
 normal block at 0x0098B8C8, 4 bytes long.
 Data: <    > CD CD CD CD
Object dump complete.

Te dane wyjściowe zgłaszają, że wyciekła alokacja w wierszu 20 debug_new.cpp.

Uwaga

Nie zalecamy tworzenia makra preprocesora o nazwie newlub innego słowa kluczowego języka.

Ustawianie punktów przerwania dla numeru alokacji pamięci

Numer alokacji pamięci informuje o przydzieleniu wycieku bloku pamięci. Blok z liczbą alokacji pamięci 18, na przykład, to 18 blok pamięci przydzielony podczas uruchamiania aplikacji. Raport CRT zlicza wszystkie alokacje bloków pamięci podczas przebiegu, w tym alokacje biblioteki CRT i innych bibliotek, takich jak MFC. W związku z tym blok alokacji pamięci numer 18 prawdopodobnie nie jest 18 blokiem pamięci przydzielonym przez kod.

Możesz użyć numeru alokacji, aby ustawić punkt przerwania na alokacji pamięci.

Aby ustawić punkt przerwania alokacji pamięci przy użyciu okna Czujka

  1. Ustaw punkt przerwania w pobliżu początku aplikacji i rozpocznij debugowanie.

  2. Gdy aplikacja wstrzymuje się w punkcie przerwania, otwórz okno Obserwowanie, wybierając pozycję Debuguj>usługę Windows>Watch 1 (lub Watch 2, Watch 3 lub Watch 4).

  3. W oknie Obserwowanie wpisz _crtBreakAlloc kolumnę Nazwa.

    Jeśli używasz wielowątkowej wersji biblioteki DLL biblioteki CRT (opcja /MD), dodaj operator kontekstu: {,,ucrtbased.dll}_crtBreakAlloc

    Upewnij się, że symbole debugowania zostały załadowane. _crtBreakAlloc W przeciwnym razie jest zgłaszany jako niezidentyfikowany.

  4. Naciśnij klawisz Enter.

    Debuger ocenia wywołanie i umieszcza wynik w kolumnie Wartość . Ta wartość to -1 , jeśli nie ustawiono żadnych punktów przerwania dla alokacji pamięci.

  5. W kolumnie Wartość zastąp wartość liczbą alokacji alokacji pamięci, w której ma zostać przerwany debuger.

Po ustawieniu punktu przerwania na numerze alokacji pamięci przejdź do debugowania. Upewnij się, że działa w tych samych warunkach, więc numer alokacji pamięci nie zmienia się. Gdy program przerywa określoną alokację pamięci, użyj okna stosu wywołań i innych okien debugera, aby określić warunki, w których przydzielono pamięć. Następnie można kontynuować wykonywanie, aby obserwować, co się dzieje z obiektem i określić, dlaczego nie jest poprawnie cofnięty przydział.

Ustawienie punktu przerwania danych na obiekcie może być również przydatne. Aby uzyskać więcej informacji, zobacz Używanie punktów przerwania.

Punkty przerwania alokacji pamięci można również ustawić w kodzie. Można ustawić:

_crtBreakAlloc = 18;

or:

_CrtSetBreakAlloc(18);

Porównanie stanów pamięci

Inną techniką lokalizowania przecieków pamięci jest tworzenie migawek stanu pamięci aplikacji w kluczowych punktach. Aby utworzyć migawkę stanu pamięci w danym punkcie aplikacji, utwórz _CrtMemState strukturę i przekaż ją do _CrtMemCheckpoint funkcji.

_CrtMemState s1;
_CrtMemCheckpoint( &s1 );

Funkcja _CrtMemCheckpoint wypełnia strukturę migawką bieżącego stanu pamięci.

Aby wyświetlić zawartość _CrtMemState struktury, przekaż strukturę do _ CrtMemDumpStatistics funkcji:

_CrtMemDumpStatistics( &s1 );

_CrtMemDumpStatistics generuje zrzut stanu pamięci, który wygląda następująco:

0 bytes in 0 Free Blocks.
0 bytes in 0 Normal Blocks.
3071 bytes in 16 CRT Blocks.
0 bytes in 0 Ignore Blocks.
0 bytes in 0 Client Blocks.
Largest number used: 3071 bytes.
Total allocations: 3764 bytes.

Aby określić, czy wyciek pamięci wystąpił w sekcji kodu, możesz wykonać migawki stanu pamięci przed sekcją i po nim, a następnie użyć polecenia _CrtMemDifference , aby porównać dwa stany:

_CrtMemCheckpoint( &s1 );
// memory allocations take place here
_CrtMemCheckpoint( &s2 );

if ( _CrtMemDifference( &s3, &s1, &s2) )
   _CrtMemDumpStatistics( &s3 );

_CrtMemDifference porównuje stany s1 pamięci i s2 zwraca wynik (s3), który jest różnicą między s1 i s2.

Jedna technika znajdowania przecieków pamięci rozpoczyna się od umieszczania _CrtMemCheckpoint wywołań na początku i na końcu aplikacji, a następnie przy użyciu funkcji _CrtMemDifference porównywania wyników. Jeśli _CrtMemDifference zostanie wyświetlony przeciek pamięci, możesz dodać więcej _CrtMemCheckpoint wywołań, aby podzielić program przy użyciu wyszukiwania binarnego, dopóki nie odizolujesz źródła wycieku.

Wyniki fałszywie dodatnie

_CrtDumpMemoryLeaks może dać fałszywe wskazania przecieków pamięci, jeśli biblioteka oznacza wewnętrzne alokacje jako normalne bloki zamiast bloków CRT lub bloków klienta. W takim przypadku _CrtDumpMemoryLeaks nie można odróżnić alokacji użytkowników i alokacji bibliotek wewnętrznych. Jeśli globalne destruktory alokacji biblioteki są uruchamiane po punkcie, w którym jest wywoływana _CrtDumpMemoryLeaks, każda wewnętrzna alokacja biblioteki jest zgłaszana jako przeciek pamięci. Wersje standardowej biblioteki szablonów starsze niż program Visual Studio .NET mogą powodować _CrtDumpMemoryLeaks zgłaszanie takich wyników fałszywie dodatnich.

Zobacz też