Майкл Джонсон - Разработка приложений в среде Linux. Второе издание
#1 0x0072c969 in raise() from /lib/tls/libc.so.6
#2 0x0072e322 in abort() from /lib/tls/libc.so.6
#3 0x007792c4 in freehook() from /lib/tls/libc.so.6
#4 0x00774fa5 in free() from /lib/tls/libc.so.6
#5 0x0804842b in broken() at broken.c:17
#6 0x08048520 in main() at broken.с:47
Важной частью этого кода является обнаруженная ошибка в строке 17 файла broken.с. Видно, что ошибка была обнаружена во время первого вызова free(), который указал на наличие проблемы в области памяти dyn. (freehook() представляет собой ловушку, с помощью которой mcheck выполняет проверки.)
Библиотека mcheck не может помочь в обнаружении переполнения или недогрузки буфера в локальных или глобальных переменных, а только в областях памяти, распределенных с помощью malloc().
7.2.2. Использование mtrace() для отслеживания распределений памяти
Один из простых способов нахождения всех утечек памяти в программе предусматривает регистрацию всех вызовов malloc() и free(). По окончании программы очень легко сопоставить блоки, распределенные через malloc(), с точками, где они были освобождены с помощью free() или сообщить об ошибке, если для какого-то блока free() не вызывалась.
В отличие от mcheck(), в mtrace() нет соответствующей библиотеки для компоновки. Это не проблема, поскольку трассировку можно осуществлять в gdb. Однако для включения трассировки с помощью mtrace() должна быть установлена переменная окружения MALLOC_TRACE в допустимое имя файла; это может быть либо имя существующего файла, в который процесс может вести запись, либо имя нового файла, который процесс создаст и будет в него записывать.
$ MALLOC_TRACE=mtrace.log gdb broken
...
(gdb) breakmain
Breakpoint 1 at 0x80483f4: filebroken.c, line 14.
(gdb) command 1
Type commands for when Breakpoint 1 is hit, one per line.
End with a line saying just "end".
>call mtrace()
>continue
>end
(gdb) run
Starting program: /usr/src/lad/code/broken
Breakpoint 1, main() at broken.с:47
47 return broken();
$1 = 0
1: 12345
2: 12345678
3: 12345678
4: 12345
5: 12345
6: 12345
7: 12345
Program exited normally.
Программа завершена нормально.
(gdb) quit
$ ls -l mtrace.log
-rw-rw-r-- 1 ewt ewt 220 Dec 27 23:41 mtrace.log
$ mtrace ./broken mtrace.log
Memory not freed:
He освобождена память:
----------------------
Address Size Caller
Адрес Размер Место вызова
0x09211378 0x5 at /usr/src/lad/code/broken.с:20
Обратите внимание, что программа mtrace точно обнаружила утечку памяти. Также она может найти факт освобождения с помощью free() памяти, которая ранее не распределялась, если этот факт будет зафиксирован в журнальном файле, но на практике так не происходит, поскольку в этом случае программа немедленно аварийно завершается.
7.3. Поиск утечек памяти с помощью mpr
Возможности mtrace() в glibc достаточно неплохие, но профилировщик распределения памяти mpr (http://www3.telus.net/taj_khattra/mpr.html) в некоторых аспектах более прост в использовании и содержит более совершенные сценарии для обработки выходных журнальных файлов.
Первый шаг в применении mpr (после сборки кода с включенной отладочной информацией[10]) состоит в установке переменной окружения MPRFI, которая указывает mpr, с какой командой связывать журнал (если переменная не установлена, журнал не генерируется). Для небольших программ MPRFI устанавливается подобно cat >mpr.log. Для программ покрупнее MPRFI можно существенно сэкономить пространство за счет сжатия журнального файла во время его записи, установив MPRFI в gzip -1 >mpr.log.gz.
Самый легкий способ — воспользоваться сценарием mpr для запуска программы; если MPRFI еще не установлена, она получит значение gzip -1 >log.%p.gz, что приведет к созданию журнального файла с идентификатором процесса отлаживаемой программы и загрузке библиотеки mpr. В результате сборка программы не понадобится. Ниже показан пример создания журнального файла для исправленной версии нашей тестовой программы.
$ MPRFI="cat >mpr.log" mpr ./broken
1: 12345
2: 12345678
3: 12345678
4: 12345
5: 12345
6: 12345
7: 12345
$ ls -l mpr.log
-rw-rw-r-- 1 ewt ewt 142 May 17 16:22 mpr.log
После создания журнального файла доступны многие средства для его анализа. Все эти программы получают журнальный файл mpr в качестве стандартного ввода. Если вывод из этих средств содержит числа в тех местах, где ожидаются имена функций (возможно, с предупреждением вроде "cannot map pc to name" ("невозможно отобразить программный счетчик на имя")), проблема может быть связана с версией утилиты awk, которую использует mpr. В документации mpr для достижения лучших результатов рекомендуется экспортировать переменную окружения MPRAWK для выбора mawk в качестве версии awk: export MPRAWK='mawk -W sprintf=4096'. Кроме того, сбить с толку mpr может еще и рандомизация стека, которая обеспечивается функциональностью ядра "Exec-shield"; исправить положение можно за счет использования команды setarch, отключающей Exec-shield во время работы исследуемой программы и во время работы фильтров mpr: setarch i386 mpr программа и setarch i386 mprmap ...
В конечном итоге для некоторых стековых фреймов mpr может не найти символическое имя; в этом случае просто проигнорируйте их.
mprmap программа Преобразует адреса программы в журнале mpr в имена функций и местоположения в исходном коде. В аргументе указывается имя исполняемого файла, для которого должен генерироваться журнал. Чтобы увидеть все распределения в программе вместе с цепочкой вызовов функций, которые осуществили эти распределения, можно использовать mprmap программа < mpr.log. По умолчанию отображаются имена функций. При указании флажка -f отображаются также имена файлов, а при указании -l — еще и номера строк внутри файлов. Флажок -l подразумевает наличие -f. Вывод этой программы является допустимым журнальным файлом mpr, который может быть связан каналом с любой другой утилитой mpr. mprchain Преобразует журнал в вывод, сгруппированный по цепочкам вызовов. Цепочка вызовов функций — это список всех функций, активных в программе на определенный момент. Например, если main() вызывает getargs(), которая впоследствии вызывает parsearg(), активная цепочка вызовов во время работы parsearg() отображается как main:getargs:parsearg. Для каждой отдельной цепочки вызовов, в которой распределялась память во время выполнения программы, mprchain отображает количество распределений и общее количество распределенных байт. mprleak Этот фильтр просматривает журнальный файл на предмет наличия всех неосвобожденных фрагментов памяти. В качестве стандартного вывода генерируется новый журнальный файл, содержащий только те распределения, которые могут привести к утечкам памяти. Вывод этой программы является допустимым журнальным файлом mpr, который может быть связан каналом с любой другой утилитой mpr. mprsize Этот фильтр сортирует распределения памяти по размеру. Чтобы просмотреть утечки памяти по размеру, нужно передать вывод mprleak на вход mprsize. mprhisto Отображает гистограмму распределений памяти.Теперь, когда известно об анализаторах журнальных файлов, очень просто найти утечки памяти в нашей тестовой программе. Для этого достаточно воспользоваться командой mprleak mpr.log | mprmap -l ./broken (что эквивалентно mprmap -l ./broken mpr.log | mprleak) и в результате обнаружить утечку памяти в строке 20.
$ mprleak mpr.log | mpr map -l ./broken
m:broken(broken.c,20): main(broken.c,47):5:134518624
7.4. Обнаружение ошибок памяти с помощью Valgrind
Valgrind (http://valgrind.kde.org/) представляет собой специфический для Intel х86 инструмент, эмулирующий центральный процессор класса х86 для непосредственного наблюдения над всеми обращениями к памяти и анализа потока данных (он может, например, выявлять чтения неинициализированной памяти, тем не менее перенос содержимого неинициализированной ячейки в другую ячейку, которая никогда для читается, как неинициализированное чтение он не трактует). Valgrind обладает множеством других возможностей, включая просмотр использования кэша и поиск состязаний в многопоточных программах. В действительности, в Valgrind имеется универсальное средство для добавления большего количества возможностей, основанных на его эмуляторе центрального процессора. Однако для наших целей мы лишь кратко рассмотрим выполнение с его помощью агрессивного поиска ошибок памяти, что представляет его стандартное поведение.