Утечка памяти или memory leak – это ошибка в исходном коде, при которой выделенная под переменную, массив, объект класса и т. д. динамическая память не освобождается и впоследствии теряется, а данные так и остаются в оперативной памяти до момента закрытия программы.
Подобные ошибки могут привести к сильной нагрузке на компьютер и значительно уменьшить его производительность во время работы приложения. Кроме того, если программа начнёт бесконечно потреблять память, то операционная система в какой-то момент аварийно завершит работу проблемного приложения и всех связанных с ним процессов.
Подобные ошибки в некоторых случаях можно рассматривать в качестве потенциальных уязвимостей для DoS-атак уровня приложения. Суть этой атаки — исчерпать лимиты ресурсов системы.
При запуске любого приложения часть оперативной памяти выделяется для созданного процесса и операций, совершаемых им. Для каждого выполняемого потока создаётся структура данных, называемая стеком. Сюда программа поочерёдно размещает информацию о переменных, вызове функций и других процессах, завершение которых также поочерёдно самостоятельно высвобождает эти данные, только в обратном порядке.
Однако размер стека довольно ограничен, и программа может запросить дополнительную память, которая ещё не была распределена операционной системой. Подобная динамическая память называется управляемой кучей (heap).
Чтобы работать с данными из кучи, в программе используются указатели — это специальные переменные, которые содержат адрес блока оперативной памяти. Но, в отличие от стека, память, выделенная в куче, автоматически не высвобождается после того, как завершится обработка данных. Поэтому память следует вернуть самостоятельно. Более того, если будет потерян указатель, ссылающийся на выделенный фрагмент памяти, его освобождение станет невозможным.
В C++ динамическая память выделяется при помощи оператора new. В C функциями: malloc, realloc, calloc, strdup и так далее. Освободить блок памяти можно при помощи оператора delete (C++) или функции free (C).
В больших проектах указатели постоянно передаются между функциями, структурами, классами, и, как следствие, довольно легко запутаться и не уследить за всеми аспектами выделения и освобождения памяти. Эта сложность и провоцирует возникновение ошибок утечек памяти.
Рассмотрим пример утечки памяти, найденной с помощью анализатора PVS-Studio в проекте Augeas:
static void xfm_error(struct tree *xfm, const char *msg) {
char *v = msg ? strdup(msg) : NULL;
char *l = strdup("error");
if (l == NULL || v == NULL)
return;
tree_append(xfm, l, v);
}
Несмотря на то, что функция маленькая, она может привести сразу к трём сценариям утечки памяти. Два сценария маловероятны, а третий вполне реальный.
Первые два сценария. Один из вызовов функций strdup вернёт NULL. В таком случае функция досрочно завершит работу, и будет потерян указатель, который вернул другой вызов strdup. Это хоть и маловероятные сценарии, но вполне возможные.
Третий более вероятный сценарий. Функция xfm_error рассчитана на то, что значение аргумента msg может быть равно NULL. В этом случае функция ничего не делает и досрочно завершает свою работу. Однако при этом будет потеряна память, выделенная вызовом strdup("error").
Чтобы избежать всех этих ошибок, код можно переписать следующим образом:
static void xfm_error(struct tree *xfm, const char *msg) {
if (msg == NULL)
return;
char *v = strdup(msg);
if (v == NULL)
return;
char *l = strdup("error");
if (l == NULL) {
free(v);
}
tree_append(xfm, l, v);
}
К сожалению, теперь кода стало больше и его нельзя назвать изящным и устойчивым к ошибкам при дальнейших изменениях. Именно поэтому управление памятью в стиле языка C так часто приводит к ошибкам.
Примечание. См. также релевантную тему "Четыре причины проверять, что вернула функция malloc".
Если не очищать данные в динамической памяти – оперативная память может исчерпаться, и по достижению доступных лимитов операционная система попросту завершит процесс нашей программы самостоятельно, не дав сохранить проделанную работу. Когда подобное аварийное закрытие не настроено в ОС (например, в некоторых дистрибутивах Linux, где для этого дополнительно требуется скачивать специальную утилиту), компьютер может зависнуть на длительное время или сильно замедлиться. В итоге его использование станет невозможным, а для корректного закрытия программы потребуется долго ждать её отклика. Иногда же устройство может замедлиться так сильно, что ничего не останется, кроме как перезагрузить его.
Куда хуже, если программа функционирует в связке с другими приложениями. В таком случае завершиться могут все связанные с ней процессы, в том числе применяемые пользователем параллельно для собственных нужд.
Это приведёт к потере информации, не только размещаемой в программе, но и той, с которой пользователь работал отдельно и не сохранял. Например, когда программа фоном использует редакторы и прочие сторонние инструменты.
Такие языки, как C и C++, предполагают ручное управление памятью, в отличие от C# и Java, где очисткой управляемой кучи занимается функция сборщика мусора (Garbage Collector). Однако в C++ существуют определённые механизмы защиты от утечек памяти. Они реализуются с помощью "умных" указателей, таких как std::unique_ptr, std::shared_ptr и так далее.
Когда требуется убедиться, что в программе нет никаких утечек, для отлова ошибок используют инструменты динамического анализа. Например, в Visual Studio можно воспользоваться библиотекой Debug CRT. В точке завершения программы вызывается функция _CrtDumpMemoryLeaks, и, как только отработает отладчик, в окне "Вывод" блока "Отладка" можно посмотреть отчёт об утечках. Эта библиотека проверяет любое выделение памяти через new или функцию malloc.
Можно воспользоваться инструментами от сторонних разработчиков, которые к тому же неплохо интегрируются со средой Visual Studio. Например, Visual Leak Detector – всё в том же окне вывода программа сообщает обо всех файлах и строчках кода, в которых произошли утечки.
На Unix-подобных операционных системах (Linux, macOS) можно воспользоваться инструментом Leak Sanitizer. Начиная с 2019 года, он также доступен и в Windows. Этот санитайзер интегрирован в более продвинутый Address Sanitizer, однако его также можно подключить автономно. Для того чтобы воспользоваться санитайзером в своей программе, необходимо скомпилировать её с флагами -fsanitize=leak в автономном режиме или -fsanitize=address в комбинации с Address Sanitizer. Столкнувшись с утечкой памяти, инструмент сохранит информацию о ней в стандартный поток вывода ошибок.
Статический анализатор PVS-Studio также может выявлять многие ошибки утечек памяти. В отличие от динамических средств диагностики кода, он делает это не так точно. Зато он проверяет весь код программы и может найти утечки даже в редко используемом коде, который по тем или иным причинам не подвергается динамическому тестированию.
В случаях, когда утечку не удаётся исправить, например она обнаружена в сторонней библиотеке, к которой не имеется доступа, следует написать баг-репорт разработчику, попробовать обновить версию или поискать сторонние аналоги. Если же без подключения такой библиотеки никак не обойтись – можно вынести выполнение связанных с ней функций в отдельное приложение, которое будет работать только в моменты необходимости, при этом, конечно, создавая утечки, но контролируемые с помощью команд запуска и завершения процесса.
Дополнительные ссылки:
0