Ошибка memory leak detected

Ловим утечки памяти в С/С++

Время на прочтение
8 мин

Количество просмотров 49K

Приветствую вас, Хабровчане!

Сегодня я хочу немного приоткрыть свет над тем, как бороться с утечкой памяти в Си или С++.

На Хабре уже существует две статьи, а именно: Боремся с утечками памяти (C++ CRT) и Утечки памяти в С++: Visual Leak Detector. Однако я считаю, что они недостаточно раскрыты, или данные способы могут не дать нужного вам результата, поэтому я хотел бы по возможности разобрать всем доступные способы, дабы облегчить вам жизнь.

Windows — разработка
Начнем с Windows, а именно разработка под Visual Studio, так как большинство начинающих программистов пишут именно под этой IDE.

Для понимания, что происходит, прикладываю реальный пример:

Main.c

struct Student create_student();
void ControlMenu();

int main()
{
    ControlMenu();
    return 0;
}

void ShowListMenu(int kX)
{
    char listMenu[COUNT_LIST_MENU][55] = { {"Read students from file"}, {"Input student and push"},
    {"Input student and push it back"}, {"Input student and push it after student"},
    {"Delete last student"}, {"Write students to file"}, {"Find student"}, {"Sort students"},
    {"Show list of students"}, {"Exit"} };
    for (int i = 0; i < COUNT_LIST_MENU; i++)
    {
        if (i == kX)
        {
            printf("%s", listMenu[i]);
            printf(" <=n");
        }
        else
            printf("%sn", listMenu[i]);
    }
}

void ControlMenu()
{
    struct ListOfStudents* list = NULL;
    int kX = 0, key;
    int exit = FALSE;
    ShowListMenu(kX);
    do
    {
        key = _getch();
        switch (key)
        {
        case 72: //up
        {
            if (kX == 0)
                kX = COUNT_LIST_MENU-1;
            else
                kX--;
        }break;
        case 80: //down
        {
            if (kX == COUNT_LIST_MENU-1)
                kX = 0;
            else
                kX++;
        }break;
        case 13:
        {
            if (kX == 0)
            {
                int sizeStudents = 0;
                struct Student* students = (struct Student*)malloc(1 * sizeof(struct Student));
                char* path = (char*)malloc(255 * sizeof(char));
                printf("Put the path to file with students: ");
                scanf("%s", path);
                int size = 0;
                students = read_students(path, &size);
                if (students == NULL)
                {
                    printf("Can't open this file.n");
                }
                else
                {
                    for (int i = 0; i < size; i++)
                    {
                        if (i == 0)
                        {
                            list = init(students[i]);
                        }
                        else
                        {
                            list = add_new_elem_to_start(list, students[i]);
                        }
                    }
                }
                                free(students);
                printf("nPress any key to continue...");
                getchar();
                getchar();
                free(path);
            }
            else if (kX == 1 || kX == 2 || kX == 3 || kX == 6)
            {
                struct Student student = create_student();
                if (kX == 1)
                {
                    if (list == NULL)
                    {
                        list = init(student);
                    }
                    else
                    {
                        list = add_new_elem_to_start(list, student);
                    }
                    printf("nPress any key to continue...");
                    getchar();
                    getchar();
                }
                else if (kX == 2)
                {
                    if (list == NULL)
                    {
                        list = init(student);
                    }
                    else
                    {
                        list = add_new_elem_to_end(list, student);
                    }
                    printf("nPress any key to continue...");
                    getchar();
                    getchar();
                }
                else if (kX == 3)
                {
                    if (list == NULL)
                    {
                        list = init(student);
                        printf("The list was empty, so, list have been created.n");
                    }
                    else
                    {
                        int position;
                        printf("Put the position: ");
                        scanf("%d", &position);
                        list = add_new_elem_after_pos(list, student, position);
                    }
                    printf("nPress any key to continue...");
                    getchar();
                    getchar();
                }
                else
                {
                    if (find_elem(list, student))
                        printf("Student exist");
                    else
                        printf("Student doesn't exist");
                    printf("nPress any key to continue...");
                    getchar();
                    getchar();
                }
            }
            else if (kX == 4)
            {
                if (list == NULL)
                {
                    printf("List is empty.n");
                }
                else
                {
                    list = delete_elem(list);
                }
                printf("nPress any key to continue...");
                getchar();
                getchar();
            }
            else if (kX == 5)
            {
                char* path = (char*)malloc(255 * sizeof(char));
                printf("Put the path to file with students: ");
                scanf("%s", path);
                if (write_students(list, path) == 0)
                {
                    printf("Can't write");
                    printf("nPress any key to continue...");
                    getchar();
                    getchar();
                }
                free(path);
            }
            else if (kX == 7)
            {
                if (list == NULL)
                {
                    printf("List is empty.n");
                }
                else
                {
                    list = sort_list(list);
                }
                printf("nThe list was successfully sorted");
                printf("nPress any key to continue...");
                getchar();
                getchar();
            }
            else if (kX == 8)
            {
                system("cls");
                show_list(list);
                printf("nPress any key to continue...");
                getchar();
                getchar();
            }
            else
                exit = TRUE;
        }break;
        case 27:
        {
            exit = TRUE;
        }break;
        }
        system("cls");
        ShowListMenu(kX);
    } while (exit == FALSE);
    while (list != NULL)
    {
        list = delete_elem(list);
    }
}

struct Student create_student()
{
    struct Student new_student;
    do
    {
        printf("Write the name of studentn");
        scanf("%s", new_student.first_name);
    } while (strlen(new_student.first_name) == 0);
    do
    {
        printf("Write the last name of studentn");
        scanf("%s", new_student.last_name);
    } while (strlen(new_student.last_name) == 0);
    do
    {
        printf("Write the patronyminc of studentn");
        scanf("%s", new_student.patronyminc);
    } while (strlen(new_student.patronyminc) == 0);
    do
    {
        printf("Write the city of studentn");
        scanf("%s", new_student.city);
    } while (strlen(new_student.city) == 0);
    do
    {
        printf("Write the district of studentn");
        scanf("%s", new_student.disctrict);
    } while (strlen(new_student.disctrict) == 0);
    do
    {
        printf("Write the country of studentn");
        scanf("%s", new_student.country);
    } while (strlen(new_student.country) == 0);
    do
    {
        printf("Write the phone number of studentn");
        scanf("%s", new_student.phoneNumber);
    } while (strlen(new_student.phoneNumber) != 13);
    char* choose = (char*)malloc(255 * sizeof(char));
    while (TRUE)
    {
        printf("Does student live in hostel? Y - yes, N - non");
        scanf("%s", choose);
        if (strcmp(choose, "y") == 0 || strcmp(choose, "Y") == 0)
        {
            new_student.is_live_in_hostel = TRUE;
            break;
        }
        if (strcmp(choose, "n") == 0 || strcmp(choose, "n") == 0)
        {
            new_student.is_live_in_hostel = FALSE;
            break;
        }
    }
    while (TRUE)
    {
        printf("Does student get scholarship? Y - yes, N - non");
        scanf("%s", choose);
        if (strcmp(choose, "y") == 0 || strcmp(choose, "Y") == 0)
        {
            new_student.is_live_in_hostel = TRUE;
            break;
        }
        if (strcmp(choose, "n") == 0 || strcmp(choose, "n") == 0)
        {
            new_student.is_live_in_hostel = FALSE;
            break;
        }
    }
    free(choose);
    for (int i = 0; i < 3; i++)
    {
        char temp[10];
        printf("Write the %d mark of ZNOn", i + 1);
        scanf("%s", temp);
        new_student.mark_zno[i] = atof(temp);
        if (new_student.mark_zno[i] == 0)
        {
            i--;
        }
    }

    return new_student;
}

А также есть Student.h и Student.c в котором объявлены структуры и функции.

Есть задача: продемонстрировать отсутствие утечек памяти. Первое, что приходит в голову — это CRT. Тут все достаточно просто.

В начало файла, где находится main, необходимо добавить этот кусок кода:

#define __CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#define DEBUG_NEW new(_NORMAL_BLOCK, __FILE__, __LINE__)
#define new DEBUG_NEW

А перед return 0 нужно прописать это: _CrtDumpMemoryLeaks();.

В итоге, в режиме Debug, студия будет выводить это:

Detected memory leaks!

Dumping objects ->
{79} normal block at 0x00A04410, 376 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
Object dump complete.

Супер! Теперь вы знаете, что у вас утечка памяти. Теперь нужно устранить это, поэтому необходимо просто узнать, где мы забываем очистить память. И вот тут возникает проблема: а где, собственно, выделялась эта память?

После того, как я повторил все шаги, я выяснил, что память теряется где-то здесь:

if (kX == 0)
            {
                int sizeStudents = 0;
                struct Student* students = (struct Student*)malloc(1 * sizeof(struct Student));
                char* path = (char*)malloc(255 * sizeof(char));
                printf("Put the path to file with students: ");
                scanf("%s", path);
                int size = 0;
                students = read_students(path, &size);
                if (students == NULL)
                {
                    printf("Can't open this file.n");
                }
                else
                {
                    for (int i = 0; i < size; i++)
                    {
                        if (i == 0)
                        {
                            list = init(students[i]);
                        }
                        else
                        {
                            list = add_new_elem_to_start(list, students[i]);
                        }
                    }
                }
                free(students);
                printf("nPress any key to continue...");
                getchar();
                getchar();
                free(path);
            }

Но как так — то? Я же все освобождаю? Или нет?

И тут мне сильно не хватало Valgrind, с его трассировкой вызовов…

В итоге, после 15 минут прогугливания, я нашел аналог Valgrind — Visual Leak Detector. Это сторонняя библиотека, обертка над CRT, которая обещала показывать трассировку! Это то, что мне необходимо.

Чтобы её установить, необходимо перейти в репозиторий и в assets найти vld-2.5.1-setup.exe

Правда, последнее обновление было со времен Visual Studio 2015, но оно работает и с Visual Studio 2019. Установка стандартная, просто следуйте инструкциям.

Чтобы подключить VLD, необходимо прописать #include <vld.h>.

Преимущество этой утилиты заключается в том, что можно не запускать в режиме debug (F5), ибо все выводится в консоль. В самом начале будет выводиться это:

Visual Leak Detector read settings from: C:Program Files (x86)Visual Leak Detectorvld.ini
Visual Leak Detector Version 2.5.1 installed.

И вот, что будет выдавать при утечке памяти:

WARNING: Visual Leak Detector detected memory leaks!
---------- Block 1 at 0x01405FD0: 376 bytes ----------
  Leak Hash: 0x555D2B67, Count: 1, Total 376 bytes
  Call Stack (TID 8908):
    ucrtbased.dll!malloc()
    test.exe!0x00F41946()
    test.exe!0x00F42E1D()
    test.exe!0x00F44723()
    test.exe!0x00F44577()
    test.exe!0x00F4440D()
    test.exe!0x00F447A8()
    KERNEL32.DLL!BaseThreadInitThunk() + 0x19 bytes
    ntdll.dll!RtlGetAppContainerNamedObjectPath() + 0xED bytes
    ntdll.dll!RtlGetAppContainerNamedObjectPath() + 0xBD bytes
  Data:
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........
    CD CD CD CD    CD CD CD CD    CD CD CD CD    CD CD CD CD     ........ ........

Visual Leak Detector detected 1 memory leak (412 bytes).
Largest number used: 3115 bytes.
Total allocations: 3563 bytes.
Visual Leak Detector is now exiting.

Вот, я вижу трассировку! Так, а где строки кода? А где названия функций?

Ладно, обещание сдержали, однако это не тот результат, который я хотел.

Остается один вариант, который я нашел в гугле: моментальный снимок памяти. Он делается просто: в режиме debug, когда доходите до return 0, необходимо в средстве диагностики перейти во вкладку «Использование памяти» и нажать на «Сделать снимок». Возможно, у вас будет отключена эта функция, как на первом скриншоте. Тогда необходимо включить, и перезапустить дебаг.

Первый скриншот

Второй скриншот

После того, как вы сделали снимок, у вас появится под кучей размер. Я думаю, это сколько всего было выделено памяти в ходе работы программы. Нажимаем на этот размер. У нас появится окошко, в котором будут содержаться объекты, которые хранятся в этой куче. Чтобы посмотреть подробную информацию, необходимо выбрать объект и нажать на кнопку «Экземпляры представления объекта Foo».

Третий скриншот

Четвертый скриншот

Да! Это победа! Полная трассировка с местоположением вызовов! Это то, что было необходимо изначально.

Linux — разработка
Теперь, посмотрим, что творится в Linux.

В Linux существует утилита valgrind. Чтобы установить valgrind, необходимо в консоли прописать sudo apt install valgrind (Для Debian-семейства).

Я написал небольшую программу, которая заполняет динамический массив, но при этом, не очищается память:

main.c

#include <stdlib.h>
#include <stdio.h>
#define N 10

int main()
{
    int * mas = (int *)malloc(N * sizeof(int));
    for(int i = 0; i < N; i++)
    {
        *(mas+i) = i;
        printf("%dt", *(mas+i));
    }
    printf("n");
    return 0;
}

Скомпилировав программу с помощью CLang, мы получаем .out файл, который мы подкидываем valgrind’у.

С помощью команды valgrind ./a.out. Как работает valgrind, думаю, есть смысл описать в отдельной статье, а сейчас, как выполнится программа, valgrind выведет это:

==2342== HEAP SUMMARY:
==2342==     in use at exit: 40 bytes in 1 blocks
==2342==   total heap usage: 2 allocs, 1 frees, 1,064 bytes allocated
==2342== 
==2342== Searching for pointers to 1 not-freed blocks
==2342== Checked 68,984 bytes
==2342== 
==2342== LEAK SUMMARY:
==2342==    definitely lost: 40 bytes in 1 blocks
==2342==    indirectly lost: 0 bytes in 0 blocks
==2342==      possibly lost: 0 bytes in 0 blocks
==2342==    still reachable: 0 bytes in 0 blocks
==2342==         suppressed: 0 bytes in 0 blocks
==2342== Rerun with --leak-check=full to see details of leaked memory
==2342== 
==2342== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
==2342== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Таким образом, valgrind пока показывает, сколько памяти было потеряно. Чтобы увидеть, где была выделена память, необходимо прописать --leak-check=full, и тогда, valgrind, помимо выше описанного, выведет это:

==2348== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==2348==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2348==    by 0x40053A: main (in /home/hunterlan/Habr/a.out)

Конечно, тут не указана строка, однако уже указана функция, что не может не радовать.

Есть альтернативы valgrind’у, такие как strace или Dr.Memory, но я ими не пользовался, да и они применяется в основном там, где valgrind бессилен.

Выводы

Я рад, что мне довелось столкнуться с проблемой поиска утечки памяти в Visual Studio, так как я узнал много новых инструментов, когда и как ими пользоваться и начал разбирать, как работают эти инструменты.

Спасибо вам за внимания, удачного написания кода вам!

It’s safe enough to say that every day hundreds of C++ developers get the message “detected memory leaks” in their Visual Studio. That is how you would view it:

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

Of course, all developers want to fix such an error as soon as possible. In this article, we will show how to activate CRT leaks detection, how to utilize CRT to catch leaks, and tell you about the better and simpler way to catch leaks in C++ applications, no matter where they come from: CRT, MFC or usual heap allocation functions.

How to enable the CRT memory leak detection?

You never got the message above by default. To activate leak detection, you should add some headers and defines, and finally call _CrtDumpMemoryLeaks:

#ifdef _DEBUG
#define _CRTDBG_MAP_ALLOC
#define _CRTDBG_MAP_ALLOC_NEW
#include <crtdbg.h>
#include <assert.h>
#endif

int main()
{
    auto p = new int[0x100];

    std::cout << "Hello World!n";

    _CrtDumpMemoryLeaks();
}

After that rebuild the project and run. You will see the famous “detected memory leaks” in the Output Windows.

How does the CRT leak detection work?

This message tells a developer that CRT (C Runtime Library) or MFC has found a memory leak. In other words, somewhere a memory block was allocated but the developer has forgotten  to free it later.

Internally, CRT stores a list of allocated memory blocks. When a function _CrtDumpMemoryLeaks is called, it outputs information about live memory blocks in a human-readable format to the Output Window.

Tip
If you want to dump the information about memory leaks to a file, instruct CRT to use a file by calling _CrtSetReportMode with _CRTDBG_MODE_FILE, and then call _CrtSetReportFile.

Unfortunately, often CRT shows very little information about leaks: only size, content, and allocation number. As you see, it doesn’t include the most important information: a call stack. Moreover, you need to know where the memory was allocated to fix it.

Detected memory leaks!

When a CRT allocates a memory block, it increments a counter that saves it to the header of, that has just been allocated. If you are lucky, the allocation number can help you catch a leak. In the case of a simple program, if its flow is the same on each run, each allocation has the same number.

To try to catch the leak, set _crtBreakAlloc to allocation number of the leaks, and run the debugging once again. CRT breaks when an allocation number reaches the value:

int main()
{
    _crtBreakAlloc = 217;

    auto p = new int[0x100];

    std::cout << "Hello World!n";

    _CrtDumpMemoryLeaks();
}

Below is how it works:

Allocation number helps find memory leaks

However, complex applications allocate memory in a different order. This makes an allocation number is useless to leaks debugging as the leaked memory block will get different allocation numbers on each run.

False positives

Another issue is that _CrtDumpMemoryLeaks is called before destructors of global objects. That is why it outputs memory block information, that will either freed later or not. _CrtDumpMemoryLeaks can’t help you if global objects allocate and free memory.

Visual Studio Extension to catch leaks

Fortunately, there is a better way to solve the problem. Deleaker is an extension for Visual Studio that assists a developer to debug leaks.

After installing, you will find a new menu item Deleaker in Visual Studio. Start debugging as usually. Deleaker immediately comes and suggests to monitor all allocations made by a process. Deleaker saves call stack and other information of each memory block.

While debugging, you can switch to the Deleaker window and take a snapshot to explore currently allocated objects. When a process quits, Deleaker automatically prepares a final snapshot that contains possible leaks. Thanks to Deleaker filters, the developer focuses on those leaks he is responsible for.

Return to the sample, and comment lines where you set _crtBreakAlloc and call of _CrtDumpMemoryLeaks. Rebuild the project and start debugging once again:

Deleaker detects memory leaks

Deleaker has found a leak that was introduced, it correctly detected the exact line and the source file where the memory was allocated. You can review the call stack. Just double-click the allocation to navigate to the source code. The source file is opened in Visual Studio and you are ready to start working with the code.

Wrapping up

Some developers names Deleaker “a Valgrind for Visual Studio”. Indeed, you may consider Deleaker as an alternative for Valgrind that works on Windows and integrates with all major development environments including Visual Studio, C++ Builder, Qt Creator, and CLion.

CRT is not helpful in most cases as very often it doesn’t show call stacks, and its reports contain memory that can be freed later.

Instead of raw console output (both CRT and Valgrind doesn’t have any UI), Deleaker has a nice UI that helps quickly locate and fix memory issues, explore memory usage in real-time.

Deleaker snapshots make memory leak detection in Visual Studio comfortable: a developer just compare consequent snapshots to find spots that commit memory actively.

Also, while CRT is designed for memory allocated by C/C++ functions only (malloc, calloc, operator new and new[]), Deleaker detects all kinds of leaks such as GDI leaks, handles leaks and leaks made by COM.


Утечка памяти – это неправильное размещение ресурса в компьютерной программе из-за неправильного распределения памяти. Это происходит, когда неиспользуемая область ОЗУ остается невыпущенной. Утечку памяти не следует путать с утечкой пространства, которая относится к программе, использующей больше оперативной памяти, чем необходимо. Утечка памяти в системе Windows 10/8/7, как говорят, произошла, когда память просто недоступна, несмотря на то, что она не используется.

Содержание

  1. Утечки памяти в Windows 10
  2. Предотвращение утечек памяти
  3. Устранение утечек памяти в Windows

Утечки памяти в Windows 10

Прежде чем начать, вы должны знать, что утечка памяти – это проблема программного обеспечения для отладки – например, в Java, JavaScript, C/C ++, Windows и т. Д. Физическая замена ОЗУ или жесткого диска не требуется.

Почему это плохо

Очевидно, что утечка памяти – это плохо, потому что это ошибка, недостаток в системе. Но давайте выясним, как именно это влияет на систему:

  1. Поскольку память не освобождается, даже когда она не используется, это приводит к ее истощению.
  2. Исчерпание памяти приводит к старению программного обеспечения.
  3. Уменьшение доступной памяти приводит к увеличению времени отклика и снижению производительности системы.
  4. Неконтролируемая утечка памяти может в конечном итоге привести к сбою приложения.

Чтобы идентифицировать утечку памяти, программист должен иметь доступ к исходному коду программы.

Обнаружение утечки

Чтобы решить проблему, нам нужно сначала ее идентифицировать. Основные шаги по обнаружению утечки памяти:

  1. Подтверждение . Определение наличия утечки.
  2. Поиск утечки памяти в режиме ядра . Поиск утечки, вызванной компонентом драйвера режима ядра.
  3. Поиск утечки памяти в пользовательском режиме . Поиск утечки, вызванной драйвером пользовательского режима или приложением.

Распределение памяти

Существуют разные режимы, в которых приложения выделяют оперативную память. Если пространство не освобождается после использования, утечка памяти будет происходить независимо от режима выделения. Некоторые общие шаблоны распределения:

  1. Функция HealAlloc для выделения кучи памяти. Эквивалентами времени выполнения C/C ++ являются malloc и новые.
  2. Функция VirtualAlloc для прямого выделения из ОС.
  3. Kernel32 API для хранения памяти ядра для приложения. Пример, CreateFile, CreateThread.
  4. User32 API и Gdi32 API.

Предотвращение утечек памяти

Мы все знаем, что профилактика лучше лечения, поэтому есть несколько способов предотвратить утечку памяти.

Мониторинг привычек

Вы должны следить за ненормальным использованием ОЗУ отдельными программами и приложениями. Вы можете перейти в диспетчер задач Windows, нажав CTRL + SHIFT + ESC и добавить такие столбцы, как дескрипторы, объекты пользователя, объекты GDI и т. Д.

Это поможет вам легко отслеживать использование ресурсов.

Инструменты Microsoft для диагностики утечек памяти

Различные инструменты диагностируют утечки памяти для различных режимов выделения:

  1. Верификатор приложения диагностирует утечки кучи.
  2. UMDH (компонент средств отладки Windows) диагностирует утечки для отдельных процессов, отслеживая выделение кучи памяти.
  3. Trace Capture для тщательного анализа использования оперативной памяти.
  4. Xperf также отслеживает шаблоны распределения кучи.
  5. CRT Debug Heap не только отслеживает выделение кучи, но также позволяет использовать методы кодирования для минимизации утечек.
  6. JavaScript Memory Leak Detector отлаживает утечки памяти в кодах.

Советы по использованию

  1. Используйте ядра HANDLE и другие умные указатели для ресурсов Win32 и выделения кучи.
  2. Получите классы для автоматического управления ресурсами для выделения ядра из библиотеки ATL. Стандарт C ++ имеет auto_ptr для распределения кучи.
  3. Инкапсулируйте указатели COM-интерфейса в «умные указатели» с помощью _com_ptr_t или _bstr_t или _variant_t .
  4. Мониторинг кода .NET на предмет ненормального использования памяти.
  5. Избегайте множественных путей выхода для функций, чтобы к концу функции освободить выделения из переменных в большинстве блоков.
  6. Используйте собственные исключения только после освобождения всех выделений в блоке _finally. Оберните всю кучу и обработайте выделения в интеллектуальные указатели, чтобы использовать исключения C ++.
  7. Всегда вызывайте функцию PropVariantClear перед повторной инициализацией или удалением объекта PROPVARIANT.

Устранение утечек памяти в Windows

Так же, как различные способы предотвращения утечек памяти, существуют различные способы остановить утечки памяти.

1] Закройте процессы и перезапустите.

Если вы видите, что ненужный процесс занимает слишком много ОЗУ, вы можете завершить процесс в диспетчере задач. Вам нужно будет перезагрузить устройство, чтобы освободившееся пространство было доступно для использования другими процессами. Без перезагрузки проблема утечки памяти не будет решена. Одним из конкретных процессов, которые имеют ошибки для замедления работы ПК, является Runtime Broker. Попробуйте, если отключение, которое само по себе работает.

2] Инструменты диагностики памяти

Чтобы получить доступ к встроенному инструменту диагностики памяти для Windows:

  1. Сохраните всю вашу важную работу.
  2. Нажмите Win + R , чтобы открыть окно Выполнить .
  3. Введите команду mdsched.exe в окне Выполнить .
  4. Перезагрузите компьютер.
  5. После перезапуска выполните базовое сканирование или выберите параметры Расширенные , например Test mix ’или Количество проходов ’.
  6. Нажмите F10 , чтобы начать тестирование.

Это все еще временные исправления.

3] Проверить обновления драйверов

Устаревшие драйверы вызывают утечки памяти. Держите все драйверы обновленными:

  1. Нажмите Win + R и откройте окно Выполнить . Введите devmgmt.msc и нажмите Enter. Вы попадете в Диспетчер устройств .
  2. Проверьте устаревшие драйверы и обновите их все.
  3. Для обновлений, которые вы могли пропустить, проверьте в Центре обновления Windows.

Это было просто.

4] Оптимизация производительности

Настройка Windows на производительность будет управлять всем, включая планирование процессора и использование памяти, чтобы предотвратить утечки памяти. Следуй этим шагам:

  1. Нажмите правой кнопкой мыши на Этот компьютер ’и выберите настройки Дополнительно на левой панели.
  2. На вкладке “ Дополнительно ” перейдите в раздел “ Эффективность “, а затем “ Настройки “.
  3. Установите флажок Настроить для лучшей производительности и нажмите ОК .
  4. Перезапустите и проверьте, решена ли проблема.

Если это простое решение не сработало, попробуйте следующее решение.

5] Отключить программы, запускаемые при запуске

Отключение проблемных программ – единственный способ избавиться от проблемы утечки памяти. Зайдите в диспетчер задач и отключите программу, создающую проблемы. Если вы не знаете, какие программы создают проблемы, сделайте следующее:

  1. Перейдите в Диспетчер задач .
  2. Перейдите в “ Запуск “.
  3. Отключите автозапуск программ, которые вам не нужно запускать по умолчанию.

6] Дефрагментация жесткого диска

Хотя Windows 10 делает это для вас автоматически, вам может понадобиться время от времени выполнять дефрагментацию жестких дисков для оптимизации производительности:

  1. Перейдите на страницу “ Этот компьютер ” или “ Мой компьютер “.
  2. Щелкните правой кнопкой мыши системный жесткий диск (обычно диск C:).
  3. Перейдите на вкладку Инструменты и выберите Свойства ‘и выберите Оптимизировать ’.
  4. Выберите диск для дефрагментации и выберите « Анализировать ».

Перезагрузите компьютер после новой фрагментации.

7] Файл ClearPage при завершении работы

Сейчас становится все сложнее, но не волнуйтесь. Вот как очищать файл подкачки при каждом выключении:

  1. Введите regedit в поле поиска, чтобы запустить редактор реестра.
  2. Введите этот путь: HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession ManagerMemory Management
  3. Измените значение ClearPageFileAtShutDown на «1».
  4. Сохраните изменения и перезагрузите устройство.

Это должно сделать это.

9] Отключить суперпатч

Этот сервис Windows оптимизирует производительность за счет минимизации времени загрузки. Это позволяет Windows управлять использованием оперативной памяти. Жизнь после отключения Superfetch не удобна, но сделайте это, если нужно. По крайней мере, попробуйте это в одиночку, чтобы изолировать проблему:

  1. Найдите services.msc и перейдите в диспетчер служб.
  2. Найдите Superfetch и нажмите его правой кнопкой мыши, чтобы перейти в Свойства .
  3. Выберите « Стоп ».
  4. Также Отключить ’сервис из раскрывающегося меню.
  5. Перезагрузите компьютер и проверьте, не улучшилась ли производительность.

Включите Superfetch, если этот не работает.

10] Проверка на наличие вредоносных программ

Используйте стороннее антивирусное программное обеспечение или встроенный в Windows 10 Защитник Windows для сканирования и устранения вредоносных программ.Убедитесь, что антивирус обновлен для поддержки вашей ОС, чтобы он не стал причиной утечки памяти.

Каждое решение, которое вам когда-либо понадобится, чтобы найти или предотвратить утечку памяти, находится здесь. Вы можете прочитать больше об утечках памяти на MSDN и Microsoft.

В работе участвовали:
Гильденберг Максим и Панкратов Семен

Утечка памяти достаточно серьезная проблема возникающая при работе программы. Масштабность проявляется особенно при длительной работе программы, когда программа может исчерпать лимит выделения для нее памяти, а это приведет к очень нехорошим последствиям.

Простейшая программа с утечкой памяти:

#include <iostream>
int main() {
  int* p = new int[2];
  std::cout << p;
  // delete[] p;
}

В данном участке кода утечка памяти происходит из-за отсутствия оператора delete[] для массива p.

В статье «Анализ программ и компиляторов в Compiler Explorer» показано, что в ряде случаев утечки памяти можно найти средствами статического анализа программ, однако такие инструменты нередко дают «ложные срабатывания», а иногда — не обнаруживают утечку.

В этой статье рассмотрены:

  1. Библиотека CRT (Windows);
  2. Visual Leak Detector (Windows);
  3. Valgrid memchek (Linux);
  4. Как это устроено внутри.
  5. Более сложный пример. Результаты.

1 Обнаружение утечек с библиотекой CRT

Библиотека CRT доступна в операционной системе Windows, она у вас уже есть если вы используете Microsoft Visual Studio. Для подключения анализатора памяти достаточно добавить пару макросов и собрать проект в отладочном режиме:

// в начало файла с точкой входа:
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#define DBG_NEW new( _NORMAL_BLOCK , __FILE__ , __LINE__ )
#define newDBG_NEW

#include <iostream>
int main() {
  int* p = new int[2];
  std::cout << p;
  // delete[] p;
  
  // перед завершением работы программы
  _CrtDumpMemoryLeaks();
}

Видно, что первая группа макросов добавляется в самое начало вашей программы. За счет этих макросов все обращения в функциям new, malloc, free и delete заменяются на другие версии, которые помимо выделения/освобождения памяти сохраняют дополнительную информацию (сколько и где было выделено). Макрос _CrtDumpMemoryLeaks добавляется в конец программы (перед завершением работы) и выводит информацию о текущем состоянии памяти.

Для приведенной выше программы в окно отладки будет выведено следующее:

Утечка была обнаружена, кроме того, нам показывают ее размер и даже строку кода где это случилось.

2 Использование Visual Leak Detector

VLD поставляется в виде плагина для Microsoft Visual Studio, для его установки:
1) Скачиваем с официального сайта Microsoft и устанавливаем.
2) Добавляем в свойствах проекта во «включаемые каталоги» путь к каталогу include:
C:ProgramFiles(x86)VisualLeakDetectorinclude.
3) Добавляем в свойствах проекта в Каталог библиотек для win32 или win64 : путь к катлогу lib:

C:ProgramFiles(x86)VisualLeakDetectorlibWin32
C:ProgramFiles(x86)VisualLeakDetectorlibWin64 

4) Добавляем в начало главного файла (main.cpp) #include<vld.h>
Получается так:

#include<vld.h>
#include <iostream>
int main() {
  int* p = new int[2];
  std::cout << p;
  // delete[] p;
}

Все говото, утечки ищутся и выводятся в консоль отладки:

Видно, что VLD выводит информацию о стеке вызовов, но формат вывода крайне неудобный. Узнать в какой строке произошла утечка памяти совсем непросто.

3 Работа с Valgrind memcheck

Valgrind предоставляет множество инструментов для динамического анализа, поиском утечек памяти это не ограничивается. Работой с памятью занимается модуль memcheck. Для его использования необходимо:
1) Сстановить valgrind:
sudo apt install valgrind

2) Скомпилировать программу в debug-режиме и запустить ее через valgrind:
valgrind ./my_program
При таком запуске будет выведено сколько памяти было потеряно. Чтобы увидеть, где была выделена память, необходимо добавить опцию
--leak-check=full.

Результаты работы memcheck для нашего примера:

4 Как это устроено внутри

Подключение CRT приводит к оборачиванию функций работы с памятью в что-то такое:

__declspec(align(16)) struct SMVECTOR
{
    union
    {
        __m128 mmv;
        struct
        {
            float x;
            float y;
            float z;
            float w;
        };
    };
 
    void* operator new(size_t size)
    {
        return (_aligned_malloc(size, 16));
    };
                 
    void operator delete(void* ptr)
    {
        _aligned_free(ptr);
    };
 
    void* operator new[](size_t size)
    {
        return (_aligned_malloc(size, 16));
    };
                 
    void operator delete[](void* ptr)
    {
        _aligned_free(ptr);
    };
};

Вы могли бы сделать это сами, но это непросто, а ведь надо еще разобрать все возможные ошибки при работе с памятью — free вместо delete, выделение объекта одного типа, а удаление — другого и так далее.

Да и зачем этим заниматься если его готовый CRT? В свою очередь, Visual Leak Detector представляет собой обертку над CRT.

Утилита Valgrind работает совсем иначе — ей не требуется модифировать исходный код вашей программы. Вместо этого программа запускается на виртуальном (моделируемом) процессоре в виртуальном окружении. Это окружение точно знает сколько памяти потребила программа, а процессор — в каких инструкциях эта память была запрошена. За счет этого valgrind позволяет не только обнаружить утечки памяти, но и получить статистику кэш-попаданий (модуль cachegrind), например. Однако, тут внутреннее устройство valgrind показано крайне упрощенно, а на самом деле все гораздо сложнее. Тем не менее, это один из лучших инструментов поиска утечек в мире.

4 Более сложный пример

Возможно, вам кажется что проблем с поиском утечек не возникает, тогда посмотрите такой пример:

#include <iostream>

class Animal {
    char *name;
public:
    Animal() {
        name = new char[50];
        std::cout << "Animaln";
    }
    /*virtual */~Animal() {
        delete name;
        std::cout << "~Animaln";
    }
    virtual void say() = 0;
};

class Cat : public Animal {
public:
    Cat() {
        std::cout << "Catn";
        color = new char[50];
    }

    ~Cat() {
        std::cout << "~Catn";
        free(color);
    }

    virtual void say() {
        std::cout << "meown";
    }
protected:
   char *color;
};

int main() {
    Animal* p = new Cat();
    p->say();

    //delete p;

    return 0;
}

Сколько времени у вас уйдет на поиск всех утечек? Ведь после возврата строки delete p в программе появятся другие утечки. Для начала — не будет вызывать деструктор ~Cat() ведь деструктор ~Animal() объявлен невиртуальным. После исправления этой ошибки — мы получим еще одну, ведь в классе Cat память выделяется через new, а освобождается через free.

Результаты применения всех трех рассмотренных инструментов примерно одинаковы (разве что VLD не выводит информацию о строке возникновения ошибки). Ниже приведен вывод valgring memcheck:

Видно, что обнаруживается 3 утечки, так как на 3 аллокации не приходится ни одной операции освобождения. После добавления delete в функцию main получаем одну утечку. Теперь на 3 аллокации приходится две операции освобождения. Найдена лишь одна утечка, однако на самом деле — вообще не вызывается деструктор ~Cat(), поэтому после замены free на delete в нем — результат работы не изменится. А вот после добавления виртуального деструктора — утечек найдено не будет.

Понятно, что описанные инструменты сильно упрощают жизнь программисту. Найти без них течки памяти в чужом коде из 10 тысяч строк кода — задача крайне сложная, а с ними — решается элементарно. Однако, вывод результатов их работы очень неинформативный. Сравните полученные результаты с выводом статических анализаторов (в обоих статьях используются одинаковые примеры).

Каждый программист, работающий с языком С++, должен уметь находить утечки памяти. Язык С++ — сложный язык, делать ошибки легко, а находить их бывает муторно. Особенно это касается утечек памяти. Ситуация с отловом утечек памяти только усугубляется, если в коде С++ используется библиотека Qt.

Эта статья посвящена разным инструментам, которые можно с той или иной степенью успешности применять для отлова утечек памяти в С++/Qt приложениях (desktop). Инструменты будут рассмотрены в связке с IDE Visual Studio 2019. В статье будут рассмотрены не все возможные инструменты, а лишь наиболее популярные и эффективные.

Наша команда давно и пристально изучает подобные инструменты и использует их в своей работе. Объем кода, на котором есть возможность проверить подобные инструменты, составляет около 1.5 миллиона строк. Опираясь на большой практический опыт, мы расскажем о плюсах и минусах разных инструментов, расскажем, что они способны найти, а что не по зубам, расскажем о неочевидных нюансах и, главное, составим сводную сравнительную таблицу по реальному примеру. Мы постараемся максимально быстро и просто ввести в курс дела (показать быстрый старт), потому даже если ты, читатель, никогда не занимался поиском утечек памяти, эта статья поможет за пару часов разобраться и найти свою первую утечку. Поехали!

В чем проблема?

Утечка памяти – ситуация, когда память была выделена (например, оператором new) и ошибочно не была удалена соответствующим оператором/функцией удаления (например, delete).

Пример 1.

int* array = nullptr;
for (int i = 0; i < 5; i++)
{
	array = new int[10];
}
delete[] array;

Здесь налицо утечка при выделении памяти для первых 4 массивов. Утекает 160 байт. Последний массив удаляется корректно. Итак, утечка строго в одной строке:

array = new int[10];

Пример 2.

class Test
{
public:
	Test()
	{
		a = new int[100];
		b = new int[300];
	}
	~Test()
	{
		delete[] a;
		delete[] b;
	}

private:
	int* a;
	int* b;
};

int main()
{
	Test* test = new Test;

	return 0;
}

Здесь утечек уже больше: не удаляется память для a (400 байт), для b (1200 байт) и для test (16 байт для x64). Впрочем, удаление a и b в коде предусмотрено, но его не происходит из-за отсутствия вызова деструктора Test. Таким образом, утечек три, но ошибка, приводящая к этим утечкам, всего одна, и она порождается строкой

Test* test = new Test;

При этом в коде класса Test ошибок нет.

Пример 3.

Пусть есть класс Qt, примерно такой:

class InfoRectangle : public QLabel
{
	Q_OBJECT

public:
	InfoRectangle(QWidget* parent = nullptr);

private slots:
	void setInfoTextDelayed();

private:
	QTimer* _textSetTimer;
};
InfoRectangle::InfoRectangle(QWidget* parent)
	: QLabel(parent)
{
	_textSetTimer = new QTimer(this);
	_textSetTimer->setInterval(50);
	connect(_textSetTimer, &QTimer::timeout, this, &InfoRectangle::setInfoTextDelayed);
}

void InfoRectangle::setInfoTextDelayed()
{
	// do anything
	setVisible(true);
}

Пусть также где-то в коде затесалось выделение памяти:

InfoRectangle* rectangle = new InfoRectangle();

Будет ли являться это утечкой, если явно не вызван delete? Это зависит от того, включен ли объект в иерархию объектов Qt. Если объект включён одним из следующих примерных вызовов, то нет, не утечка:

mnuLayout->addWidget(rectangle);
rectangle->setParent(this);

В остальных же случаях – утечка. Причем если мы будем считать точное количество утечек в этом примере, то можем наткнуться на неожиданный вывод: утечек больше, чем можно сначала предположить. Очевидная утечка – выделение памяти для InfoRectangle. Побочная утечка – выделение памяти для QTimer, несмотря на включение объекта _textSetTimer в иерархию объектов Qt. А вот утечка, которая совсем не очевидна – вызов функции connect.

Дело в том, что в ее реализации вызовом new всё же создается некий объект:

template <typename Func1, typename Func2>
    static inline QMetaObject::Connection connect(
const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal,
                const typename QtPrivate::FunctionPointer<Func2>::Object *receiver, Func2 slot,
                Qt::ConnectionType type = Qt::AutoConnection)
    {
        typedef QtPrivate::FunctionPointer<Func1> SignalType;
        typedef QtPrivate::FunctionPointer<Func2> SlotType;

        const int *types = nullptr;
        if (type == Qt::QueuedConnection || type == Qt::BlockingQueuedConnection)
            types = QtPrivate::ConnectionTypes<typename SignalType::Arguments>::types();

        return connectImpl(sender, reinterpret_cast<void **>(&signal),
                           receiver, reinterpret_cast<void **>(&slot),
                           new QtPrivate::QSlotObject<Func2, typename QtPrivate::List_Left<
typename SignalType::Arguments, SlotType::ArgumentCount>::Value,
                                          	typename SignalType::ReturnType>(slot),
                            type, types, &SignalType::Object::staticMetaObject);
    } 

Таким образом, утечки памяти могут обнаруживаться в весьма неожиданных местах. А могут и вовсе не обнаруживаться, в зависимости от используемого инструментария. К примеру, если инструментарий не способен проникнуть внутрь кода Qt, то он не сможет ни обнаружить утечку в connect, ни разобраться с иерархиями Qt, а последнее, в свою очередь, уже чревато ложными срабатываниями.

Зафиксируем еще один важный момент: причина утечки одна, а самих утечек при этом может быть много. Этот нюанс важен при обсуждении инструментариев  по отлову утечек. Идеальный инструментарий – тот, который, не давая ложных срабатываний и не пропуская утечек, указывает именно на первопричины утечек, то есть на каждую причину выдает ровно одну строку кода, ей соответствующую (а не целую кучу). То есть делает так, как делал бы человек. Забегая вперед, скажем, что таких инструментариев на данный момент не существует.

Раз их не существует, то как сравнивать между собой разные, не всегда коррелирующие результаты поиска утечек, полученные разными реальными инструментами? Мы ведь хотим сравнения…

Для этого мы взяли реальный пример – конкретную ревизию репозитория, для которой было точно известно, что утечки есть, и начали подробно с ней работать. Мы взяли один типичный сценарий работы пользователя с нашим приложением и начали на нём запускать все возможные динамические инструменты отлова утечек. Запускали многократно с разными настройками, детально анализируя полученные отчеты об ошибках. В итоге сформировали итоговый список ошибок в коде, приведших к утечкам памяти. Этот список назвали эталонным и посчитали, что других ошибок (ненайденных) нет. И в дальнейшем результаты, полученные каждой отдельной утилитой поиска утечек, сравнивали с эталонным списком ошибок.

Проект

Размер кода

Сценарий работы пользователя

Ревизия репозитория

Кол-во ошибок в эталоне

Суммарный объем утекающей памяти

Конкретный проект

1.5 млн строк

Конкретный сценарий: запускаем ПО, жмем на кнопку 1, потом на кнопку 2, ждем завершения вычислений, закрываем ПО

конкретная

7

253 кБ

Таблица 1. Эталон поиска утечек памяти.

Перейдем теперь непосредственно к обзору конкретных утилит, помогающих искать утечки памяти.

Intel Inspector

Intel Inspector – графическая утилита, удобно встраиваемая в Visual Studio и позволяющая в виде кликабельного списка выдавать места в коде с предполагаемыми утечками оперативной памяти проверяемого приложения и некоторыми другими проблемами памяти. В сценарии отлова утечек памяти Intel Inspector использует динамический анализ, а это значит, что если в процессе работы приложения код с утечками памяти не будет вызван, то и проблем в нем не будет найдено.

Установка

Intel Inspector входит в состав пакета Intel Parallel Studio 2019, при этом есть возможность установить только сам Intel Inspector, убрав галочки с остальных компонентов дистрибутива при установке. Visual Studio 2019 должна быть закрыта в момент установки Intel Parallel Studio. После установки, Intel Inspector будет автоматически встроен в Visual Studio и должен появиться на панели инструментов (рис. 1).

Рис. 1. Начало работы с Intel Inspector`ом

Рис. 1. Начало работы с Intel Inspector`ом

Если значок Intel Inspector’а не виден на панели инструментов, нужно щёлкнуть правой кнопкой мыши где-нибудь на этой панели инструментов и поставить галочку «Intel Inspector».

Запуск

При нажатии на кнопку-значок появится вкладка Intel Inspector с выбором глубины анализа. Выбираем первый пункт «Detect Leaks» и включаем все галочки, соответствующие всем видам анализа (рис. 2). Если какие-то галочки пропустить, то, к сожалению, есть риск, что не все утечки будут найдены.

Рис. 2. Вкладка Intel Inspector`а для его настройки и запуска

Рис. 2. Вкладка Intel Inspector`а для его настройки и запуска

Далее нажимаем кнопку «Start», через некоторое время откроется приложение. В нем нужно запустить тот или иной сценарий работы, а лучше все сразу (то есть, как следует «погонять» приложение), затем закрыть. Чем больше на разных параметрах, в разных режимах и в разных сценариях проработает приложение, тем больше утечек памяти будет найдено. И это общий принцип для всех механизмов поиска утечек, использующих динамический анализ. Как мы уточнили ранее, в целях сравнения мы запускали только эталонный сценарий тестирования (см. табл. 1). Итак, после закрытия приложения Intel Inspector слегка задумывается и в итоге выдаёт отчёт следующего вида (рис. 3):

Рис. 3. Пример результатов анализа ПО на утечки памяти с помощью Intel Inspector.

Рис. 3. Пример результатов анализа ПО на утечки памяти с помощью Intel Inspector.

В отчете выдаются кликабельный и сортируемый список утечек, размеры утечек, места в коде с утечками, call-stack и многое другое.  Короче, форма выдачи результатов весьма и весьма на уровне. Все очень быстро понимается и усваивается. Все это – внутри IDE!

Это будет работать, если есть отладочная информация. То есть debug работать будет, а release нет. В С++-приложениях часто бывает так, что работа в режиме debug намного медленнее, чем в release (мы фиксировали разницу в скорости  до 20 раз), и пользоваться debug’ом очень некомфортно. Однако на этот случай есть лайфхак – собрать версию release (быструю, со всеми ключами оптимизации), дополнительно включив в нее отладочную  информацию. Это позволяет Intel Inspector’у подсветить строки в исходном коде, где он предполагает наличие утечек. О том, как включить в release отладочную информацию, написано здесь.

Результаты

Мы провели сравнение скоростных характеристик работы приложения в разных режимах работы: с Intel Inspector (будем называть его Инспектор) и без него, в debug и release. Тестирование проводилось на эталонном примере (см. табл 1).

Конфигурация

Среднее время теста, с

Замедление
работы, что привносит Инспектор, раз

Без Инспектора

С Инспектором

Release c отладочной информацией

10

70

7

Debug

101

973

9,6

Таблица 2. Время тестирования с учётом работы Intel Inspector`а

По таблице можно видеть, что проверка на утечки памяти требует весьма существенного времени. Особенно этот факт начинает играть существенными красками в свете непрерывной интеграции. То есть, если, к примеру, тот сценарий, на котором мы проверяли, включен в состав тестов регрессионного тестирования, то он в них выполняется за 10 секунд. Если же мы захотим на этом же сценарии прогонять поиск утечек памяти (в debug), то это займет примерно в 100 раз больше времени. Прогонять каждую ночь (а тем более, по каждому коммиту) тесты становится невозможно.

Самое главное – что можно сказать по качеству найденных утечек? Действительно ли они являются утечками? Нет ли утечек, не замеченных Intel Inspector`ом?

Конфигурация

Кол-во ошибок в эталоне: n

Найдено утечек

Пропущено ошибок из эталона: r

Точность: (n-r)/n

Избыточность: N/n

Всего: N

Верных

Ложных

Release c отладочной информацией

7

192

168

24

0

1 (100%)

27 раз

Debug

7

129

107

22

0

1 (100%)

18 раз

Таблица 3. Результаты работы Intel Inspector

Да, Intel Inspector действительно способен найти реальные утечки памяти. Это долго и мучительно, но он их находит. Пропусков утечек памяти мы не зафиксировали. При этом в итоговом отчете, который формирует Intel Inspector, бывает так, что на каждую строчку кода, где реально совершена ошибка, выводится куча строчек, которые «породились» этой ошибкой (как в примерах 2 и 3, см. выше).

Если ликвидировать все такие реальные ошибки, то Intel Inspector все равно будет показывать еще немало утечек, и все они – ложные. Более того, по таблице видно, что этих ложных срабатываний в release больше, чем в debug. И в этом случае, судя по всему, сработала оптимизация при компиляции – она скрыла от Инспектора некоторые детали, и Инспектор запутался.

Приведем несколько произвольных примеров ложных срабатываний.

Пример 1. Утечки в системных dll.

Intel Inspector может обнаружить вот такие вот странные утечки в подгружаемых системных dll, с таким интересным стеком. К проверяемому нами коду такие утечки вообще отношения не имеют, даже если по факту там утечки и есть.

Рис. 4. Утечки в системных dll.

Рис. 4. Утечки в системных dll.

Пример 2. aligned_malloc.

m_pVitData = (VITDEC_DATA*)_aligned_malloc(sizeof(VITDEC_DATA), 16);
m_pDcsnBuf = (byte*)_aligned_malloc(64 * (VITM6_BUF_LEN + VITM6_MAX_WND_LEN), 16);
...
_aligned_free(m_pDcsnBuf);
_aligned_free(m_pVitData);

К счастью, подобная «утечка» находится только в release, а в debug нет.

Пример 3. Pragma.

#pragma omp parallel for schedule(dynamic)
for (int portion = 0; portion < portionsToProcess; ++portion)
{
	…
}

Утечка показывается именно в строке с директивой #pragma!

Возможно, какими-то настройками (внутри Intel Inspector, внутри VS, переменные окружения и т.д.) можно победить этот вал ложных утечек, но из коробки – точно нет. Возможно также, что на маленьких и простых приложениях (<50000 строк кода) таких проблем с Intel Inspector не будет. На серьезных же приложениях – точно будут, к гадалке не ходи.

Вывод

Intel Inspector – штука довольно удобная и полезная, способная найти все утечки (если прогнать все сценарии), выдающая относительно немного ложных срабатываний. Работа в конфигурации release с отладочной информацией довольно быстра, но выдает больше ложных срабатываний (а значит, больше ручной работы по их проверке), чем debug. При этом работа в конфигурации debug фантастически медленна.

Что касается стабильности работы, то Intel Inspector здесь не может показать выдающиеся результаты. Иногда в процессе тестирования бывают падения, иногда зависания, причем на ровном месте. Иногда нам попадались такие важные для нас проекты и сценарии работы пользователя, когда  вообще не получалось даже «завести» Intel Inspector, не то, что «доехать» на нём до получения результатов.

Visual Leak Detector

Visual Leak Detector (далее VLD) – маленькая библиотека, включаемая в исходный код каждого проекта и выводящая в окно Output (IDE Visual Studio 2019) отчёт по утечкам памяти.

Установка

  1. Убедиться, что Visual Studio не запущена.

  2. Перед установкой VLD нужно сначала удалить старый VLD, если такой есть, через Панель управления, т.к. предлагаемая автоматическая деинсталляция старой версии не всегда работает.

  3. Далее установить последний VLD (на момент написания статьи vld-2.5.1-setup.exe) по умолчанию, оставив при установке все галочки включёнными (на добавление в Path и встраивание в Visual Studio). Установщик можно скачать отсюда.

  4. На момент написания статьи в дистрибутиве VLD нет нужных dll-файлов для работы с Visual Studio 2019, потому необходимо скопировать dbghelp.dll из папки C:Program Files (x86)Microsoft Visual Studio2019EnterpriseCommon7IDEExtensionsTestPlatformExtensionsCppx64 в папку C:Program Files (x86)Visual Leak DetectorbinWin64.

  5. Нужно создать заголовочный файл примерно со следующим содержанием:

    #pragma once
    
    //#define LEAKS_DETECTION
    
    #ifdef LEAKS_DETECTION
    #include <vld.h>
    #endif

    Как видно, пока что это пустой файл, проверка на утечки памяти в нем выключена.

  6. В любой файл реализации (сpp) нужно включить этот новый заголовочник. Это нужно сделать только для одного файла внутри проекта, и для каждого проекта в solution.

Запуск

Достаточно раскомментировать в заголовочном файле строчку

#define LEAKS_DETECTION

и собрать solution. После этого можно запускать (F5) приложение и прогонять разные сценарии, где могут быть утечки памяти. Запускать можно только в конфигурации debug. Release c отладочной информацией работать не будет.

После закрытия проверяемого приложения VLD выведет отчет в окно Output. Отчет содержит список утечек, кликабельный call-stack по каждой утечке, размеры утечек.

Пример того, что выводит VLD

---------- Block 652047 at 0x0000000027760070: 8787200 bytes ----------
  Leak Hash: 0x02B5C300, Count: 1, Total 8787200 bytes
  Call Stack (TID 30996):
    ucrtbased.dll!malloc()
    d:agent_work63ssrcvctoolscrtvcstartupsrcheapnew_array.cpp (29): SniperCore.dll!operator new[]()
    D:SOURCESAP_Gitsap_win64corealgfbgddecS2Ldfg.cpp (445): SniperCore.dll!CS2Ldfg::CreateLLRTbls() + 0xD bytes
    D:SOURCESAP_Gitsap_win64corealgfbgddecS2Ldfg.cpp (217): SniperCore.dll!CS2Ldfg::SetModeEB()
    D:SOURCESAP_Gitsap_win64corealgfbgddecS2Ldfg.cpp (1447): SniperCore.dll!CS2Ldfg::Set() + 0xA bytes
    D:SOURCESAP_Gitsap_win64corealgfbgddecddec.cpp (509): SniperCore.dll!DFBase::instanceS2Dec()
    D:SOURCESAP_Gitsap_win64corealgfbgddecddec.cpp (58): SniperCore.dll!DFBase::DFBase() + 0xF bytes
    D:SOURCESAP_Gitsap_win64corealgfbgddecddec.cpp (514): SniperCore.dll!DgbS5FecAnlzr::DgbS5FecAnlzr() + 0xA bytes
    D:SOURCESAP_Gitsap_win64corealgfbgfbganalyser.cpp (45): SniperCore.dll!TechnicalLayer::FBGAnalyser::FBGAnalyser() + 0x21 bytes
    D:SOURCESAP_Gitsap_win64coreenginehandlersfbganalysishandler.cpp (218): SniperCore.dll!TechnicalLayer::FBGAnalysisHandler::init() + 0x2A bytes
    D:SOURCESAP_Gitsap_win64coreenginehandlersfbganalysishandler.cpp (81): SniperCore.dll!TechnicalLayer::FBGAnalysisHandler::enqueueRequest()
    D:SOURCESAP_Gitsap_win64coreenginethreadedhandler2.cpp (57): SniperCore.dll!TotalCore::ThreadedHandler2::run()
    Qt5Cored.dll!QTextStream::realNumberPrecision() + 0x89E8E bytes
    kernel32.dll!BaseThreadInitThunk() + 0xD bytes
    ntdll.dll!RtlUserThreadStart() + 0x1D bytes
  Data:
    00 00 00 00    01 01 01 01    01 01 01 02    02 02 02 02     ........ ........
    02 02 03 03    03 03 03 03    03 04 04 04    04 04 04 04     ........ ........
    05 05 05 05    05 05 05 05    06 06 06 06    06 06 06 07     ........ ........
    07 07 07 07    07 07 08 08    08 08 08 08    08 09 09 09     ........ ........
    09 09 09 09    0A 0A 0A 0A    0A 0A 0A 0B    0B 0B 0B 0B     ........ ........
    0B 0B 0C 0C    0C 0C 0C 0C    0C 0D 0D 0D    0D 0D 0D 0D     ........ ........
    0E 0E 0E 0E    0E 0E 0E 0E    0F 0F 0F 0F    0F 0F 0F 10     ........ ........
    10 10 10 10    10 10 11 11    11 11 11 11    11 12 12 12     ........ ........
    EE EE EE EE    EF EF EF EF    EF EF EF F0    F0 F0 F0 F0     ........ ........
    F0 F0 F1 F1    F1 F1 F1 F1    F1 F2 F2 F2    F2 F2 F2 F2     ........ ........
    F3 F3 F3 F3    F3 F3 F3 F3    F4 F4 F4 F4    F4 F4 F4 F5     ........ ........
    F5 F5 F5 F5    F5 F5 F6 F6    F6 F6 F6 F6    F6 F7 F7 F7     ........ ........
    F7 F7 F7 F7    F8 F8 F8 F8    F8 F8 F8 F9    F9 F9 F9 F9     ........ ........
    F9 F9 FA FA    FA FA FA FA    FA FB FB FB    FB FB FB FB     ........ ........
    FC FC FC FC    FC FC FC FC    FD FD FD FD    FD FD FD FE     ........ ........
    FE FE FE FE    FE FE FF FF    FF FF FF FF    FF 00 00 00     ........ ........


---------- Block 2430410 at 0x000000002E535B70: 48 bytes ----------
  Leak Hash: 0x7062B343, Count: 1, Total 48 bytes
  Call Stack (TID 26748):
    ucrtbased.dll!malloc()
    d:agent_work63ssrcvctoolscrtvcstartupsrcheapnew_scalar.cpp (35): SniperCore.dll!operator new() + 0xA bytes
    C:Program Files (x86)Microsoft Visual Studio2019EnterpriseVCToolsMSVC14.28.29333includexmemory (78): SniperCore.dll!std::_Default_allocate_traits::_Allocate()
    C:Program Files (x86)Microsoft Visual Studio2019EnterpriseVCToolsMSVC14.28.29333includexmemory (206): SniperCore.dll!std::_Allocate<16,std::_Default_allocate_traits,0>() + 0xA bytes
    C:Program Files (x86)Microsoft Visual Studio2019EnterpriseVCToolsMSVC14.28.29333includexmemory (815): SniperCore.dll!std::allocator<TotalCore::TaskResult *>::allocate()
    C:Program Files (x86)Microsoft Visual Studio2019EnterpriseVCToolsMSVC14.28.29333includevector (744): SniperCore.dll!std::vector<TotalCore::TaskResult *,std::allocator<TotalCore::TaskResult *> >::_Emplace_reallocate<TotalCore::TaskResult * const &>() + 0xF bytes
    C:Program Files (x86)Microsoft Visual Studio2019EnterpriseVCToolsMSVC14.28.29333includevector (708): SniperCore.dll!std::vector<TotalCore::TaskResult *,std::allocator<TotalCore::TaskResult *> >::emplace_back<TotalCore::TaskResult * const &>() + 0x1F bytes
    C:Program Files (x86)Microsoft Visual Studio2019EnterpriseVCToolsMSVC14.28.29333includevector (718): SniperCore.dll!std::vector<TotalCore::TaskResult *,std::allocator<TotalCore::TaskResult *> >::push_back()
    D:SOURCESAP_Gitsap_win64includecoreenginetask.h (119): SniperCore.dll!TotalCore::LongPeriodTask::setTmpResult()
    D:SOURCESAP_Gitsap_win64includecoreenginediscretestephandler.h (95): SniperCore.dll!TotalCore::DiscreteStepHandler::setResult()
    D:SOURCESAP_Gitsap_win64coreenginehandlersprmbdtcthandler.cpp (760): SniperCore.dll!TechnicalLayer::PrmbDtctHandler::setContResult() + 0x1A bytes
    D:SOURCESAP_Gitsap_win64coreenginehandlersprmbdtcthandler.cpp (698): SniperCore.dll!TechnicalLayer::PrmbDtctHandler::processPortion()
    D:SOURCESAP_Gitsap_win64coreenginethreadedhandler2.cpp (109): SniperCore.dll!TotalCore::ThreadedHandler2::tryProcess()
    D:SOURCESAP_Gitsap_win64coreenginethreadedhandler2.cpp (66): SniperCore.dll!TotalCore::ThreadedHandler2::run()
    Qt5Cored.dll!QTextStream::realNumberPrecision() + 0x89E8E bytes
    kernel32.dll!BaseThreadInitThunk() + 0xD bytes
    ntdll.dll!RtlUserThreadStart() + 0x1D bytes
  Data:
    10 03 51 05    00 00 00 00    B0 B4 85 09    00 00 00 00     ..Q..... ........
    60 9D B9 08    00 00 00 00    D0 1B 24 06    00 00 00 00     `....... ..$.....
    30 B5 4F 11    00 00 00 00    CD CD CD CD    CD CD CD CD     0.O..... ........

В конце отчёта присутствует краткий итог в виде:

Visual Leak Detector detected 383 memory leaks (253257876 bytes).
Largest number used: 555564062 bytes.
Total allocations: 2432386151 bytes.
Visual Leak Detector is now exiting.

Или, если утечек нет,

No memory leaks detected.
Visual Leak Detector is now exiting.

Результаты

Мы провели сравнение скоростных характеристик работы приложения  в конфигурации debug в разных режимах работы: с VLD и без него. Как было сказано, в конфигурации release (пусть даже и с отладочной информацией) vld работать не может. В табл. 4 замеры времени выполнения для release приводятся исключительно для сравнения с debug. Тестирование проводилось на эталонном примере (см. табл. 1).

Конфигурация

Среднее время теста, с

Замедление работы, что привносит VLD, раз

Без VLD

С VLD

Debug

101

172

1,7

Release c отладочной информацией

10

Таблица 4. Время тестирования с учётом работы VLD

Что можно сказать по качеству найденных утечек? Действительно ли они являются утечками? Нет ли утечек, не замеченных VLD?

Конфигурация

Кол-во ошибок в эталоне: n

Найдено утечек

Пропущено ошибок из эталона: r

Точность: (n-r)/n

Избыточность: N/n

Всего: N

Верных

Ложных

Debug

7

185

185

0

0

1 (100%)

26 раз

Таблица 5. Результаты работы VLD

Да, VLD находит реальные утечки памяти. Пропусков утечек памяти мы не зафиксировали. При этом в итоговом отчете, который формирует VLD, бывает так, что на каждую строчку кода, где реально совершена ошибка, выводится очень большая куча строчек, которые «породились» этой ошибкой (как в примерах 2 и 3, см. выше). Из-за того, что эти утечки нельзя никак сортировать (или группировать), оказывается не очень приятно работать с такой большой плоской «простыней». Да и вообще, как поначалу можно доверять тому списку утечек, где фигурирует такая вот утечка:

connect(arrowKeyHandler, &ArrowKeyHandler::upPressed,
			[this] { selectNeighbourSignal(TopSide); }); 

Потом, разбираясь, оказывается, что деструктор класса всё же не вызывается, и утечка в функции connect действительно есть (см. пример 3). Но поначалу этому списку сложно поверить из-за большой избыточности.

Если ликвидировать все такие реальные ошибки, то VLD честно скажет, что утечек нет. И этот факт крайне важен для continuous integration.

Вывод

Visual Leak Detector – штука очень простая и очень полезная, способная найти все утечки (если прогнать все сценарии) и при этом не выдающая ложных срабатываний. Прогон сценариев в VLD довольно медленный, однако, он всё же быстрее, чем в Intel Inspector в конфигурации debug. Плоский, не очень дружественный и «простынообразный» вывод результатов способен запутать своей объемностью и дубликатами, однако со временем и к нему можно привыкнуть и даже использовать в continuous integration.

По стабильности работы больших нареканий нет. Однако всё же было несколько важных для нас проектов и сценариев работы в них, когда vld выдавал исключение и категорически отказывался работать. А жаль, если бы не это обстоятельство, то ценность этой утилиты была бы намного больше.

Снимки оперативной памяти в VS 2019

В IDE Visual Studio 2019 есть собственный встроенный компонент для диагностики проблем – Diagnostic Tools. В его составе есть механизм получения снимков памяти (snapshots). С его помощью можно находить утечки памяти как разницу (дельта) между снимками.  Само собой, чтобы дельта показывала именно утечки, надо делать снимки в определенные, далеко не случайные моменты.

Запуск

Запустите приложение в отладчике (в конфигурации debug или release c отладочной информацией). При запуске  должна по умолчанию появиться панель Diagnostic Tools. Выберите на этой панели вкладку Memory Usage, нажмите кнопку Heap Profiling и дальше делайте снимки кнопкой Take Snapshot.

Чтобы поймать утечку памяти, возникающую в каком-то сценарии работы с приложением, надо получить снимки непосредственно до и после выполнения сценария, а затем взять между ними дельту. Это будет работать только для тех сценариев, для которых точно известно, что они должны освобождать занятые ресурсы после своего завершения. К сожалению, далеко не для всех сценариев это так.

Перед созданием каждого снимка удобно либо временно приостанавливать работу приложения кнопкой Break All при отладке, либо ставить точку останова на нужное место в коде, и она сработает в нужный момент, после чего можно делать снимок.

Рис. 5. Работа со снимками памяти.

Рис. 5. Работа со снимками памяти.

Для просмотра результатов нажмём на прирост памяти между снимками (см. Рис. 5, где стрелочка). В появившейся вкладке в области редактора кода выберем ViewMode -> Stacks View (вместо Types View), и через некоторое время формирования отчёта увидим интерактивное дерево вызовов:

Рис. 6. Работа со снимками памяти, call-stack.

Рис. 6. Работа со снимками памяти, call-stack.

Результаты

Надо честно признать, что работать со снимками памяти при нахождении утечек очень сложно в больших приложениях на Qt: очень большой лог, глубоко завязанный на специфику Qt. Из него можно вычленять крупицы реальных ошибок, но это довольно трудоемко. Взяв наш эталонный сценарий тестирования (см. табл. 1), мы не смогли за пару часов найти ни одной утечки. Зато смогли добиться того, чтобы инструмент самопроизвольно отключался с ошибкой.

Вывод

Для поиска утечек памяти механизмом снимков можно пользоваться только, если никакой другой инструментарий попросту не запускается на вашем тесте (падает, зависает и т.д.). Даже если механизмом снимков удастся найти утечку, скорее всего, это потребует колоссального ручного анализа. Однако для поиска простых утечек (вроде выделения большого куска памяти в маленьком приложении) снимки памяти могут пригодиться.

PVS-Studio

Последняя из рассматриваемых в данной статье утилита, которая может помочь в нахождении утечек памяти, — PVS-Studio. Она делает статический анализ кода, не требующий запуска приложения. Анализ может запускаться и для одного выделенного проекта, и для всех проектов в solution. Также он может запускаться при каждом новом сохранении редактируемого файла исходного кода, «инкрементно», сразу указывая на сомнительные места в новом коде.

Установка

Проблем с установкой нет. В результате установки PVS-Studio оказывается встроенной в Visual Studio 2019, в меню «Extensions».

Запуск

Для запуска всего solution`а вызываем команду Extensions->PVS-Studio->Check. Результат проверки выдается во вкладке «PVS-Studio» и содержит список потенциальных ошибок в коде, распределённых по вкладкам с «критичностью» High, Medium и Low.

Этот объемный результирующий список будет содержать не только утечки памяти, а и все остальные ошибки в коде, что PVS-Studio посчитает ошибками. Чтобы оставить только то, что касается только утечек памяти, нужно фильтровать список по следующим кодам: V599, V680, V689, V701, V772, V773, V1005, V1023 (более подробно см. здесь).

Для фильтрации нужно зайти в настройки Visual Studio в меню Tools -> Options -> PVS-Studio и на вкладке «Detectable Errors (C++)» выставить нужные галочки, убрав остальные (при этом удобно сначала использовать команду «Hide All», а потом уже ставить галочки) – Рис. 8. Также нужно убрать галочки из других групп и вкладки «Detectable Errors (C#)» (выбирая «Hide All» или «Disabled»).

Рис. 8. Фильтрация списка найденных утилитой PVS-Studio ошибок.

Рис. 8. Фильтрация списка найденных утилитой PVS-Studio ошибок.

Чтобы показать все сообщения с выбранными кодами ошибок, нужно убедиться, что в окне PVS-Studio над сообщениями все кнопки High, Medium и Low включены.

Результаты

Итак, для поиска утечек памяти был запущен анализ на проекте, включающем около 1.5 млн строк кода и 2269 файлов кода. Анализ производился на Intel Core i7 4790K. Конфигурация кода (debug или release) значения не имеет, поскольку анализ статический (если более точно, разница есть из-за условной компиляции, но она непринципиальна).

Время анализа

Кол-во ошибок в эталоне: n

Найдено утечек

Пропущено ошибок из эталона: r

Точность: (n-r)/n

Всего

Верных

Ложных

30 мин

7

2

0

2

7

0 %

Таблица 6. Поиск утечек памяти утилитой PVS-Studio

Вывод

Для поиска утечек памяти этой утилитой можно пользоваться только, если под рукой нет чего-то более заточенного под утечки памяти (Intel Inspector, VLD). Она не способна находить все утечки, но выдает ложные срабатывания. Это не удивительно, поскольку утилита PVS-Studio никогда и не заявлялась как специализированный инструмент поиска утечек.

Сравнение работы инструментов для поиска утечек памяти

Подводя итоги, можно однозначно выделить в качестве лучших 2 инструмента для поиска утечек – Intel Inspector и Visual Leak Detector. На основании проведенного тестирования мы получаем следующую их сравнительную таблицу:  

Intel Inspector

VLD

Вид анализа

Динамический

Динамический

Стабильность работы

Средняя

Высокая

На любом ли примере (сценарии) может отработать

Нет

Нет

Удобство использования

Среднее

Низкое

Замедление debug

9.6 раз

1,7 раз

Замедление release с отладочной информацией

7 раз

Находит ли реальные утечки в debug

Да, все. Избыточность результатов – 18 раз.

Да, все. Избыточность результатов – 26 раз.

Находит ли реальные утечки в release с отладочной информацией

Да, все. Избыточность результатов – 27 раз.

Ложные срабатывания в debug

Да, немного

Нет

Ложные срабатывания в release с отладочной информацией

Да, немного

Можно ли использовать в Continuous Integration

Нет

Да

Таблица7. Сравнение Intel Inspector и VLD.

Место №1 в рейтинге целесообразно отдать VLD, поскольку он не выдает ложных срабатываний, более стабилен в работе и более подходит для использования в сценариях непрерывной интеграции.

It’s safe enough to say that every day hundreds of C++ developers get the message “detected memory leaks” in their Visual Studio. That is how you would view it:

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

Of course, all developers want to fix such an error as soon as possible. In this article, we will show how to activate CRT leaks detection, how to utilize CRT to catch leaks, and tell you about the better and simpler way to catch leaks in C++ applications, no matter where they come from: CRT, MFC or usual heap allocation functions.

How to enable the CRT memory leak detection?

You never got the message above by default. To activate leak detection, you should add some headers and defines, and finally call _CrtDumpMemoryLeaks:

#ifdef _DEBUG
#define _CRTDBG_MAP_ALLOC
#define _CRTDBG_MAP_ALLOC_NEW
#include <crtdbg.h>
#include <assert.h>
#endif

int main()
{
    auto p = new int[0x100];

    std::cout << "Hello World!n";

    _CrtDumpMemoryLeaks();
}

After that rebuild the project and run. You will see the famous “detected memory leaks” in the Output Windows.

How does the CRT leak detection work?

This message tells a developer that CRT (C Runtime Library) or MFC has found a memory leak. In other words, somewhere a memory block was allocated but the developer has forgotten  to free it later.

Internally, CRT stores a list of allocated memory blocks. When a function _CrtDumpMemoryLeaks is called, it outputs information about live memory blocks in a human-readable format to the Output Window.

Tip
If you want to dump the information about memory leaks to a file, instruct CRT to use a file by calling _CrtSetReportMode with _CRTDBG_MODE_FILE, and then call _CrtSetReportFile.

Unfortunately, often CRT shows very little information about leaks: only size, content, and allocation number. As you see, it doesn’t include the most important information: a call stack. Moreover, you need to know where the memory was allocated to fix it.

Detected memory leaks!

When a CRT allocates a memory block, it increments a counter that saves it to the header of, that has just been allocated. If you are lucky, the allocation number can help you catch a leak. In the case of a simple program, if its flow is the same on each run, each allocation has the same number.

To try to catch the leak, set _crtBreakAlloc to allocation number of the leaks, and run the debugging once again. CRT breaks when an allocation number reaches the value:

int main()
{
    _crtBreakAlloc = 217;

    auto p = new int[0x100];

    std::cout << "Hello World!n";

    _CrtDumpMemoryLeaks();
}

Below is how it works:

Allocation number helps find memory leaks

However, complex applications allocate memory in a different order. This makes an allocation number is useless to leaks debugging as the leaked memory block will get different allocation numbers on each run.

False positives

Another issue is that _CrtDumpMemoryLeaks is called before destructors of global objects. That is why it outputs memory block information, that will either freed later or not. _CrtDumpMemoryLeaks can’t help you if global objects allocate and free memory.

Visual Studio Extension to catch leaks

Fortunately, there is a better way to solve the problem. Deleaker is an extension for Visual Studio that assists a developer to debug leaks.

After installing, you will find a new menu item Deleaker in Visual Studio. Start debugging as usually. Deleaker immediately comes and suggests to monitor all allocations made by a process. Deleaker saves call stack and other information of each memory block.

While debugging, you can switch to the Deleaker window and take a snapshot to explore currently allocated objects. When a process quits, Deleaker automatically prepares a final snapshot that contains possible leaks. Thanks to Deleaker filters, the developer focuses on those leaks he is responsible for.

Return to the sample, and comment lines where you set _crtBreakAlloc and call of _CrtDumpMemoryLeaks. Rebuild the project and start debugging once again:

Deleaker detects memory leaks

Deleaker has found a leak that was introduced, it correctly detected the exact line and the source file where the memory was allocated. You can review the call stack. Just double-click the allocation to navigate to the source code. The source file is opened in Visual Studio and you are ready to start working with the code.

Wrapping up

Some developers names Deleaker “a Valgrind for Visual Studio”. Indeed, you may consider Deleaker as an alternative for Valgrind that works on Windows and integrates with all major development environments including Visual Studio, C++ Builder, Qt Creator, and CLion.

CRT is not helpful in most cases as very often it doesn’t show call stacks, and its reports contain memory that can be freed later.

Instead of raw console output (both CRT and Valgrind doesn’t have any UI), Deleaker has a nice UI that helps quickly locate and fix memory issues, explore memory usage in real-time.

Deleaker snapshots make memory leak detection in Visual Studio comfortable: a developer just compare consequent snapshots to find spots that commit memory actively.

Also, while CRT is designed for memory allocated by C/C++ functions only (malloc, calloc, operator new and new[]), Deleaker detects all kinds of leaks such as GDI leaks, handles leaks and leaks made by COM.


In my wxWidgets application, while running in debug mode i got this message in Output of Visual Studio 2010. The application ran fine, and i only saw this after closing it.

Detected memory leaks!

Dumping objects ->

{9554} normal block at 0x003CDCC0, 44 bytes long.
Data: < e n d > 20 C1 65 01 01 00 00 00 6E 00 00 00 9C CE 64 01

{9553} normal block at 0x003CDB58, 8 bytes long.

Data: < D e < > 44 BD 65 01 C0 DC 3C 00
{9552} normal block at 0x003CDC50, 48 bytes long.

Data: < e > A0 95 65 01 01 00 00 00 19 00 00 00 19 00 00 00

Object dump complete.

In my program i am not explicitly allocating memory, however the wxWidgets framework is. I have got such a message for first time, and don’t know the exact cause of it.

How can i get rid of this memory leak?

asked Dec 17, 2011 at 10:25

Vinayak Garg's user avatar

Vinayak GargVinayak Garg

6,51810 gold badges53 silver badges80 bronze badges

3

You just have to add the following lines at the beginning of your main function. Adding this flag, Visual Studio will break at the line that is creating the memory leak.

    _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
    _CrtSetBreakAlloc(9554);
    _CrtSetBreakAlloc(9553);
    _CrtSetBreakAlloc(9552);

Make sure you have the correct object normal block address because they might change and ensure you are compiling on _DEBUG.

See also: _CrtSetDbgFlag and _CrtSetBreakAlloc.

Nathan's user avatar

answered Dec 17, 2011 at 12:23

mihaipopescu's user avatar

4

  1. Never just ‘assume’ that your code is memory leak proof. Unless you are one of the programming demi-gods, no one is immune from possibly writing memory leaks.

  2. You could use a tool like bounds checker (From Microfocus) to help identify the memory leak because it will give you a callstack. The memory leak report you got from the debug CRT just tells you memory leaked at a particular address. A product like bounds checker will give you a callstack for that memory leak, along with lots of other goodies. There are other memory leak tools out there in the market, but I won’t attempt to list them here.

  3. If you are sure the memory leak is due to ‘wxWidgets’, then perhaps you should inform the writers of that library and perhaps they will fix it (With suitable repro steps).

answered Dec 17, 2011 at 11:31

C.J.'s user avatar

C.J.C.J.

15.6k9 gold badges61 silver badges77 bronze badges

1

Maybe some kinds of static instances are still allocated by the framework. Try to solve it with profiler like «devpartner».

answered Dec 17, 2011 at 11:03

AlexTheo's user avatar

AlexTheoAlexTheo

3,9941 gold badge21 silver badges35 bronze badges

3

This wiki suggests adding the following to every source file you have, after all other header include’s:

#ifdef __WXMSW__
    #include <wx/msw/msvcrt.h>      // redefines the new() operator 
#endif

This will result in leaks being reported when your program ends.

More specifically, make sure you call ->Destroy() on all objects you create using new (except maybe your top window).

answered Dec 17, 2011 at 11:52

user1071136's user avatar

user1071136user1071136

15.6k4 gold badges42 silver badges61 bronze badges

If the location of the leak reported by vs is same every time you could set a databreakpoint to see when this memory is being changed and hopefully figure out who is allocating this memory

answered Dec 17, 2011 at 13:00

ans's user avatar

#статьи

  • 29 июн 2020

  • 15

Разбираемся в трудно уловимых уязвимостях приложений, чтобы всё работало гладко и без тормозов.

 vlada_maestro / shutterstock

Евгений Кучерявый

Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.

Утечка памяти (англ. memory leak) — это неконтролируемое уменьшение свободной оперативной или виртуальной памяти компьютера. Причиной утечек становятся ошибки в программном коде.

В этой статье мы поймём, как появляются утечки и как их устранять.

Любые программы используют в своей работе память, чтобы хранить какие-то данные. В C++ и многих других языках память динамическая. Это значит, что операционная система при запуске программы резервирует какое-то количество ячеек в ОЗУ, а потом выделяет новые, если они нужны.

Создали переменную для числа (int)? Вот тебе 16 бит (4 байта). Нужен массив из ста элементов для больших чисел (long)? Вот тебе ещё 3200 бит (800 байт).

Когда программисту уже не нужен какой-то массив или объект, он должен сказать системе, что его можно удалить с помощью оператора delete[] и освободить память.

//Создаём пустой указатель
double *pointer;
 
//Создаём массив и получаем ссылку на него
pointer = new double[15];
 
//Указываем тестовое значение для первой ячейки
pointer[0] = 5;
 
//Выведет адрес ячейки и 5, так как по адресу находится первый элемент массива
cout << "Address: " << pointer << " | Value: " << *pointer << "n";
 
delete [] pointer;
cout << "Deletedn";
 
//Адрес остался прежним, но значение будет 0, так как память была очищена
cout << "Address: " << pointer << " | Value: " << *pointer << "n";

Вот результат:

Однако иногда случаются ошибки, которые приводят к утечкам памяти. Вот одна из них:

На примере в цикле десять раз создаётся новый массив, а его адрес записывается в указатель. Адреса старых массивов при этом удаляются. Поэтому дальше оператор delete[] удаляет только последний созданный массив. Остальные останутся в памяти до тех пор, пока не будет закрыта программа.

Когда приложение съест всю доступную память, сработает защита ОС и ваша программа аварийно закроется. Однако у утечек могут быть и более опасные последствия.

Например, приложение может работать с каким-нибудь файлом непосредственно перед закрытием. В этом случае файл будет повреждён. Последствия возможны самые разные: от нервного срыва пользователя, если это была презентация, над которой он работал несколько дней, до поломки системы, если это был очень важный файл.

В отдельных случаях утечка памяти одного приложения может привести к последствиям для других работающих приложений. Например, если ваш код изменил или занял память, используемую другой программой.

Может показаться, что раз это «утечка», то что-то случится с вашими данными. На самом деле утекает именно свободная память, а не её содержимое.

Если у вас есть доступ к исходникам, то изучите код, чтобы определить, нет ли там утечек. Вручную делать это бессмысленно, особенно если проект большой, поэтому обратимся к отладчику использования памяти (англ. memory debugger).

Если вы пользуетесь IDE вроде Visual Studio, то там должен быть встроенный отладчик. Есть и сторонние инструменты вроде GDB или LLDB. Отладчик покажет, какие данные хранит программа и к каким ячейкам имеет доступ.

И будьте осторожнее с указателями:

Указатель на указатель на указатель на переменную X

Если доступа к коду у вас нет, но нужна библиотека, в которой есть утечка, то её стоит вынести в отдельную программу (B). Ваша основная программа (A) запустит программу B, чтобы вызвать какую-то функцию. После этого программа B будет закрываться, чтобы освободить всю использованную (в том числе и утёкшую) память.

Если же функционал из библиотеки нужен постоянно, то программу B можно оставить работать, но перезапускать её с определённой периодичностью — до того как она сожрёт слишком большой объём памяти.

Конечно, лучше избегать таких библиотек, но мы живём в неидеальном мире — иногда альтернатив попросту нет.

В более высокоуровневых языках вроде C# или Java существуют сборщики мусора (англ. garbage collector). Это специальный процесс, который сканирует память и удаляет те ячейки, которые уже не нужны приложению.

В C++ сборщика мусора нет, поэтому приходится следить за памятью самостоятельно. Это требует более высокой квалификации разработчика, но позволяет увеличить скорость работы приложений.

Впрочем, иногда от утечек не спасает и сборщик мусора.

Такие проблемы всегда сложно искать в коде, поэтому они встречаются так часто и поэтому многие приложения виснут и вылетают.

Научитесь: Профессия Разработчик на C++ с нуля
Узнать больше

  • Ошибка memory could not be read
  • Ошибка microsoft edge не работает
  • Ошибка memory allocation for 1398096 bytes failed
  • Ошибка microsoft c exception r2 online windows 10
  • Ошибка memori management виндовс 10