Memcheck как устранить в си

от admin

Выявление утечек памяти в C++

Утечка памяти достаточно серьезная проблема возникающая при работе программы. Масштабность проявляется особенно при длительной работе программы, когда программа может исчерпать лимит выделения для нее памяти, а это приведет к очень нехорошим последствиям.

Простейшая программа с утечкой памяти:

В данном участке кода утечка памяти происходит из-за отсутствия оператора delete[] для массива p.

В статье «Анализ программ и компиляторов в Compiler Explorer» показано, что в ряде случаев утечки памяти можно найти средствами статического анализа программ, однако такие инструменты нередко дают «ложные срабатывания», а иногда — не обнаруживают утечку.

В этой статье рассмотрены:

1 Обнаружение утечек с библиотекой CRT

Библиотека CRT доступна в операционной системе Windows, она у вас уже есть если вы используете Microsoft Visual Studio. Для подключения анализатора памяти достаточно добавить пару макросов и собрать проект в отладочном режиме:

Видно, что первая группа макросов добавляется в самое начало вашей программы. За счет этих макросов все обращения в функциям new, malloc, free и delete заменяются на другие версии, которые помимо выделения/освобождения памяти сохраняют дополнительную информацию (сколько и где было выделено). Макрос _CrtDumpMemoryLeaks добавляется в конец программы (перед завершением работы) и выводит информацию о текущем состоянии памяти.

Для приведенной выше программы в окно отладки будет выведено следующее:

Утечка была обнаружена, кроме того, нам показывают ее размер и даже строку кода где это случилось.

2 Использование Visual Leak Detector

VLD поставляется в виде плагина для Microsoft Visual Studio, для его установки:
1) Скачиваем с официального сайта Microsoft и устанавливаем.
2) Добавляем в свойствах проекта во «включаемые каталоги» путь к каталогу include:
C:\ProgramFiles(x86)\VisualLeakDetector\include .
3) Добавляем в свойствах проекта в Каталог библиотек для win32 или win64 : путь к катлогу lib:

4) Добавляем в начало главного файла ( main.cpp ) #include<vld.h>
Получается так:

Все говото, утечки ищутся и выводятся в консоль отладки:

Видно, что VLD выводит информацию о стеке вызовов, но формат вывода крайне неудобный. Узнать в какой строке произошла утечка памяти совсем непросто.

3 Работа с Valgrind memcheck

Valgrind предоставляет множество инструментов для динамического анализа, поиском утечек памяти это не ограничивается. Работой с памятью занимается модуль memcheck. Для его использования необходимо:
1) Сстановить valgrind:
sudo apt install valgrind

2) Скомпилировать программу в debug-режиме и запустить ее через valgrind:
valgrind ./my_program
При таком запуске будет выведено сколько памяти было потеряно. Чтобы увидеть, где была выделена память, необходимо добавить опцию
—leak-check=full .

Результаты работы memcheck для нашего примера:

4 Как это устроено внутри

Подключение CRT приводит к оборачиванию функций работы с памятью в что-то такое:

Вы могли бы сделать это сами, но это непросто, а ведь надо еще разобрать все возможные ошибки при работе с памятью — free вместо delete , выделение объекта одного типа, а удаление — другого и так далее.

Да и зачем этим заниматься если его готовый CRT? В свою очередь, Visual Leak Detector представляет собой обертку над CRT.

Утилита Valgrind работает совсем иначе — ей не требуется модифировать исходный код вашей программы. Вместо этого программа запускается на виртуальном (моделируемом) процессоре в виртуальном окружении. Это окружение точно знает сколько памяти потребила программа, а процессор — в каких инструкциях эта память была запрошена. За счет этого valgrind позволяет не только обнаружить утечки памяти, но и получить статистику кэш-попаданий (модуль cachegrind), например. Однако, тут внутреннее устройство valgrind показано крайне упрощенно, а на самом деле все гораздо сложнее. Тем не менее, это один из лучших инструментов поиска утечек в мире.

4 Более сложный пример

Возможно, вам кажется что проблем с поиском утечек не возникает, тогда посмотрите такой пример:

Сколько времени у вас уйдет на поиск всех утечек? Ведь после возврата строки delete p в программе появятся другие утечки. Для начала — не будет вызывать деструктор

Cat() ведь деструктор

Animal() объявлен невиртуальным. После исправления этой ошибки — мы получим еще одну, ведь в классе Cat память выделяется через new , а освобождается через free .

Результаты применения всех трех рассмотренных инструментов примерно одинаковы (разве что VLD не выводит информацию о строке возникновения ошибки). Ниже приведен вывод valgring memcheck:

Видно, что обнаруживается 3 утечки, так как на 3 аллокации не приходится ни одной операции освобождения. После добавления delete в функцию main получаем одну утечку. Теперь на 3 аллокации приходится две операции освобождения. Найдена лишь одна утечка, однако на самом деле — вообще не вызывается деструктор

Cat() , поэтому после замены free на delete в нем — результат работы не изменится. А вот после добавления виртуального деструктора — утечек найдено не будет.

Понятно, что описанные инструменты сильно упрощают жизнь программисту. Найти без них течки памяти в чужом коде из 10 тысяч строк кода — задача крайне сложная, а с ними — решается элементарно. Однако, вывод результатов их работы очень неинформативный. Сравните полученные результаты с выводом статических анализаторов (в обоих статьях используются одинаковые примеры).

How do I use valgrind to find memory leaks?

How do I use valgrind to find the memory leaks in a program?

Please someone help me and describe the steps to carryout the procedure?

I am using Ubuntu 10.04 and I have a program a.c , please help me out.

4 Answers 4

How to Run Valgrind

Not to insult the OP, but for those who come to this question and are still new to Linux—you might have to install Valgrind on your system.

Valgrind is readily usable for C/C++ code, but can even be used for other languages when configured properly (see this for Python).

To run Valgrind, pass the executable as an argument (along with any parameters to the program).

The flags are, in short:

  • —leak-check=full : "each individual leak will be shown in detail"
  • —show-leak-kinds=all : Show all of "definite, indirect, possible, reachable" leak kinds in the "full" report.
  • —track-origins=yes : Favor useful output over speed. This tracks the origins of uninitialized values, which could be very useful for memory errors. Consider turning off if Valgrind is unacceptably slow.
  • —verbose : Can tell you about unusual behavior of your program. Repeat for more verbosity.
  • —log-file : Write to a file. Useful when output exceeds terminal space.

Finally, you would like to see a Valgrind report that looks like this:

I have a leak, but WHERE?

So, you have a memory leak, and Valgrind isn’t saying anything meaningful. Perhaps, something like this:

Let’s take a look at the C code I wrote too:

Well, there were 5 bytes lost. How did it happen? The error report just says main and malloc . In a larger program, that would be seriously troublesome to hunt down. This is because of how the executable was compiled. We can actually get line-by-line details on what went wrong. Recompile your program with a debug flag (I’m using gcc here):

Now with this debug build, Valgrind points to the exact line of code allocating the memory that got leaked! (The wording is important: it might not be exactly where your leak is, but what got leaked. The trace helps you find where.)

Techniques for Debugging Memory Leaks & Errors

Make use of www.cplusplus.com! It has great documentation on C/C++ functions.

General advice for memory leaks:

Make sure your dynamically allocated memory does in fact get freed.

Don’t allocate memory and forget to assign the pointer.

Don’t overwrite a pointer with a new one unless the old memory is freed.

General advice for memory errors:

Access and write to addresses and indices you’re sure belong to you. Memory errors are different from leaks; they’re often just IndexOutOfBoundsException type problems.

Don’t access or write to memory after freeing it.

Sometimes your leaks/errors can be linked to one another, much like an IDE discovering that you haven’t typed a closing bracket yet. Resolving one issue can resolve others, so look for one that looks a good culprit and apply some of these ideas:

List out the functions in your code that depend on/are dependent on the "offending" code that has the memory error. Follow the program’s execution (maybe even in gdb perhaps), and look for precondition/postcondition errors. The idea is to trace your program’s execution while focusing on the lifetime of allocated memory.

Try commenting out the "offending" block of code (within reason, so your code still compiles). If the Valgrind error goes away, you’ve found where it is.

If all else fails, try looking it up. Valgrind has documentation too!

A Look at Common Leaks and Errors

Watch your pointers

As a teaching assistant, I’ve seen this mistake often. The student makes use of a local variable and forgets to update the original pointer. The error here is noticing that realloc can actually move the allocated memory somewhere else and change the pointer’s location. We then leave resizeArray without telling array->data where the array was moved to.

Invalid write

Notice that Valgrind points us to the commented line of code above. The array of size 26 is indexed [0,25] which is why *(alphabet + 26) is an invalid write—it’s out of bounds. An invalid write is a common result of off-by-one errors. Look at the left side of your assignment operation.

Invalid read

Valgrind points us to the commented line above. Look at the last iteration here, which is
*(destination + 26) = *(source + 26); . However, *(source + 26) is out of bounds again, similarly to the invalid write. Invalid reads are also a common result of off-by-one errors. Look at the right side of your assignment operation.

The Open Source (U/Dys)topia

How do I know when the leak is mine? How do I find my leak when I’m using someone else’s code? I found a leak that isn’t mine; should I do something? All are legitimate questions. First, 2 real-world examples that show 2 classes of common encounters.

Jansson: a JSON library

This is a simple program: it reads a JSON string and parses it. In the making, we use library calls to do the parsing for us. Jansson makes the necessary allocations dynamically since JSON can contain nested structures of itself. However, this doesn’t mean we decref or "free" the memory given to us from every function. In fact, this code I wrote above throws both an "Invalid read" and an "Invalid write". Those errors go away when you take out the decref line for value .

Why? The variable value is considered a "borrowed reference" in the Jansson API. Jansson keeps track of its memory for you, and you simply have to decref JSON structures independent of each other. The lesson here: read the documentation. Really. It’s sometimes hard to understand, but they’re telling you why these things happen. Instead, we have existing questions about this memory error.

SDL: a graphics and gaming library

What’s wrong with this code? It consistently leaks

212 KiB of memory for me. Take a moment to think about it. We turn SDL on and then off. Answer? There is nothing wrong.

That might sound bizarre at first. Truth be told, graphics are messy and sometimes you have to accept some leaks as being part of the standard library. The lesson here: you need not quell every memory leak. Sometimes you just need to suppress the leaks because they’re known issues you can’t do anything about. (This is not my permission to ignore your own leaks!)

Answers unto the void

How do I know when the leak is mine?
It is. (99% sure, anyway)

How do I find my leak when I’m using someone else’s code?
Chances are someone else already found it. Try Google! If that fails, use the skills I gave you above. If that fails and you mostly see API calls and little of your own stack trace, see the next question.

I found a leak that isn’t mine; should I do something?
Yes! Most APIs have ways to report bugs and issues. Use them! Help give back to the tools you’re using in your project!

Further Reading

Thanks for staying with me this long. I hope you’ve learned something, as I tried to tend to the broad spectrum of people arriving at this answer. Some things I hope you’ve asked along the way: How does C’s memory allocator work? What actually is a memory leak and a memory error? How are they different from segfaults? How does Valgrind work? If you had any of these, please do feed your curiousity:

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

Valgrind является многоцелевым инструментом профилирования кода и отладки памяти для Linux на x86, и, начиная с версии 3 и AMD64. Это позволяет запускать программу в собственной среде Valgrind, что контролирует использование памяти, например, вызовы malloc и free (или new и delete в C++). Если вы используете неинициализированную память, записываете за пределами концов массива, или не освобождаете указатель, Valgrind может это обнаружить. Поскольку это наиболее распространенные проблемы, эта статья будет сосредоточена главным образом на использовании Valgrind для обнаружения простых проблем с памятью, хотя Valgrind — это инструмент, который может сделать гораздо больше.

Читать:
Как проверить кто блочит порт 8080

Для пользователей Windows: если вы не имеете доступа к Linux, или если вы хотите разрабатывать специальное программное обеспечение Windows, вас может заинтересовать IBM Purify, которая имеет аналогичные Valgrind функции для поиска утечек и неправильного доступа к памяти. Доступна пробная версия.

Получение Valgrind

Если вы работаете в Linux и у вас пока нет копии, то вы можете получить Valgrind на странице загрузки Valgrind.

Установка проста: распакуйте архив используя команды bzip2 и tar (X.Y.Z — номер версии в примерах ниже, разделённые символами точки):

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

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

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

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

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

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

(Обратите внимание, что 15916 — идентификатор процесса в системе, он будет отличаться от запуска к запуску.)

Если у вас утечка памяти, то количество malloc и количество free будут отличаться (вы не можете использовать один free для освобождения памяти, принадлежащей более чем одному malloc ). Мы вернемся к ошибкам позже, а сейчас заметьте, что некоторые ошибки могут быть упущены — это потому, что эти ошибки будут в стандартных библиотечных функциях, а не в вашем коде.

Если количество malloc отличается от количества free , вы захотите перезапустить программу снова с опцией leak-check . Это покажет вам все вызовы malloc/new и т.д., которые не имеют соответствующего free .

В целях демонстрации я буду использовать очень простую программу, которую я скомпилирую в исполняемый файл под названием «example1»

Это покажет некоторую информацию о программе, завершающуюся списком вызовов new , которые не имеют последующих вызовов delete :

Это не дает нам столько информации, как хотелось бы, но — мы знаем, что утечка памяти произошла из-за вызова new в main , но у нас нет номера строки. Проблема в том, что мы компилировали без параметра -g в g++ , который добавляет символы отладки. Поэтому, если мы перекомпилируем код с отладочной информацией, мы получим более полезную информацию:

Теперь мы знаем точную строку, где был вызов new , в третьей строке. Хотя отслеживание места, где необходимо освободить память, еще под вопросом, по крайней мере, вы знаете, с чего начать поиск. И так как для каждого вызова malloc или new , вы должны иметь план по работе с памятью, знание, где произошла утечка памяти поможет вам выяснить, где начать поиск.

Иногда —leak-check=yes не показывает все утечки памяти. Чтобы найти абсолютно все непарные вызовы free или new , необходимо использовать —show-reachable=yes . Вывод программы будет почти точно такой же, но он будет показывать больше неосвобождённой памяти.

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

Valgrind может также показывать неверное использование памяти с помощью инструмента Memcheck. Например, если выделить массив импользуя malloc или new , а затем попытаться получить доступ к элементу за пределами массива:

Valgrind обнаружит его. Например, проверим следующую программу, с помощью Valgrind

Сначала компилируем в g++ этот исходник, команда g++ -g programname . После этого в терминале вводим команду запуска Valgrind:

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

Это говорит нам о том, что мы используем указатель, выделенный для 10 байт, за пределами этого диапазона, — следовательно, мы получаем Invalid write . Если бы мы пытались читать из этой памяти, мы бы получили предупреждение Invalid read of size num , где num — это объем памяти, который мы пытаемся прочитать. (Для char это будет один, а для int это будет либо 2, либо 4, в зависимости от вашей системы.) Как обычно, Valgrind выводит трассировку стека вызовов функций, так что мы точно знаем, где произошла ошибка .

Обнаружение использования неинициализированных переменных

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

через Valgrind, получим следующий ответ:

Valgrind достаточно умен, чтобы знать, что, если переменной не присваивается значение, то эта переменная все еще находится в «неинициализированном» состоянии, а значит никаких операций с ней быть не должно, до тех пор пока она не инициализируется. Например, выполнив следующий код:

в Valgrind, результом будет следующее предупреждение:

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

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

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

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

Valgrind также обнаруживает неправильно выбранные методы освобождения памяти. Например, в C++ есть три основных варианта для освобождения динамической памяти: free , delete и delete[] . Функция free должна быть согласована с вызовом malloc , а не с вызовом, например, delete — на некоторых системах вы могли бы не делать этого, но это не очень переносимо. Кроме того, ключевое слово delete должно быть только в паре с ключевым словом new (для выделения отдельных объектов), а ключевое слово delete[] должно быть только в паре с ключевым словом new[] (для распределения массивов). (Хотя некоторые компиляторы позволят обойтись без использования неправильной версии delete , нет никакой гарантии, что все из них позволят это.)

Что Valgrind не найдет?

Valgrind не выполняет проверку границ в статических массивах (выделенных в стеке). Так что если вы объявите массив внутри функции:

то Valgrind не предупредит вас! Одно из возможных решений для тестирования — просто изменить свои статические массивы на динамически выделяемые, где вы получите проверку на границы, хотя это может внести дополнительную путаницу связанную с вызовами delete .

Еще несколько предостережений

Какой недостаток использования Valgrind? Он использует больше памяти — до двух раз больше, чем ваша обычная программа. Если вы тестируете программу, которая использует очень много памяти, у вас могут возникнуть проблемы. Также увеличивается время запуска программы, когда вы используете Valgrind. Но это не проблема, так как это только во время тестирования. Но если ваша программа медленная, это может быть существенным.

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

Вывод

Valgrind является инструментом для архитектур x86 и AMD64, и в настоящее время работает под управлением Linux. Valgrind позволяет программисту запустить исполняемый файл внутри своей собственной среды, в которой он проверяет непарные вызовы malloc и другие виды недопустимого использования памяти (например, инициализируемой памяти) или неверных операций с памятью (например, двойное освобождение блока памяти или вызов неправильной функции освобождения памяти). Valgrind не проверяет использование статистически выделенных массивов.

Ловим утечки памяти в С/С++

Сегодня я хочу немного приоткрыть свет над тем, как бороться с утечкой памяти в Си или С++.

На Хабре уже существует две статьи, а именно: Боремся с утечками памяти (C++ CRT) и Утечки памяти в С++: Visual Leak Detector. Однако я считаю, что они недостаточно раскрыты, или данные способы могут не дать нужного вам результата, поэтому я хотел бы по возможности разобрать всем доступные способы, дабы облегчить вам жизнь.

Windows — разработка
Начнем с Windows, а именно разработка под Visual Studio, так как большинство начинающих программистов пишут именно под этой IDE.

Для понимания, что происходит, прикладываю реальный пример:

А также есть Student.h и Student.c в котором объявлены структуры и функции.

Есть задача: продемонстрировать отсутствие утечек памяти. Первое, что приходит в голову — это CRT. Тут все достаточно просто.

В начало файла, где находится main, необходимо добавить этот кусок кода:

А перед return 0 нужно прописать это: _CrtDumpMemoryLeaks(); .

В итоге, в режиме Debug, студия будет выводить это:

Супер! Теперь вы знаете, что у вас утечка памяти. Теперь нужно устранить это, поэтому необходимо просто узнать, где мы забываем очистить память. И вот тут возникает проблема: а где, собственно, выделялась эта память?

После того, как я повторил все шаги, я выяснил, что память теряется где-то здесь:

Но как так — то? Я же все освобождаю? Или нет?

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

В итоге, после 15 минут прогугливания, я нашел аналог Valgrind — Visual Leak Detector. Это сторонняя библиотека, обертка над CRT, которая обещала показывать трассировку! Это то, что мне необходимо.

Чтобы её установить, необходимо перейти в репозиторий и в assets найти vld-2.5.1-setup.exe

Правда, последнее обновление было со времен Visual Studio 2015, но оно работает и с Visual Studio 2019. Установка стандартная, просто следуйте инструкциям.

Чтобы подключить VLD, необходимо прописать #include <vld.h> .

Преимущество этой утилиты заключается в том, что можно не запускать в режиме debug (F5), ибо все выводится в консоль. В самом начале будет выводиться это:

И вот, что будет выдавать при утечке памяти:

Вот, я вижу трассировку! Так, а где строки кода? А где названия функций?

Ладно, обещание сдержали, однако это не тот результат, который я хотел.

Остается один вариант, который я нашел в гугле: моментальный снимок памяти. Он делается просто: в режиме debug, когда доходите до return 0, необходимо в средстве диагностики перейти во вкладку «Использование памяти» и нажать на «Сделать снимок». Возможно, у вас будет отключена эта функция, как на первом скриншоте. Тогда необходимо включить, и перезапустить дебаг.

После того, как вы сделали снимок, у вас появится под кучей размер. Я думаю, это сколько всего было выделено памяти в ходе работы программы. Нажимаем на этот размер. У нас появится окошко, в котором будут содержаться объекты, которые хранятся в этой куче. Чтобы посмотреть подробную информацию, необходимо выбрать объект и нажать на кнопку «Экземпляры представления объекта Foo».

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

Linux — разработка
Теперь, посмотрим, что творится в Linux.

В Linux существует утилита valgrind. Чтобы установить valgrind, необходимо в консоли прописать sudo apt install valgrind (Для Debian-семейства).

Я написал небольшую программу, которая заполняет динамический массив, но при этом, не очищается память:

Скомпилировав программу с помощью CLang, мы получаем .out файл, который мы подкидываем valgrind’у.

С помощью команды valgrind ./a.out . Как работает valgrind, думаю, есть смысл описать в отдельной статье, а сейчас, как выполнится программа, valgrind выведет это:

Таким образом, valgrind пока показывает, сколько памяти было потеряно. Чтобы увидеть, где была выделена память, необходимо прописать —leak-check=full , и тогда, valgrind, помимо выше описанного, выведет это:

Конечно, тут не указана строка, однако уже указана функция, что не может не радовать.

Есть альтернативы valgrind’у, такие как strace или Dr.Memory, но я ими не пользовался, да и они применяется в основном там, где valgrind бессилен.

Выводы

Я рад, что мне довелось столкнуться с проблемой поиска утечки памяти в Visual Studio, так как я узнал много новых инструментов, когда и как ими пользоваться и начал разбирать, как работают эти инструменты.

Похожие статьи