FreeBSD Debug

Материал из OpenWiki
Перейти к: навигация, поиск

Отладка проблем во FreeBSD

Введение

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

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

На кого ориентирована эта статья? Сложно сказать. Не каждый пользователь или системный администратор решится взять в руки отладчик, чтобы попытаться разобраться в сути проблемы. Но бывают случаи, когда сбои случаются регулярно и серьёзно мешают работе, либо в погоне за новыми возможностями вы стали использовать версию системы для разработчиков. Возможно, вы просто хотите помочь проекту и принимаете участие в тестировании предварительных выпусков системы или каких-нибудь экспериментальных патчей, программ или драйверов. Чтобы ваша помощь была наиболее полезной, либо вы хотите как можно скорее решить свою проблему, вы должны попытаться воспользоваться рассказанными в этой статье методиками по созданию наиболее информативного отчёта о проблеме (PR – Problem Report). Некоторые методики могут оказаться полезными для людей поддерживающих порты FreeBSD или желающих создать свой порт для какого-нибудь приложения.

Большинство приводимых в этой статье методик, утилит, имён переменных и других специфических данных ориентировано на использование в последних версиях FreeBSD, к моменту написания статьи это FreeBSD 6.2-RELEASE и 7.0-CURRENT. Но практически всё это будет работать и на более старых версиях системы. Если у вас по каким-то причинам не получается воспроизвести примеры или аналогичные статье действия, проверьте правильность параметров и имён в соответствующих руководствах пользователя.

Косвенные методы отладки

Бывает так, что программы работают некорректно или завершаются с ошибкой по причине неверной конфигурации. В этом случае конечно можно обвинить разработчиков в том, что не предусмотрели такой ситуации, но с другой стороны это ошибка администратора. Да и к тому же, программа может и не иметь прямого отношения к проекту FreeBSD. Из широко распространённых ошибок такого типа можно отметить: отсутствие файлов конфигурации, отсутствие каких-либо файлов специальных устройств (/dev/null, /dev/random и т.п.), неверные права доступа, отсутствие разделяемых библиотек, либо несоответствие их версий. Такие ошибки можно обнаружить и исправить без помощи отладчика и разработчиков. Но даже если не получается это сделать, информация, полученная при этом, будет очень полезна разработчикам.

Проверка оборудования

Файлы журналов

Не буду оригинальным, первый метод это, конечно же, чтение документации и файлов журналов. Если программа ничего не выводит и просто «молча» завершается, стоит проверить, нет ли среди её параметров или в файле конфигурации специальных опций для отладки, для повышения уровня отладочных сообщений, для запуска не в фоновом режиме и т.п. Возможно, что сервис syslogd не настроен на сохранение сообщений с тем уровнем или типом, который использует приложение. Если в документации нет информации о типе и уровне сообщений, с которыми программа взаимодействует с сервисом syslogd, всегда можно обратиться к исходному тексту. Простой пример того, как можно узнать эту информацию:

# cd /path/to/program/src
# grep –rnE "openlog|syslog" *
main.c:44:#include <syslog.h>
main.c:203:             syslog(LOG_CRIT, "cannot allocate buffer");
main.c:235:             syslog(LOG_ERR, "cannot encode message");
main.c:1425:    openlog(prefix, LOG_PID | (background ? 0 : LOG_PERROR), LOG_USER);
main.c:1480:            syslog(LOG_ERR, "atexit failed: %m");
main.c:1484:            syslog(LOG_WARNING, "cannot start UDP transport");
main.c:1486:            syslog(LOG_WARNING, "cannot start LSOCK transport");
main.c:1493:            syslog(LOG_ERR, "evCreate: %m");
main.c:1501:            syslog(LOG_ERR, "error in config file"); 

Из этого вывода видно, что программа взаимодействует с демоном syslogd с помощью сообщений, тип которых определяется как LOG_USER, а уровни – LOG_CRIT, LOG_ERR, LOG_WARNING. Эти сообщения могут сохраняться сервисом syslogd в файлы, для которых в /etc/syslog.conf определена маска, например user.*, *.crit, *.warning или user.err.

Более простой метод – включить в syslog.conf сохранение всех сообщений в один файл, либо настроить выборочное сохранение сообщений для конкретной программы. Подробнее об этом можно почитать в руководствах syslog(3) и syslog.conf(5).

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

# cd /path/to/program/src
# grep –rnE "DEBUG|DBG" *
snmp_mibII/mibII_route.c:138:#ifdef DEBUG_ROUTE
snmp_mibII/mibII_route.c:152:#ifdef DEBUG_ROUTE 
...
snmpd/config.c:359:#ifdef DEBUGGING
snmpd/config.c:365:#ifdef DEBUGGING
...
snmpd/main.c:76:      LOG_DEBUG,      /* log_pri */
snmpd/main.c:1123:    syslog(LOG_DEBUG, "Dump of SNMPd %lu\n", (u_long)getpid());

Здесь среди прочего вывода есть конструкции условной компиляции препроцессора языка Си #ifdef DEBUGGING и #ifdef DEBUG_ROUTE. Если посмотреть в код, то там будет нечто похожее:

#ifdef DEBUGGING
  fprintf(stderr, "EOF");
#endif

/* ... */

#ifdef DEBUG_ROUTE
  syslog(LOG_WARNING, "%s: DELETE: %u.%u.%u.%u "
        "%u.%u.%u.%u %u %u.%u.%u.%u not found", __func__,
        key.index[0], key.index[1], key.index[2],
        key.index[3], key.index[4], key.index[5],
        key.index[6], key.index[7], key.index[8],
        key.index[9], key.index[10], key.index[11],
        key.index[12]);
#endif

То есть, если на этапе обработки программы препроцессором языка Си определить константы DEBUGGING и DEBUG_ROUTE, то код, выводящий дополнительные сообщения будет включён в компилируемую программу, иначе он будет пропущен. Имена констант по негласному соглашению разработчиков обычно содержат в себе слова DEBUG или DBG. Так как эти соглашения являются негласными, то поиск перечисленных сочетаний может не дать результатов. Тогда можно попытаться использовать подобную команду:

# grep -rn -B2 -A2 -E "#\s*(ifdef|ifndef|if)" * | less

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

#ifndef DEBUGGING
#define DEBUGGING 1
#endif
#ifndef DEBUG_ROUTE
#define DEBUG_ROUTE 1
#endif

Только делать это нужно именно в тех файлах, где встречаются соответствующие директивы и желательно в самом начале файлов.

Более правильный, но в тоже время более сложный способ – перекомпилирование программы с заданием нужных констант в командной строке компилятора. Сложность здесь заключается в том, что программы могут конфигурироваться и компилироваться с помощью множества различных методов, для которых определение этих констант может оказаться различным. В случае если это системная программа или поставленное из портов ПО, можно переустановить его поместив строку CFLAGS+= –DDEBUGGING –DDEBUG_ROUTE в /etc/make.conf. Параметр –D<имя константы> в строке CFLAGS указывает компилятору (подразумевается gcc) на необходимость определить в заданное значение (если значение не указано, то подразумевается единица) соответствующую константу препроцессора. Сама же переменная CFLAGS содержит параметры, с которыми make(1) будет вызывать компилятор языка Си. В большинстве случаев этого должно быть достаточно. Что бы не изменять make.conf, можно попытаться указать утилите make дополнительные параметры через коммандную строку:

# make DEBUG_FLAGS="-DDEBUGGING –DDEBUG_ROUTE" install

Переменная DEBUG_FLAGS будет добавлена к CFLAGS, и, соответсвенно, компилятор будет вызван с нужными параметрами.

Для большинства программ, устанавливаемых из исходных текстов с использованием configure и gmake, аналогичных результатов можно добиться путём установки переменных окружения CFLAGS или CPPFLAGS (CXXFLAGS). Стоит так же проверить наличие дополнительных параметров с подобным функционалом у скрипта configure (при помощи ./configure --help), если конечно configure существует. Удостовериться в том, что константы установились, и программа будет верно скомпилирована, можно по выводу утилиты make. В параметрах компилятора обязательно должны присутствовать заданные константы, например:

...
==> lib
/bin/sh ..//libtool --mode=compile cc -c -O2 -fno-strict-aliasing -pipe  -DDEBUGGING -DDEBUG_ROUTE -o asn1.lo asn1.c
...

Когда программа выполняется в изолированном окружении, нужно помнить, что для работы с сервисом syslogd у неё должен быть доступ к нему. Сервис syslogd(8) может принимать сообщения от программ через UNIX сокеты и через сеть. Обычно это /var/run/log, /var/run/logpriv и 514 UDP порт. Но syslogd может быть сконфигурирован для прослушивания дополнительных сокетов (подробнее об этом можно прочитать в руководстве).

Поиск в Интернете

После получения хоть какой-то отладочной информации из вывода программы или из системных журналов нужно проанализировать её. Если причина ошибки остаётся непонятной, то надо воспользоваться поиском в Интернете, в архивах специализированных списков рассылок и форумов. Поиск в Интернете, это вообще искусство, которым овладевают со временем, но чтобы им овладеть, нужно хотя бы пытаться найти ответ по возникшей проблеме, прежде чем просить помощи у разработчиков. Простейшее «copy/paste» сообщения об ошибке в поисковик очень часто помогает найти советы по решению проблемы и сокращает время на ожидание, когда какой-нибудь «гуру» в форуме найдёт свободную минутку для ответа на ваши вопросы. Из поисковиков, по личному опыту, всем рекомендую http://google.com, и как частные случаи – http://groups.google.com и http://google.com/bsd. Так же, неплохой сервис поиска по спискам рассылок проекта FreeBSD есть у Рамблера – http://freebsd.rambler.ru, и на официальном сайте проекта есть поисковая система, как по всему сайту в целом, так и по конференциям.

ldd(1) и nm(1)

Может случиться так, что программа не запускается из-за проблем с разделяемыми библиотеками. При проблемах с некорректными версиями библиотек, их отсутствием и другими ошибками, выдаваемыми компоновщиком (смотрите руководство ld(1)) при запуске программы, могут помочь утилиты ldd(1) и nm(1). Простой пример:

/libexec/ld-elf.so.1: Shared object "libglib-2.0.so.0" not found, required by "mc"

Увидев такое сообщение при запуске программы, можно сделать вывод что не найдена библиотека "libglib-2.0.so.0". В сообщении об ошибке явно говорится, какой библиотеки не хватает для запуска программы, узнать полный список необходимых библиотек можно при помощи команды ldd:

# ldd /usr/local/bin/mc
/usr/local/bin/mc:
        libintl.so.6 => /usr/local/lib/libintl.so.6 (0x2810d000)
        libglib-2.0.so.0 => not found (0x0)
        libiconv.so.3 => /usr/local/lib/libiconv.so.3 (0x28116000)
        libncurses.so.6 => /lib/libncurses.so.6 (0x28203000)
        libc.so.6 => /lib/libc.so.6 (0x28242000)

Это удобно, например, при создании изолированных окружений chroot(8) или jail(8). Решением проблемы с отсутствующими библиотеками естественно является установка соответствующего пакета или порта, либо копирование недостающих файлов. Чтобы узнать в каком порте или пакете находится нужная библиотека, можно посмотреть список зависимостей программы и переустановить зависимости:

# pkg_info -rx mc-4.6.1
Information for mc-4.6.1_2:

Depends on:
Dependency: pkgconfig-0.15.0_1
Dependency: perl-5.8.7_2
Dependency: libiconv-1.9.2_1
Dependency: expat-1.95.8
Dependency: gettext-0.13.1_1
Dependency: glib-2.6.6

Либо найти нужный порт в базе установленных пакетов, и определить к какому из них относится утерянная библиотека. Для этого можно воспользоваться командой pkg_info с ключом "–W" или grep:

# cd /var/db/pkg
# grep -r libglib-2.0 *
glib-2.6.6/+CONTENTS:lib/libglib-2.0.a
glib-2.6.6/+CONTENTS:lib/libglib-2.0.so
glib-2.6.6/+CONTENTS:lib/libglib-2.0.so.6

Пример другой проблемы – компоновщик не может обнаружить требуемых программе объектов по имеющейся символьной информации в какой-либо из библиотек. Такие ошибки сопровождаются сообщениями "Undefined symbol xxx", где ххх – имя какого-то объекта, например, функции. Чаще всего это связано с неверной версией имеющейся библиотеки и может быть результатом некорректного обновления пакетов/портов или ручным вмешательством (замена библиотек или исполняемых файлов). Когда программа самостоятельно загружает динамические библиотеки при помощи dlopen(3), то она в большинстве случаев знает, в какой библиотеке не хватает нужных ей объектов. Вот реальный пример подобной проблемы:

snmpd[69703]: lm_load: open /usr/local/lib/snmp_mibII.so: Undefined symbol "op_begemot_mibII"

С помощью команды nm(1) можно просмотреть символьную информацию и определить, какие объекты доступны в каждой конкретной библиотеке необходимой для запуска программы.

# nm /usr/local/lib/snmp_mibII.so | grep -A2 -B2 op_begemot_mibII  
00020ea0 b oidnum
00020f20 b oidnum
         U op_begemot_mibII
00005b07 T op_icmpstat
00006045 T op_ifentry

Флаг "U" перед именем op_begemot_mibII говорит о том, что этот объект неопределён (undefined) в этой библиотеке, хотя программа пытается его найти именно в ней. При беглом просмотре исходного кода (опять же при помощи grep) среди прочего вывода можно обнаружить:

# grep –r op_begemot_mibII *
snmp_mibII/mibII_begemot.c:op_begemot_mibII(struct snmp_context *ctx __unused, struct snmp_value *value,

# grep –A2 –B2 ^op_begemot_mibII snmp_mibII/mibII_begemot.c
*/
int
op_begemot_mibII(struct snmp_context *ctx __unused, struct snmp_value *value,
    u_int sub, u_int idx __unused, enum snmp_op op)
{

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

Реализация функции op_begemot_mibII в исходном тексте присутствует. Если реализация есть, почему её нет в собранной библиотеке? Разделяемые библиотеки, как и исполняемые модули, создаются из объектных файлов, которые являются результатом компиляции исходного кода в объектный. Просмотрев на список объектных файлов в каталоге после сборки, можно заметить, что для каждого файла с исходным текстом есть объектный файл:

# ls *.c *.o
mibII.c			mibII_ip.c		mibII_route.o
mibII.o			mibII_ip.o		mibII_tcp.c
mibII_begemot.c		mibII_ipaddr.c		mibII_tcp.o
mibII_ifmib.c		mibII_ipaddr.o		mibII_tree.c
mibII_ifmib.o		mibII_nettomedia.c	mibII_tree.o
mibII_ifstack.c		mibII_nettomedia.o	mibII_udp.c
mibII_ifstack.o		mibII_rcvaddr.c		mibII_udp.o
mibII_interfaces.c	mibII_rcvaddr.o
mibII_interfaces.o	mibII_route.c

Но у файла mibII_begemot.c, в котором находится реализация необходимой функции, его нет. Это значит, что файл не был скомпилирован. Для этого конкретного случая проблема решилась добавлением отсутствующего имени файла в Makefile и пересборкой библиотеки:

SRCS=	${MOD}_tree.c mibII.c mibII_ifmib.c mibII_ip.c			\
	mibII_interfaces.c mibII_ipaddr.c mibII_ifstack.c		\
	mibII_rcvaddr.c mibII_nettomedia.c mibII_tcp.c mibII_udp.c	\
	mibII_route.c mibII_begemot.c

При других подобных проблемах, решение может оказаться менее тривиальным. И в общем случае, узнать более точно, в какой библиотеке не хватает объектов – задача непростая. Можно попытаться поискать имя объекта в руководствах, которые обычно в большом количестве устанавливаются вместе с библиотеками, или в поисковике. Если результат будет удачным – переустановить необходимую библиотеку.

Трассировка

Другим чрезвычайно полезным методом является трассировка процессов. Трассирование системных вызовов во FreeBSD можно выполнять как минимум с помощью трёх различных утилит: ktrace, truss и strace. Последняя не входит в состав системы, но присутствует в дереве портов - devel/strace. С помощью трассировки можно определить последовательность действий программы и, возможно, обнаружить причину неправильной работы или сбоя. Особенно легко определить, какие файлы пытается открыть программа и увидеть результат этих попыток.

Простой пример, определим, в каком каталоге интерпретатор языка php пытается найти свой конфигурационный файл php.ini. Конечно, задача достаточно тривиально решается при помощи команд интерпретатора:

# echo "<? phpinfo(); ?>" | php | grep .ini
<tr><td class="e">Configuration File (php.ini) Path </td><td class="v">/usr/local/lib </td></tr>

Но, всё же, посмотрим, как эта задача решается при помощи трассировки:

# ktrace -t n /usr/local/bin/php
^C
# kdump | grep ini
 44172 php      NAMI  "./php-cgi.ini"
 44172 php      NAMI  "/usr/local/bin//php-cgi.ini"
 44172 php      NAMI  "/usr/local/lib/php-cgi.ini"
 44172 php      NAMI  "./php.ini"
 44172 php      NAMI  "/usr/local/bin//php.ini"
 44172 php      NAMI  "/usr/local/lib/php.ini"

Утилита ktrace(1) используется для управления специальной подсистемой ядра, отвечающей за трассирование системных вызовов. По-умолчанию, если не задан параметр командной строки "-f", ktrace создаёт в текущем рабочем каталоге файл ktrace.out с результатами своей работы. Вывод утилиты имеет нетекстовый формат, для расшифровки которого используется утилита kdump(1). В этом примере ktrace запускается с параметром "-t". Этот параметр указывает ядру точки трассирования находящиеся в коде ядра, при прохождении через которые генерируются события, отображаемые в журнале трассировки.

Как видите, трассировка дала несколько более широкое представление о действиях интерпретатора php. В этом примере показано только трассирование операций разрешения имён (точка трассирования "n"). Добавив к набору точек трассирования ещё и системные вызовы (точка "c") можно получить более интересную картину:

# ktrace -t nc /usr/local/bin/php
^C
# kdump | grep -A1 -B1 .ini
 47924 php      CALL  open(0xbfbff0a0,0,0x1b6)
 47924 php      NAMI  "./php-cgi.ini"
 47924 php      RET   open -1 errno 2 No such file or directory
 47924 php      CALL  open(0xbfbff0a0,0,0x1b6)
 47924 php      NAMI  "/usr/local/bin//php-cgi.ini"
 47924 php      RET   open -1 errno 2 No such file or directory
 47924 php      CALL  open(0xbfbff0a0,0,0x1b6)
 47924 php      NAMI  "/usr/local/lib/php-cgi.ini"
 47924 php      RET   open -1 errno 2 No such file or directory
 47924 php      CALL  open(0xbfbff0a0,0,0x1b6)
 47924 php      NAMI  "./php.ini"
 47924 php      RET   open -1 errno 2 No such file or directory
 47924 php      CALL  open(0xbfbff0a0,0,0x1b6)
 47924 php      NAMI  "/usr/local/bin//php.ini"
 47924 php      RET   open -1 errno 2 No such file or directory
 47924 php      CALL  open(0xbfbff0a0,0,0x1b6)
 47924 php      NAMI  "/usr/local/lib/php.ini"
 47924 php      RET   open 3

В этом выводе отображена последовательность попыток php открыть файлы конфигурации в различных каталогах. Так же видны коды возврата системного вызова open(2), по которым можно сделать вывод, что файл был найден только в каталоге /usr/local/lib/.

Выполнять трассировку при помощи ktrace(1) можно не только на этапе запуска процесса, при помощи опции "-p" есть возможность включить трассирование системных вызовов в уже исполняющемся процессе. Если вы не планируете завершать трассируемый процесс, то не забывайте отключить трассирование при помощи вызова ktrace с опцией "-С" или "-c". Программы, изменяющие свои реальный или эффективный идентификаторы пользователя или группы, могут трассироваться только суперпользователем. Для трассирования процессов потомков можно воспользоваться опцией "-d".

Ещё одна утилита для трассирования, входящая в базовую систему - это truss(1). Возможно, кому-то она покажется более дружественной, чем ktrace(1), к тому же её вывод не требует декодирования. Но для работы truss необходима смонтированная файловая система procfs(5), что не всегда приемлемо и возможно. Вот пример той же трассировки php при помощи truss:

# mount -t procfs procfs /proc
# truss -o truss.out php
^C
# grep .ini truss.out
open("./php-cgi.ini",0x0,0666)              ERR#2 'No such file or directory'
open("php/php-cgi.ini",0x0,0666)            ERR#2 'No such file or directory'
open("/usr/local/lib/php-cgi.ini",0x0,0666) ERR#2 'No such file or directory'
open("./php.ini",0x0,0666)                  ERR#2 'No such file or directory'
open("php/php.ini",0x0,0666)                ERR#2 'No such file or directory'
open("/usr/local/lib/php.ini",0x0,0666)     = 4 (0x4)
lstat("php.ini",0xbfbfe6f0)                 = 0 (0x0)

Как видите, в выводе truss параметры у системных вызовов представлены в более понятной форме. Это может оказаться полезным в ряде случаев. Например, при трассировании работающих с сетью программ в выводе truss легко читаются IP адреса. В общем, если вам недостаточно информации, выдаваемой одной из предложенных утилит, попробуйте выполнить трассировку другой. А так же, не забывайте, что в коллекции портов есть утилита strace [1].

Все три перечисленные утилиты предназначены для трассирования системных вызовов. В коллекции портов есть ещё одна утилита трассировки – sysutils/ltrace. Она предназначена для трассирования вызовов функций разделяемых библиотек, например, различные функции работы со строками (копирование, сравнение и т.п.). Подобная информация может оказаться полезной для более полного понимания того, как работает программа.

Другие утилиты

Отладка программ

И так, методы "косвенной отладки" не дали положительных результатов. Остаётся надеяться на помощь разработчиков, либо попытаться заняться отладкой самим.

Файлы .core

Какую же информацию могут получить пользователи с помощью отладчика, чтобы предоставить её разработчикам? Наибольший интерес для программиста представляет последовательность вызовов функций и состояние переменных перед тем как произошёл сбой. Эту информацию после краха программы можно получить из файла дампа памяти процесса, которые обычно называются по имени процесса с расширением core (смотрите руководство core(5)). Система создаёт файлы дампа при некорректных действиях программы, например, при обращении к некорректному адресу памяти, при вызове несуществующего системного вызова. На самом деле, создание дампа является действием по-умолчанию по обработке некоторых сигналов, таких как SIGSEGV, SIGSYS, SIGBUS и некоторых других. Подробное описание по сигналам и действиям по-умолчанию для них можно прочитать в руководстве signal(3).

Файлы дампа обычно сохраняются в рабочем каталоге процесса, но есть ряд случаев, когда дамп не будет сохранён. Во-первых, сохранение дампов можно отключить полностью через специальную переменную sysctl kern.coredump (1 – сохранять дампы, 0 – не сохранять). Во-вторых, если у пользователя, от имени которого работал процесс, нет прав для записи в рабочий каталог процесса, то файл дампа не будет создан. Например, многие сетевые серверы после запуска изменяют свой (или своих потомков) идентификатор пользователя на менее привилегированный, который может не иметь прав для записи в рабочий каталог процесса. Это ограничение можно обойти путём задания пути в переменной sysctl kern.corefile для сохранения дампов в каталог, доступный для записи всем пользователям. Через неё так же можно изменить и имя файла дампа, для того чтобы дампы не перезаписывались друг другом. Имя задаётся шаблоном при помощи специальных последовательностей: %N – имя файла работающего процесса; %P – идентификатор процесса; %U – идентификатор пользователя, от имени которого работал процесс. Например, создав каталог /var/coredumps c правами доступа 0777 можно определить значение sysctl kern.corefile="/var/coredumps/%U.%N.%P.core". В-третьих, на максимальный размер файла дампа может быть введено ограничение через переменную coredumpsize в login.conf(5). И, в-четвёртых, дамп не создаётся для процессов, которые изменяют свои реальный или эффективный идентификаторы пользователя или группы. Это поведение так же может быть изменено с помощью переменной sysctl kern.sugid_coredump.

Подытожив всё вышесказанное, настройм создание дампов памяти процессов в системе:

# cat >> /etc/sysctl.conf
kern.coredump=1
kern.corefile=/var/coredumps/%U.%N.%P.core
#Раскомментируйте следующую строку, если вы уверены в её необходимости
#kern.sugid_coredump=1
^D
# mkdir /var/coredumps
# chmod a=rwx /var/coredumps
# /etc/rc.d/sysctl restart

Для случаев, когда программа работает некорректно и не завершается с созданием дампа памяти, можно сделать дамп самостоятельно. Для этого используется утилита gcore(1). Чтобы gcore смогла создать файл дампа, ей необходима смонтированная файловая система procfs(5). В качестве параметров утилита принимает идентификатор исполняющегося процесса, так же можно указать путь к исполняемому файлу программы, чей дамп памяти вы хотите получить.

Подготовка к отладке

Для того чтобы из файла дампа памяти процесса можно было извлечь что-то полезное, необходимо скомпилировать программу с включением отладочной информации. Эта информация описывает типы данных всех переменных и функций, а так же соответствие строк исходного текста программы и адресов в исполняемом коде. Для включения отладочной информации при компиляции приложения нужно использовать ключ –g компилятора gcc. Так же, рекомендуется не использовать опции оптимизации, так как в некоторых случаях оптимизирующий компилятор может значительно изменить код программы, что усложняет отладку. Кроме того, по-умолчанию, устанавливая программы при помощи стандартного для FreeBSD средства – утилиты install(1), отладочная информация удаляется из бинарных файлов программ (смотрите руководства install(1) и strip(1)). Для системных программ, библиотек и большинства приложений из коллекции портов можно перед компиляцией задать переменную утилиты make(1) DEBUG_FLAGS. Например, для перекомпиляции с отладочной информацией и переустановки программы можно воспользоваться такой командой:

# make DEBUG_FLAGS=-g install

Для программ, не использующих системные make скрипты, можно попробовать установить переменные окруженя CFLAGS и CPPFLAGS в значение –g. Чтобы удостовериться в наличии ключа –g при компиляции просмотрите вывод команд сборки приложения.

Основы gdb

Во FreeBSD стандартный поставляемый с системой отладчик – это gdb (GNU Debugger). Я не планирую в этой статье научить вас в совершенстве владеть техникой отладки программ при помощи gdb. Одних теоретических знаний для этого недостаточно. В общем-то, и цель у нас немного другая – составление наиболее информативного отчёта о найденной проблеме. А для этого более чем достаточно знания десятка команд встроенного интерпретатора gdb, и, к тому же, может послужить толчком к дальнейшему изучению и совершенствованию техники отладки. Документации на эту тему в Сети (в том числе и на русском языке) предостаточно.

Отладчик gdb предоставляет консольный интерфейс с интерпретатором команд. Для первого ознакомления можно просто запустить его без параметров:

# gdb
GNU gdb 6.1.1 [FreeBSD]
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-marcel-freebsd".
(gdb)

Последняя строка – это приглашение к вводу команд интерпретатора. Команды могут автоматически дополняться по нажатию клавиши "Tab", для выхода нужно набрать команду quit. Для получения помощи можно ввести команду help. Справка состоит из нескольких разделов, каждый из которых содержит список возможных команд. Чтение справочной информации затруднений вызывать не должно, поэтому перейдём к делу.

Первое, что будет рассмотрено – это исследование файла дампа памяти при помощи отладчика. В качестве исходных данных имеется файл дампа test_prog.core от тестовой программы test_prog. Запускаем gdb и указываем два параметра: путь к файлу программы и файлу дампа.

# gdb –q /path/to/test_prog /path/to/test_prog.core
(no debugging symbols found)...Core was generated by `test_prog'.
Program terminated with signal 8, Arithmetic exception.
Reading symbols from /lib/libc.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /libexec/ld-elf.so.1...(no debugging symbols found)...done.
Loaded symbols for /libexec/ld-elf.so.1
#0  0x080484a1 in ?? ()

Ключ –q указывает gdb не выводить при запуске текст о лицензии. Надпись в первой строке: "no debugging symbols found", - говорит о том, что в указанной программе отсутствует отладочная информация. Таким образом, без пересборки программы из неё нельзя извлечь никакой полезной информации, кроме причины создания этого дампа. Надпись: "Program terminated with signal 8, Arithmetic exception.", - говорит о том, что программа была завершена из-за арифметического исключения, такого как деление на ноль, например. Перекомпилируем программу с включением отладочной информации:

# cd /path/to/src/test_prog/
# make DEBUG_FLAGS=-g
cc -O2 -fno-strict-aliasing -pipe  -g  -c test_prog.c
cc -O2 -fno-strict-aliasing -pipe  -g   -o test_prog test_prog.o

Как можно заметить, кроме ключа –g здесь присутствуют другие опции. Чаще всего эти опции заданы в /etc/make.conf, и используются по-умолчанию при компиляции всего ПО в системе. Если не изменять условий сборки программы, кроме добавления ключа –g, то с большой долей вероятности скомпилированный бинарный файл можно будет использовать без получения нового дампа памяти, что не всегда бывает простой задачей. Теперь используем новый бинарный файл для исследования дампа (переустановка программы необязательна):

# cd /path/to/src/test_prog/
# gdb –q ./test_prog /path/to/test_prog.core
warning: exec file is newer than core file.
Core was generated by `test_prog'.
Program terminated with signal 8, Arithmetic exception.
Reading symbols from /lib/libc.so.6...done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /libexec/ld-elf.so.1...done.
Loaded symbols for /libexec/ld-elf.so.1
#0  0x080484a1 in test_func (a=5, b=0) at test_prog.c:7
7               return (a/b);

Отладчик определил, что файл дампа был получен не от этого бинарного файла и выдал предупреждение: "exec file is newer than core file". Но на результатах это не сказалось, отладочная информация была обнаружена. Теперь приступим к исследованию дампа.

(gdb) bt
#0  0x080484a1 in test_func (a=5, b=0) at test_prog.c:7
#1  0x080484e1 in main (argc=1, argv=0xbfbfec4c) at test_prog.c:16

Команда bt (сокращение от команды backtrace) выводит в обратном порядке последовательность вызовов функий, по которой можно определить, что привело к сбою в программе. Последовательность вызовов формируется на основе информации из стека программы. При вызове любой функции в программе создаётся новый кадр стека (stack frame), в котором сохраняются параметры функции, адрес предыдущего кадра, адрес возврата из функции, локальные переменные и другая информация. Результат выполнения команды bt для дампа памяти тестовой программы содержит два кадра стека. Нулевой кадр – это тот, на котором выполнение программы завершилось, поэтому можно предположить, что сбой произошёл в нём, то есть в функции test_func. Нужно заметить, что это не всегда так. Функция test_func была вызвана из первого кадра стека (из функции main). В скобках после имени функции указаны имена аргументов со значениями, которые были переданы в функцию. В конце каждой строки с информацией о кадре стека указывается имя файла и номер строки, в которой находится вызов функции. Создавая отчёт о проблеме более предпочтительно использовать команду "bt full", которая в дополнение выводит значения локальных переменных функций:

(gdb) bt full
#0  0x080484a1 in test_func (a=5, b=0) at test_prog.c:7
No locals.
#1  0x080484e1 in main (argc=1, argv=0xbfbfec4c) at test_prog.c:16
        a = 5
        b = 0
        c = 672298569

Перед тем как сохранить вывод от команды bt full, просмотрите результат выполнения команды bt. В некоторых случаях отладчик не может получить значения локальных переменных, и в выводе bt full могут отсутствовать многие кадры стека. Поэтому, если число кадров стека в выводах этих команд отличается, то лучше сохранять результат команды bt, как наиболее полный.

Ещё одна полезная команда – frame. Без параметров она показывает текущий кадр стека. Если же указать номер кадра в качестве параметра, команда делает его текущим для более детального изучения:

(gdb) frame
#0  0x080484a1 in test_func (a=5, b=0) at test_prog.c:7
7               return (a/b);
(gdb) frame 1
#1  0x080484e1 in main (argc=1, argv=0xbfbfec4c) at test_prog.c:16
16              c = test_func(a, b);
(gdb) list
11      main(int argc, char *argv[])
12      {
13              int a, b, c;
14              a = 5;
15              b = 0;
16              c = test_func(a, b);
17
18              return (0);
19      }
20

Команда list отображает исходный текст программы. Без параметров отображается только 10 строк кода, включая строку, связанную с текущим кадром стека. Каждый последующий вызов команды list или простое нажатие enter (повтор предыдущей команды) отображают следующие 10 строк кода. В качестве параметров команда может принимать названия функций, номер строки файла или адрес соответсвующей функции. Несколько примеров:

(gdb) list *0x080484a1
0x80484a1 is in test_func (test_prog.c:7).
2
3
4       static int
5       test_func(int a, int b)
6       {
7               return (a/b);
8       }
9
10      int
11      main(int argc, char *argv[])
(gdb) list test_prog.c:18
13              int a, b, c;
14              a = 5;
15              b = 0;
16              c = test_func(a, b);
17
18              return (0);
19      }
20

И в заключение обзора gdb ещё несколько полезных команд: info, print, printf и set. Команда info имеет более десятка подкоманд и предназначена для показа информации о различных объектах отлаживаемой программы. Например, для просмотра информации об аргументах функции служит команда info args; локальных переменных – info locals; глобальных и статических переменных – info variables; информации о кадре стека – info frame.

Команда print служит для просмотра значений отдельных переменных, структур, адресов и другой информации. Для отображения информации в заданном формате можно использовать команду printf, форматная строка которой аналогична аргументу функции printf(3).

Команда set предназначена для изменения внутренних переменных окружения и настроек gdb. Например, при помощи команды set radix 16 можно изменить формат вывода числовых значений переменных на шестнадцатиричную систему счисления. Более подробно обо всех командах можно прочитать в справочной информации.

Отладка ядра

Теперь от пользовательских приложений перейдём к отладке проблем ядра операционной системы. Проблемы в ядре могут приводить к различным результатам, начиная с некорректной работы системы и заканчивая аварийными перезагрузками или зависаниями. Зависания в основном происходят либо из-за аппаратных проблем, либо из-за взаимных блокировок в ядре (deadlock). Аварийное завершение работы системы сопровождается "паникой ядра" (kernel panic), которая может приводить к дампу памяти ядра на жёсткий диск, либо переходу в режим отладки (в зависимости от настроек).

Дамп памяти ядра

Как и во многих других операционных системах, во FreeBSD есть механизм и инструменты сохранения памяти ядра для дальнейшего исследования. В первую очередь эта возможность предназначена для разработчиков. Поэтому в стабильных версиях системы эта возможность по-умолчанию отключена и паника ядра приводит к перезагрузке системы без сохранения образа памяти. Когда ваша система ведёт себя нестабильно и периодически паникует, нужно сконфигурировать её на сохранение дампа памяти ядра, с использованием которого в дальнейшем будет создан отчёт о проблеме. Начнём с установки переменных /etc/rc.conf(5).

Есть три переменные, которые отвечают за сохранение дампа памяти ядра: dumpdev, dumpdir и savecore_flags. Эти переменные влияют на работу двух стартовых скриптов системы: /etc/rc.d/dumpon и /etc/rc.d/savecore. Скрипт dumpon при помощи одноимённой утилиты настраивает систему на сохранение дампа (либо отключает сохранение, смотрите руководство dumpon(8)). Скрипт savecore во время загрузки (так же, при помощи одноимённой утилиты) извлекает из swap-партиции, либо из другого сконфигурированного устройства, сохранённый после паники дамп памяти ядра.

Опция dumpdev отвечает за выбор устройства, на которое будет записан дамп во время паники системы, и где будет производиться его поиск скриптом savecore. Значение этой переменной может быть "AUTO" или "NO". Значение "AUTO" определяет автоматический выбор устройства для дампа на основании списка /etc/fstab. Из списка выбирается первое, подходящее по размеру устройство, у которого в поле FStype находится значение "swap" или "dump". Размер этого устройства должен быть не меньше текущего размера оперативной памяти. Отчасти по этой причине размер swap-партиции рекомендуется делать не менее чем в два раза превышающим размер оперативной памяти. На самом деле, есть некоторые обходные пути для этого ограничения, о которых будет сказано чуть позже. Значение "NO" отключает настройку сохранения дампа памяти ядра на этапе загрузки системы. И, соответственно, скрипт savecore не производит поиск дампа. Любое другое значение этой переменной будет передано в качестве параметра утилите dumpon(8). Это можно использовать для определения конкретного устройства для сохранения дампа. Не стоит указывать здесь партиции, содержащие файловые системы, если не хотите потерять данные.

Переменная dumpdir служит для настройки пути к каталогу, в который скрипт savecore будет извлекать дампы. По-умолчанию это /var/crash. Свободного места на партиции с каталогом /var/crash должно быть достаточно для сохранения дампа, иначе в переменной dumpdir нужно указать другой путь.

Переменная savecore_flags задаёт набор опций для утилиты savecore(8). По-умолчанию, эта переменная не содержит никаких опций. Пожалуй, наиболее полезной опцией для использования во время загрузки является "-z". Она указывает savecore сохранять дамп памяти в сжатом виде. Это может пригодиться, если на разделе с dumpdir недостаточно места.

Теперь вернёмся к упоминаемым ранее обходным путям, позволяющим получить дамп памяти ядра, когда в наличии нет необходимого объёма свопа либо свободного места в dumpdir. Первый способ, который применим практически ко всем версиям системы – ограничение доступного объёма памяти при помощи переменных окружения загрузчика системы (loader(8)). За это отвечает переменная hw.physmem, которую можно определить вручную, как при загрузке из loader prompt, так и через /boot/loader.conf. Но этот метод применим только, когда у вас есть возможность перезагрузить систему и вам удаётся воспроизвести панику.

Начиная с версии системы 6.2, появилась возможность сохранять в дамп не всю память, а только реально используемые страницы памяти. Это управляется переменной sysctl debug.minidump. Если установить эту переменную в единицу, то система будет сохранять только используемые страницы памяти, что, в большинстве случаев, составляет гораздо меньший объём от всей доступной памяти. И, соответсвенно, для такого дампа требуется меньший объём свопа и свободного диского пространства.

Итак, для того, чтобы включить сохранение дампа памяти ядра при панике системы (при условии выполнения рекомендаций о размере своп-партиции), достаточно выполнить следующие действия:

# echo 'dumpdev="AUTO"' >> /etc/rc.conf
# /etc/rc.d/dumpon start
# echo debug.minidump=1 >> /etc/sysctl.conf
# sysctl debug.minidump=1 

Отладочные опции

Ядро FreeBSD содержит множество отладочного кода, который может быть активирован во время сборки. К тому же, как и в случае с пользовательскими программами, для исследования дампа памяти ядра отладчиком нужна отладочная информация. Рассмотрим отладочные опций ядра, которые могут оказаться наиболее полезными для создания отчёта о проблеме:

1  makeoptions	DEBUG=-g
2  options	DDB
3  options	GDB
4  options	KDB
5  options	KDB_TRACE
6  options	KDB_UNATTENDED
7  options	WITNESS
8  options	WITNESS_KDB
9  options	INVARIANTS
10 options	INVARIANT_SUPPORT
11 options	DEBUG_LOCKS
12 options	DIAGNOSTIC
13 options	BREAK_TO_DEBUGGER

makeoptions DEBUG=-g

Исследование дампа ядра

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

Последовательная консоль: загрузка и отладка

Особенности отладки модулей ядра

LOR’ы

Основы ddb

Что ещё можно попробовать?

Как правильно оформить отчёт о проблеме

Копирование содержимого консоли

ident, dmesg

Списки рассылок

Что делать с портами?

send-pr

diff и patch

Анонимный CVS и CVSWeb

Заключение