Использование Valgrind для поиска утечек и недопустимого использования памяти

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 — номер версии в примерах ниже, разделённые символами точки):

bzip2 -d valgrind-X.Y.Z.tar.bz2
tar -xf valgrind-X.Y.Z.tar

После выполнения этих команд, будет создан каталог с именем valgrind-X.Y.Z, зайдите в этот каталог (команда cd и через пробел путь) и выполните следующие команды:

./configure
make
make install

Теперь, когда вы установили Valgrind, давайте посмотрим как его использовать.

Поиск утечек памяти с помощью Valgrind

Утечки памяти одни из самых трудных для обнаружения ошибок, потому что они не вызывают никаких внешних проблем, до тех пор, пока у вас не закончится память и вам не удастся вызвать malloc. В самом деле, при работе с языками C или C++, которые не имеют сборки мусора, почти половину времени вы можете потратить на правильное освобождение памяти. И даже одна ошибка может дорого обойтись, если ваша программа работает достаточно долго и следует этой ветви кода.

Когда вы запустите код, вы должны будете указать инструмент, который хотите использовать, просто запустите Valgrind для получения текущего списка. В этой статье мы сосредоточимся в основном на инструменте Memcheck, так как Valgrind с инструментом Memcheck позволит нам проверить правильность использования памяти. Без дополнительных аргументов, Valgrind выведет обзор вызовов free и malloc:

valgrind —tool=memcheck myProg

myProg — это имя программы (объектный файл, который получается после процесса построения проекта — компиляции). Если в проекте нет утечки памяти, вывод будет похож на этот

==15916== HEAP SUMMARY:
==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== HEAP SUMMARY:
==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== HEAP SUMMARY:
==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 —tool=memcheck —leak-check=yes ‘/home/den/a.out’

В ответ Valgrind нам выдаст следующее сообщение:

==15911== Invalid write of size 1
==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== Conditional jump or move depends on uninitialised value(s)
==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== Conditional jump or move depends on uninitialised value(s)
==16371== at 0x4006E3: func(int) (man.cpp:5)
==16371== by 0x400707: main (man.cpp:14)

Вы думаете, что проблема была в func, и что остальная часть вызовов стека, вероятно, не так уж важна. Но так как main предоставляет неинициализированное значение в foo (мы не присваиваем значение num), то получается, что здесь мы должны начать искать и отслеживать путь присвоения переменных, пока не найдем переменную, которая не была инициализирована.

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

Что еще находит Valgrind

Valgrind обнаружит другие случаи неправильного использования памяти: если вы вызываете delete  дважды с одним и тем же значением указателя, Valgrind обнаружит это, и вы получите сообщение об ошибке:

==16441== Invalid free() / delete / delete[] / realloc()

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 не проверяет использование статистически выделенных массивов.

Автор: Marienko L.
Дата: 20.10.2012
Поделиться:

Оставить комментарий

Вы должны войти, чтобы оставить комментарий.