Valgrind является многоцелевым инструментом профилирования кода и отладки памяти для Linux на x86, и, начиная с версии 3 и AMD64. Это позволяет запускать программу в собственной среде Valgrind, что контролирует использование памяти, например, вызовы malloc
и free
(или new
и delete
в C++). Если вы используете неинициализированную память, записываете за пределами концов массива, или не освобождаете указатель, Valgrind может это обнаружить. Поскольку это наиболее распространенные проблемы, эта статья будет сосредоточена главным образом на использовании Valgrind для обнаружения простых проблем с памятью, хотя Valgrind — это инструмент, который может сделать гораздо больше.
Для пользователей Windows: если вы не имеете доступа к Linux, или если вы хотите разрабатывать специальное программное обеспечение Windows, вас может заинтересовать IBM Purify, которая имеет аналогичные Valgrind функции для поиска утечек и неправильного доступа к памяти. Доступна пробная версия.
Получение Valgrind
Если вы работаете в Linux и у вас пока нет копии, то вы можете получить Valgrind на странице загрузки Valgrind.
Установка проста: распакуйте архив используя команды bzip2 и tar (X.Y.Z — номер версии в примерах ниже, разделённые символами точки):
tar -xf valgrind-X.Y.Z.tar
После выполнения этих команд, будет создан каталог с именем valgrind-X.Y.Z
, зайдите в этот каталог (команда cd
и через пробел путь) и выполните следующие команды:
make
make install
Теперь, когда вы установили Valgrind, давайте посмотрим как его использовать.
Поиск утечек памяти с помощью Valgrind
Утечки памяти одни из самых трудных для обнаружения ошибок, потому что они не вызывают никаких внешних проблем, до тех пор, пока у вас не закончится память и вам не удастся вызвать malloc
. В самом деле, при работе с языками C или C++, которые не имеют сборки мусора, почти половину времени вы можете потратить на правильное освобождение памяти. И даже одна ошибка может дорого обойтись, если ваша программа работает достаточно долго и следует этой ветви кода.
Когда вы запустите код, вы должны будете указать инструмент, который хотите использовать, просто запустите Valgrind для получения текущего списка. В этой статье мы сосредоточимся в основном на инструменте Memcheck, так как Valgrind с инструментом Memcheck позволит нам проверить правильность использования памяти. Без дополнительных аргументов, Valgrind выведет обзор вызовов free и malloc:
myProg — это имя программы (объектный файл, который получается после процесса построения проекта — компиляции). Если в проекте нет утечки памяти, вывод будет похож на этот
==15916== in use at exit: 16 bytes in 1 blocks
==15916== total heap usage: 5 allocs, 4 frees, 80 bytes allocated
==15916==
==15916== LEAK SUMMARY:
==15916== definitely lost: 16 bytes in 1 blocks
==15916== indirectly lost: 0 bytes in 0 blocks
==15916== possibly lost: 0 bytes in 0 blocks
==15916== still reachable: 0 bytes in 0 blocks
==15916== suppressed: 0 bytes in 0 blocks
==15916== Rerun with —leak-check=full to see details of leaked memory
==15916==
==15916== For counts of detected and suppressed errors, rerun with: -v
==15916== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
(Обратите внимание, что 15916 — идентификатор процесса в системе, он будет отличаться от запуска к запуску.)
Если у вас утечка памяти, то количество malloc
и количество free
будут отличаться (вы не можете использовать один free
для освобождения памяти, принадлежащей более чем одному malloc
). Мы вернемся к ошибкам позже, а сейчас заметьте, что некоторые ошибки могут быть упущены — это потому, что эти ошибки будут в стандартных библиотечных функциях, а не в вашем коде.
Если количество malloc
отличается от количества free
, вы захотите перезапустить программу снова с опцией leak-check
. Это покажет вам все вызовы malloc/new
и т.д., которые не имеют соответствующего free
.
В целях демонстрации я буду использовать очень простую программу, которую я скомпилирую в исполняемый файл под названием "example1"
int main() { char *ix = new char [10]; // или, char *ix = malloc(10) для языка СИ return 0; }
Это покажет некоторую информацию о программе, завершающуюся списком вызовов new
, которые не имеют последующих вызовов delete
:
==18350== in use at exit: 10 bytes in 1 blocks
==18350== total heap usage: 1 allocs, 0 frees, 10 bytes allocated
Это не дает нам столько информации, как хотелось бы, но — мы знаем, что утечка памяти произошла из-за вызова new
в main
, но у нас нет номера строки. Проблема в том, что мы компилировали без параметра -g
в g++
, который добавляет символы отладки. Поэтому, если мы перекомпилируем код с отладочной информацией, мы получим более полезную информацию:
==15635== in use at exit: 10 bytes in 1 blocks
==15635== total heap usage: 1 allocs, 0 frees, 10 bytes allocated
==15635==
==15635== 10 bytes in 1 blocks are definitely lost in loss record 1 of 1
==15635== at 0x4C2BAD7: operator new[](unsigned long) (vg_replace_malloc.c:363)
==15635== by 0x400575: main (man.cpp:3)
Теперь мы знаем точную строку, где был вызов new
, в третьей строке. Хотя отслеживание места, где необходимо освободить память, еще под вопросом, по крайней мере, вы знаете, с чего начать поиск. И так как для каждого вызова malloc
или new
, вы должны иметь план по работе с памятью, знание, где произошла утечка памяти поможет вам выяснить, где начать поиск.
Иногда --leak-check=yes
не показывает все утечки памяти. Чтобы найти абсолютно все непарные вызовы free
или new
, необходимо использовать --show-reachable=yes
. Вывод программы будет почти точно такой же, но он будет показывать больше неосвобождённой памяти.
Поиск недопустимого использования указателя с Valgrind
Valgrind может также показывать неверное использование памяти с помощью инструмента Memcheck. Например, если выделить массив импользуя malloc
или new
, а затем попытаться получить доступ к элементу за пределами массива:
char *ptr = new char [10]; ptr[10] = 'c';
Valgrind обнаружит его. Например, проверим следующую программу, с помощью Valgrind
int main() { char *ptr = new char [10]; ptr[10] = 'c'; return 0; }
Сначала компилируем в g++ этот исходник, команда g++ -g programname
. После этого в терминале вводим команду запуска Valgrind:
В ответ Valgrind нам выдаст следующее сообщение:
==15911== at 0x400582: main (man.cpp:4)
==15911== Address 0x5a0504a is 0 bytes after a block of size 10 alloc’d
==15911== at 0x4C2BAD7: operator new[](unsigned long) (vg_replace_malloc.c:363)
==15911== by 0x400575: main (man.cpp:3)
Это говорит нам о том, что мы используем указатель, выделенный для 10 байт, за пределами этого диапазона, — следовательно, мы получаем Invalid write
. Если бы мы пытались читать из этой памяти, мы бы получили предупреждение Invalid read of size num
, где num
— это объем памяти, который мы пытаемся прочитать. (Для char
это будет один, а для int
это будет либо 2, либо 4, в зависимости от вашей системы.) Как обычно, Valgrind выводит трассировку стека вызовов функций, так что мы точно знаем, где произошла ошибка .
Обнаружение использования неинициализированных переменных
Другой тип операции, которую обнаруживает Valgrind, это использование неинициализированного значения в условном операторе. Хотя у вас должна войти в привычку инициализация всех переменных, которые вы создаете, Valgrind поможет найти их в тех случаях, когда вы ее не делаете. Например, выполнив следующий код:
#include <iostream> int main() { int num; if(num == 0) std::cout << "num равна нулю"; // или printf() для языка Си return 0; }
через Valgrind, получим следующий ответ:
==16284== at 0x4006E0: main (man.cpp:6)
Valgrind достаточно умен, чтобы знать, что, если переменной не присваивается значение, то эта переменная все еще находится в «неинициализированном» состоянии, а значит никаких операций с ней быть не должно, до тех пор пока она не инициализируется. Например, выполнив следующий код:
#include <iostream> int func(int val) { if(val < 7) { std::cout << "val меньше 7\n"; } } int main() { int num; func(num); }
в Valgrind, результом будет следующее предупреждение:
==16371== at 0x4006E3: func(int) (man.cpp:5)
==16371== by 0x400707: main (man.cpp:14)
Вы думаете, что проблема была в func
, и что остальная часть вызовов стека, вероятно, не так уж важна. Но так как main
предоставляет неинициализированное значение в foo
(мы не присваиваем значение num
), то получается, что здесь мы должны начать искать и отслеживать путь присвоения переменных, пока не найдем переменную, которая не была инициализирована.
Это поможет только если вы на самом деле тестируете ту ветвь кода, и, в частности, тот условный оператор. Убедитесь в том, что вы охватили все пути выполнения во время тестирования!
Что еще находит Valgrind
Valgrind обнаружит другие случаи неправильного использования памяти: если вы вызываете delete
дважды с одним и тем же значением указателя, Valgrind обнаружит это, и вы получите сообщение об ошибке:
Valgrind также обнаруживает неправильно выбранные методы освобождения памяти. Например, в C++ есть три основных варианта для освобождения динамической памяти: free
, delete
и delete[]
. Функция free
должна быть согласована с вызовом malloc
, а не с вызовом, например, delete
— на некоторых системах вы могли бы не делать этого, но это не очень переносимо. Кроме того, ключевое слово delete
должно быть только в паре с ключевым словом new
(для выделения отдельных объектов), а ключевое слово delete[]
должно быть только в паре с ключевым словом new[]
(для распределения массивов). (Хотя некоторые компиляторы позволят обойтись без использования неправильной версии delete
, нет никакой гарантии, что все из них позволят это.)
Что Valgrind не найдет?
Valgrind не выполняет проверку границ в статических массивах (выделенных в стеке). Так что если вы объявите массив внутри функции:
int main() { char string[10]; string[11] = 'c'; }
то Valgrind не предупредит вас! Одно из возможных решений для тестирования — просто изменить свои статические массивы на динамически выделяемые, где вы получите проверку на границы, хотя это может внести дополнительную путаницу связанную с вызовами delete
.
Еще несколько предостережений
Какой недостаток использования Valgrind? Он использует больше памяти — до двух раз больше, чем ваша обычная программа. Если вы тестируете программу, которая использует очень много памяти, у вас могут возникнуть проблемы. Также увеличивается время запуска программы, когда вы используете Valgrind. Но это не проблема, так как это только во время тестирования. Но если ваша программа медленная, это может быть существенным.
Наконец, Valgrind не обнаружит все ваши ошибки — если вы не проверите переполнение буфера с помощью длинных строк ввода, Valgrind не скажет, что ваш код может записывать в память, которой он не должен касаться. Valgrind, как и любой другой инструмент, необходимо использовать разумно, как способ устранения проблем.
Вывод
Valgrind является инструментом для архитектур x86 и AMD64, и в настоящее время работает под управлением Linux. Valgrind позволяет программисту запустить исполняемый файл внутри своей собственной среды, в которой он проверяет непарные вызовы malloc
и другие виды недопустимого использования памяти (например, инициализируемой памяти) или неверных операций с памятью (например, двойное освобождение блока памяти или вызов неправильной функции освобождения памяти). Valgrind не проверяет использование статистически выделенных массивов.