Содержание статьи
В «Диспетчере задач» очень часто можно увидеть, что некоторые программы занимают совершенно неприличное количество памяти. Особенно это свойственно интернет-браузерам. Рано или поздно перед каждым разработчиком встает задача отлова утечек памяти. Сегодня мы узнаем, как это сделать в языке C++ на MSVC.
Искать утечки памяти мы будем в ОС Windows, а для сборки кода использовать компилятор от Microsoft. Существует множество способов избежать мемори ликов, но основное правило этой борьбы можно сформулировать как «класть на место все, что взяли». К сожалению, в «боевом кодинге» такое не всегда возможно. Банальный человеческий фактор или сработавший exception запросто может отменить выполнение оператора delete. В этой статье мы не будем рассматривать, что нужно делать, а что не стоит, чтобы память не утекала. Мы будем действовать в контексте уже имеющейся проблемы: утечка есть и нам надо ее перекрыть.
Для решения этой задачки многие программисты используют сторонние библиотеки (а самые крутые пишут собственные менеджеры памяти), но мы начнем с чего-нибудь попроще — например, воспользуемся средствами Debug CRT.
Debug CRT
Для использования Debug CRT надо подключить соответствующий хидер и включить использование Debug Heap Alloc Map. Делается это всего несколькими строками кода:
Подключение Debug CRT
#ifdef _DEBUG
#include <crtdbg.h>
#define _CRTDBG_MAP_ALLOC
#endif
После этих действий при выделении памяти через new и malloc() данные будут оборачиваться в специальную структуру _CrtMemBlockHeader. С помощью этой обертки мы сможем узнать имя файла и строку, в которой резервировалась ликнутая память, ее объем и сами данные. Все записи объединены в двусвязный список, поэтому по нему можно легко пробежаться и найти проблемные участки.
Структура _CrtMemBlockHeader
typedef struct _CrtMemBlockHeader
{
struct _CrtMemBlockHeader * pBlockHeaderNext;
struct _CrtMemBlockHeader * pBlockHeaderPrev;
char* szFileName;
int nLine;
size_t nDataSize;
int nBlackUse;
long lRequest;
unsigned char gap[nNoMansLandSize];
unsigned char data[nDataSize];
unsigned char anotherGap[nNoMansLandSize];
} _CrtMemBlockHeader;
Чтобы пройтись по этому списку, нужно воспользоваться функцией _CrtDumpMemoryLeaks(). Она не принимает никаких параметров, а просто выводит список утекших блоков памяти. Но, к сожалению, она ничего не говорит нам о файле и строке, в которых выделялась память. Результат работы этой функции выглядит примерно так:
Вывод _CrtDumpMemoryLeaks()
Detected memory leaks!
Dumping objects ->
{163} normal block at 0x00128788, 4 bytes long.
Data: < > 00 00 00 00
{162} normal block at 0x00128748, 4 bytes long.
Data: < > 00 00 00 00
Object dump complete.
Вот оно как: в Microsoft Visual C++ 6.0 в файле crtdbg.h имело место переопределение функции new, которое должно было точно показать файл и строку, в котором происходило выделение памяти. Но оно не давало желаемого результата — FILE:LINE всегда разворачивались в crtdbg.h file line 512. В следующих версиях Microsoft вообще убрали эту фичу, и весь груз ответственности лег на программистов. Сделать нормальный вывод можно с помощью следующего переопределения:
Переопределение new
#define new new( _NORMAL_BLOCK, __FILE__, __LINE__)
Эту строчку желательно вынести в какой-нибудь общий заголовочный файл и подключать его после crtdbg.h. Теперь перед нами стоит задача записать все это в какой-нибудь лог или хотя бы выводить в консоль. Для перенаправления вывода нам потребуются две функции: _CrtSetReportMode и _CrtSetReportFile. Вторым параметром _CrtSetReportFile может быть хендл нашего лог-файла или флаг вывода в stdout.
Перенаправление вывода
_CrtSetReportMode( _CRT_WARN, _CRTDBG_MODE_FILE );
// выводим все в stdout
_CrtSetReportFile( _CRT_WARN, _CRTDBG_FILE_STDOUT );
У этого метода есть еще одна проблемка — он выводит информацию о памяти, которая не утекла, а просто не успела вернуться. Это, например, может быть какая-нибудь глобальная переменная или объект. Нам нужно как-то удалить эти куски памяти из вывода _CrtDumpMemoryLeaks(). Делается это следующим образом:
Ограничение зоны действия _CrtDumpMemoryLeaks()
int _tmain(int argc, _TCHAR* argv[])
{
_CrtMemState _ms;
_CrtMemCheckpoint(&_ms);
// some logic goes here...
_CrtMemDumpAllObjectsSince(&_ms);
return 0;
}
Мы записываем начальное состояние памяти в специальную структуру с помощью функции _CrtMemCheckpoint(), а в конце, используя _CrtMemDumpAllObjectsSince(), выводим все, что утекло после того, как мы сделали слепок памяти.
Вот так вот, с помощью нехитрых функций Debug CRT, мы можем достаточно эффективно бороться с мемори ликами в нашей программе. Конечно, это не заменит серьезных библиотек по отлову утечек, но вполне подойдет для небольших проектов.
Visual Leak Detector
Visual Leak Detector — это уже сторонняя библиотека, но по сути она является надстройкой над Debug CRT, которую мы рассмотрели чуть раньше. Пользоваться ей достаточно просто — надо всего лишь включить заголовочный файл vld.h в любой файл проекта. Но с двумя оговорками.
Во-первых, если у нас в проекте есть несколько бинарных модулей (DLL или EXE), то include для vld.h надо делать как минимум в одном исходном файле для каждого модуля. То есть, если у нас после компиляции на выходе получается module_1.dll и module_2.dll, то нам нужно сделать #include <vld.h> как минимум в module_1.h и в module_2.h.
Во-вторых, включение заголовочного файла Visual Leak Detector должно происходить после включения прекомпилед хидера. То есть, после stdafx.h и других подобных файлов. После выполнения этих условий достаточно запустить программу в дебаг-сборке, и библиотека сразу начнет работать.
Visual Leak Detector можно настроить под свои нужды. Конфиг хранится в файле vld.ini, который, в свою очередь, лежит в директории с установленным VLD. Файл с настройками можно скопировать в папку с проектом, и тогда библиотека будет использовать эту копию с индивидуальными настройками для каждого проекта.
Параметров для тюнинга Visual Leak Detector предостаточно. Например, можно настроить тот же вывод в файл или в дебаг консоль студии. Делается это ключом ReportTo. По умолчанию там стоит «debugger», но можно заменить это значение на «file» или «both». Надеюсь, их смысл пояснять не нужно.
Если мы включим вывод в файл, то надо указать путь к этому файлу с помощью параметра ReportFile. Также можно выбрать кодировку файла с помощью ReportEncoding: unicode или ASCII.
Еще есть интересная опция поиска ликов библиотеки в самой себе (SelfTest). Да, бывает и такое. Если постараться, то можно получить в output что-то вроде этого:
ERROR: Visual Leak Detector: Detected a memory leak internal to Visual Leak Detector.
Еще VLD позволяет ограничить размер дампа памяти, выводимого в лог, или вообще подавить этот вывод, настроить глубину и метод проходки по стеку и так далее. Но это уже специфичные вещи, которые некоторым могут пригодиться, а некоторым и нет. В общем и целом Visual Leak Detector прост, удобен и не требует много кода для включения режима поиска утечек.
Valgrind
Все, что мы рассмотрели выше, было актуально для Windows и MS Visual Studio. Но есть и другие ОС. Valgrind как раз для них. Он работает в Linux и Mac OS X и используется не только для отлова мемори ликов, но и для отладки памяти и профилирования (сбор характеристик работы программы). По сути, Valgrind является виртуальной машиной, использующей методы JIT-компиляции. Ее усилиями программа не выполняется непосредственно на процессоре компьютера, а транслируется в так называемое «промежуточное представление». С этим представлением и работает Valgrind. Точнее, работают его инструменты, но об этом чуть позже.
После обработки промежуточного представления Valgrind перегоняет все обратно в машинный код. Такие действия значительно (в 4-5 раз) замедляют выполнение программы. Но это вполне обоснованная плата за контроль расхода памяти. Как я говорил выше, Valgrind предоставляет несколько инструментов. Самый популярный из них — Memcheck. Он заменяет стандартное выделение памяти языка C собственной реализацией. Memcheck обнаруживает попытки использования неинициализированной памяти, чтение/запись после её освобождения и с конца выделенного блока, а также утечки памяти.
Есть и другие инструменты, например Addrcheck — более легкая, но менее функциональная версия Memcheck. Инструменты Helgrind и DRD используются для поиска ошибок в многопоточном коде. Иначе говоря, Valgrind гораздо более мощная штука, чем просто библиотека по поиску мемори ликов.
Заключение
Утечки памяти — одна из самых распространенных проблем в программировании. Даже самые опытные из нас могут заиметь парочку ликов. Инструментов для их отлова великое множество, и эти несколько страниц должны помочь тебе начать борьбу с memory leaks.