Самые «сложные» ошибки в программах, написанных на Си, возникают из-за неправильного управления памятью. С помощью доступа к памяти навредить очень легко, уж поверьте. Например, выделить неправильный объем памяти или забыть проинициализировать переменные, очищать память несколько раз. Найти «корень зла» порой нелегко, но, к счастью, есть одна очень полезная программа. Она называется Valgrind. Эта программа может во многом помочь.

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

Valgrind также сообщает про менее опасные проблемы с памятью, например, утечки памяти, или если вы выделили память в куче, а затем забыли её освободить. Valgrind, запускается на бинарном исполняемом файле, а не на .c или .h. Так что прежде, чем воспользоваться Valgrind, убедитесь, что вы уже скомпилировали с помощью clang или make самую актуальную версию вашей программы. Тогда для запуска программы под Valgrind нужно просто добавить слово Valgrind к стандартной команде.

Сначала для проверки памяти при настройках исполняющего файла исполнения Valgrind выполняет сложные манипуляции, поэтому такая операция может длиться довольно долго. Потом программа будет работать нормально, но несколько медленнее, чем без Valgrind. После окончания работы, в консоль будет выведен отчёт об использовании программой памяти. Наша программа не принимает на вход никаких аргументов. Clean_program только и делает, что выделяет место в куче для блока целых чисел int, записывает туда значения и освобождает целый блок.

Если все хорошо, это выглядит примерно так:

jharvard@appliance (~/valgrind): make clean_program
clang –ggdb –std=c99 –Wall –Werror   clean_program.c –lcrypt –lcs50 –lm –o clean_program
jharvard@appliance (~/valgrind): valgrind ./clean_program

==22623== Memcheck, a memory error detector
==22623== Copyright © 2002-2011, and GNU GPL ’d, by Julian Seward et al.
==22623== Using Valgrind-3.7.0 and LibVEX; rerun with –h for copyright info
==22623== Command: ./clean_program
==22623==
==22623==
==22623== HEAP SUMMARY:
==22623==           in use at exit: 0 bytes in 0 blocks
==22623==     total heap usage: 1 allocs, 1 frees, 16 bytes allocated
==22623==
==22623== All heap blocks were freed – no leaks are possible
==22623==
==22623== For counts of detected and suppressed errors, rerun with: -v
==22623== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
jharvard@appliance (~/valgrind):

Как видите, ошибок и утечек нет.

Приведем пример работы Valgrind с проблемной программой, которая выполняет недействительное считывание из кучи. Если просто запустить программу на выполнение, то скрытую ошибку компилятор не заметит. Программа выполнилась, как будто бы всё хорошо!

jharvard@appliance (~/valgrind): make invalid_read
clang –ggdb –std=c99 –Wall –Werror   invalid_read.c –lcrypt –lcs50 –lm –o invalid_read
jharvard@appliance (~/valgrind): valgrind ./ invalid_read

Теперь запустим программу вместе с Valgrind:

jharvard@appliance (~/valgrind): valgrind ./invalid_read
Valgrind - 1

Отчет сообщает о двух ошибках. Два недействительных считывания размером в 4 байта.

Valgrind - 2

Оба они выполняются в функции main, первый — в 16 ряду, второй — в 19.

Приоткроем завесу. Вот код этой функции:

Valgrind - 3

Из кода видно, что первый printf пытается считать целое число после окончания выделенной памяти. Именно об этом нам и говорил Valgrind, утверждая в своем отчёте, что адрес, по которому мы попытались провести считывание, находится за 0 байт за окончанием блока размером 16 байта.