Обнаружение и локализация утечек памяти

Автор: Эдвард Райт
Перевод: Александр Шаргин
Источник: MSDN

Версия текста: 1.0.2

Введение

Динамическое распределение и освобождение памяти - одна из мощнейших возможностей языка C/C++. Но, как заметил китайский философ Сан Тзю, великая сила может обернуться великой слабостью. Это особенно верно в отношении приложений, написанных на C/C++, где ошибки при работе с памятью относятся к числу самых распространённых. Из них наиболее тонкими и трудными для обнаружения являются утечки памяти, которые возникают, когда память, выделенная программой, никогда не освобождается. Небольшие утечки памяти, возникающие один раз, могут никак не сказаться на работе программы. Однако значительные утечки, накапливающиеся со временем, могут привести к различным неприятным последствиям, начиная с невысокой производительности, и заканчивая полным отказом программы вследствие нехватки памяти. Что ещё хуже, программа, допускающая утечки памяти, может привести к краху совсем другую программу, и пользователь никогда не узнает, в чём состоит истинная причина отказа. Кроме всего перечисленного, даже самая незначительная утечка может свидетельствовать о наличии других серьёзных проблем в вашей программе.

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

Активизация режима обнаружения утечек памяти

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

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

Директивы #include должны идти в указанном порядке. В противном случае функции, которыми мы воспользуемся, будут работать неправильно. Включая файл crtdbg.h, мы перенаправляем вызовы функций malloc и free на их отладочные версии _malloc_dbg и _free_dbg, которые отслеживают все операции по распределению и освобождению памяти. Перенаправление происходит только в отладочной версии программы (то есть, когда определён символ _DEBUG). В окончательной версии будут использоваться обычные функции malloc и free.

Директива #define непосредственно отображает базовые функции для работы с "кучей" на их отладочные версии. Вставлять её в программу не обязательно, но без неё отчёт об утечках памяти будет менее подробным.

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

_CrtDumpMemoryLeaks();

Когда программа выполняется под управлением отладчика, _CrtDumpMemoryLeaks отображает информацию об утечках памяти на вкладке Debug окна Output. Эта информация выглядит примерно так:

Detected memory leaks!
Dumping objects ->
C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18} normal block at 0x00780E80, 64 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
Object dump complete.

Если бы вы не включили в программу директиву #define _CRTDBG_MAP_ALLOC, отчёт выглядел бы так:

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

Как видите, _CrtDumpMemoryLeaks выдаёт гораздо более полезную информацию, когда символ _CRTDBG_MAP_ALLOC определён. Без него вам выдаются следующие данные:

Если _CRTDBG_MAP_ALLOC определён, вам дополнительно показывается имя файла, в котором произошло распределение памяти. После имени файла в скобках содержится номер строки (20 в нашем примере). Если сделать двойной щелчок на строчке, содержащей имя файла и номер строки в нём:

C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18} normal block at 0x00780E80, 64 bytes long.

то курсор переместится на строку в файле с исходным кодом программы (в нашем примере строку 20 в файле leaktest.cpp), где произошло распределение памяти. Аналогичного эффекта можно добиться, выделив строчку и нажав F4.

Использование _CrtSetDbgFlag

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

_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

После этого функция _CrtDumpMemoryLeaks будет автоматически вызываться перед завершением работы вашей программы. Вы должны задать оба флага (_CRTDBG_ALLOC_MEM_DF и _CRTDBG_LEAK_CHECK_DF), как это показано выше.

Типы блоков памяти

Как я уже говорил, в информацию об утечках памяти входит тип блока, который может быть нормальным, клиентским или CRT-блоком. На практике вам скорее всего будут встречаться блоки первых двух типов.

Существует ещё два типа блоков, которые никогда не попадают в отчёт об утечках памяти:

Установка режима сообщений CRT

По умолчанию _CrtDumpMemoryLeaks выводит свой отчёт об утечках памяти на вкладку Debug окна Output, как это уже описывалось выше. Однако вы можете перенаправить эту информацию в другое место посредством _CrtSetReportMode. И наоборот: если вы используете библиотеку, которая куда-то перенаправляет вывод, вы можете вернуть его в окно Output, используя следующий вызов:

_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG );

За дополнительной информацией об использовании функции _CrtSetReportMode обратитесь к разделу "_CrtSetReportMode" в документации на Visual C++ (Visual C++ Programmer’s Guide, Run-Time Library Reference, Debug Function Reference).

Установка точки останова на нужном распределении памяти

Имя файла и номер строки в отчёте об утечках памяти указывает вам, где именно в программе был выделен соответствующий блок. Тем не менее, иногда этой информации оказывается недостаточно, чтобы выявить проблему. В процессе выполнения программы одно и то же распределение может происходить множество раз, но приводить к утечкам лишь в некоторых случаях. Поэтому нужно определить не только точку, в которой потерянный блок был выделен, но и при каких условиях произошла утечка. Решить задачу поможет порядковый номер распределения, то самое число, которое выводится в фигурных скобках вслед за именем файла и номером строки в нём (если, конечно, вы включили их отображение). Например, в приведённом ниже отчёте "18" - это порядковый номер распределения. Он означает, что потерянный блок был выделен восемнадцатым по счёту.

Detected memory leaks!
Dumping objects ->
C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18} normal block at 0x00780E80, 64 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
Object dump complete.

CRT учитывает каждый блок, выделенный вашей программой в процессе работы, в том числе и блоки, распределяемые самой CRT и другими библиотеками, такими как MFC. Поэтому блок с порядковым номером n - это n-й блок, распределённый вашей программой, но совсем не обязательно n-й блок, распределённый вашим собственным кодом. Как правило, это не так.

Вы можете использовать порядковый номер распределения, чтобы поставить точку останова в точности на то место, где выделяется соответствующий блок. Для этого поставьте обычную точку останова где-нибудь в начале вашей программы. Когда выполнение прервётся в этой точке, вы сможете поставить точку останова на распределение памяти из диалога QuickWatch или из окна Watch. Например, в окне Watch наберите в колонке "Name" имя:

_crtBreakAlloc

Если вы используете многопоточную версию CRT, размещённую в динамически подключаемой библиотеке (опция /MD), вы должны задать полный контекстный оператор, как показано ниже:

{,,msvcrtd.dll}_crtBreakAlloc

Теперь нажмите RETURN. Отладчик рассчитает значение переменной и разместит его в колонке "Value". Если перед этим вы не ставили точек останова на распределения памяти, значение будет равно -1. Замените его порядковым номером распределения, на котором вы хотите прервать выполнение программы, например, номером 18, чтобы прерваться на распределении, показанном выше.

После того, как вы поставили точку останова на интересующее вас распределение, можно продолжать отладку. Однако будьте осторожны и выполняйте программу в тех же условиях, что и в предыдущий раз, чтобы порядок распределений памяти не изменился. Когда программа прервётся на распределении нужного блока, вы сможете проанализировать содержимое окна Call Stack и любую другую информацию, предоставляемую отладчиком, чтобы определить условия, при которых происходит утечка. Если нужно, вы можете также продолжить выполнение программы, чтобы проследить, что произойдёт с объектом дальше и почему он не освобождается, как положено (тут вам могут пригодиться точки останова по данным).

Хотя в большинстве случаев удобнее ставить точки останова на распределения памяти прямо в отладчике, никто не мешает вам сделать это прямо в коде программы. Для этого добавьте в программу строку следующего вида:

_crtBreakAlloc = 18;

Альтернативный вариант заключается в использовании функции _CrtSetBreakAlloc, которая делает то же самое:

_CrtSetBreakAlloc(18);

Сравнение состояний памяти

Другой способ обнаружения утечек памяти подразумевает использование "моментальных снимков" (snapshots) памяти в ключевых точках вашей программы. Специально для хранения таких "снимков" в CRT предусмотрена структура _CrtMemState:

_CrtMemState s1, s2, s3;

Чтобы сделать "снимок" памяти в некоторый момент времени, передайте указатель на структуру _CrtMemState функции _CrtMemCheckpoint. Эта функция записывает в структуру информацию о текущем состоянии памяти:

_CrtMemCheckpoint( &s1 );

Вы можете в любой момент вывести содержимое структуры _CrtMemState, передав указатель на неё функции _CrtMemDumpStatistics:

_CrtMemDumpStatistics( &s1 );

Эта функция выдаёт отчёт обо всех выделенных блоках, который выглядит примерно так:

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

Чтобы определить, не было ли утечек памяти на каком-то участке вашей программы, вы можете сделать "снимки" памяти до и после этого участка, а затем вызвать _CrtMemDifference, чтобы сравнить два состояния:

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

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

Как и подразумевает её название, функция _CrtMemDifference сравнивает два состояния памяти (первые два параметра) и записывает разницу в третий параметр. Таким образом, размещение _CrtMemCheckpoint в начале и в конце вашей программы с последующим использованием _CrtMemDifference для сравнения состояний даёт вам ещё один метод контроля утечек памяти. Если утечка обнаружена, вы можете разбивать программу на части и искать, где она произошла, используя стратегию двоичного поиска.

Источники дополнительной информации


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.