Отладка кода с помощью strace

This article is protected by the Open Publication License, V1.0 or later.
Copyright © 2005 by Red Hat, Inc.
Original article: http://www.redhat.com/magazine/010aug05/features/strace/

Перевод: © Иван Песин

Вы когда-нибудь ломали себе голову над поиском ошибки? Ошибки, которую вы не можете найти в исходном коде, но которая появляется с завидным постоянством после компиляции и запуска программы. Знакомьтесь: strace. strace — это утилита, которая позволяет вам трассировать системные вызовы и сигналы конкретной команды. Какой команды вы спросите? А какие у вас есть?

strace это свободное программное обеспечение, распространяемое под BSD-подобной лицензией. Изначально, утилита была написана Полом Краненбургом (Paul Kranenburg) для SunOS по мотивам другой утилиты SunOS trace. На Linux ее портировал Бранко Ланкестер (Branko Lankester), который, кроме того, реализовал поддержку в ядре. В 1993, Рик Слэдки (Rick Sladkey) объединил strace 2.5 для SunOS со вторым релизом strace для Linux, добавив при этом много возможностей от truss(1) из SVR4. В результате, появилась strace, которая работала на обеих платформах. Сегодня, strace поддерживается Уичертом Аккерманом (Wichert Akkerman) и Роландом МакГрасом (Roland McGrath). В Red Hat® Enterprise Linux® 4 включен strace версии 4.5.8, в Fedora™ Core 4 — версии 4.5.11, а на сайте SourceForge.net доступна версия 4.5.12. В этой статье рассматривается версия 4.5.12.

Установка strace

Если strace у вас не установлена, а работаете вы в Red Hat Enterprise Linux 4, то для ее установки выполните следующую команду от пользователя root:

up2date strace

Для установки strace в Fedora Core 4, выполните от пользователя root команду:

yum install strace

А можно просто собрать strace из исходников. Загрузите strace-4.5.12.tar.bz2 с ближайшего к вам зеркала сервера SourceForge.net.

Распакуйте содержимое файла strace-4.5.12.tar.bz2:

tar -xjvf strace-4.5.12.tar.bz2

Перейдите в каталог strace-4.5.12:

cd strace-4.5.12

И запустите configure:

./configure

Вы увидите следующий вывод:

checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for gawk... gawk
checking whether make sets $(MAKE)... yes
checking whether to enable maintainer-specific portions of Makefiles... no
checking build system type...
<snip>
checking whether sys_siglist is declared... yes
checking whether _sys_siglist is declared... yes
checking for perl... /usr/bin/perl
configure: creating ./config.status
config.status: creating Makefile
config.status: creating config.h
config.status: executing depfiles commands

Теперь запустите make:

make

Для установки strace выполните команду make install:

make install

При установке strace из исходников, по-умолчанию он будет находиться в каталоге /usr/local/bin/, но вы можете переопределить это значение с помощью опции --prefix=PATH скрипта configure (при установке из RPM-пакетов в системах Red Hat Enterprise Linux и Fedora Core strace будет находиться в каталоге /usr/bin/).

Трассировка helloworld

Работа strace заключается в перехвате и записи системных вызовов, выполненных процессом, а также полученных им сигналов. Для каждого системного вызова в стандартный файл ошибок, или в другой заданный файл, выводится его имя, аргументы и код возврата. Как на счет пары примеров?

Чтобы не усложнять работу strace, я использовал простейшую из возможных программ.

#include "stdio.h"

main()
{
printf("Hello World \n");
}

Быстренько компилируем:

cc -o helloworld helloworld.c

Готово! Команда ./helloworld теперь выдает:

Hello World

Выглядит воплощением простоты, и это действительно так и есть. Но давайте теперь запустим strace. strace протрассирует и выведет в stderr все системные вызовы и сигналы:

strace ./helloworld

Вывод команды strace ./helloworld

Примечание:
я добавил номера строк в вывод strace для лучшей читабельности, но в выводе strace их нет.

Первое, что мы видим — это execve. execve() выполняет программу, заданную именем файла. Далее, uname()получает имя и тип системы, которую я использовал для теста. После этого, наша программа запрашивает некоторое количество памяти (brk) и карты размещения разделяемых библиотек, необходимых для работы, например, динамический загрузчик и libc. Ко всей этой информации мы скоро вернемся, а пока посмотрим, что происходит дальше. Строка 23 вывода strace наконец нам открывает назначение нашей программы helloworld (если бы мы не видели исходного кода): вывести в файловый дескриптор 1 (stdout), сообщить количество байт в выводе и их значение. Не сложно догадаться, что применение strace может помочь вам отследить в программе конкретный вызов или сигнал. Чем же это полезно?  Если вы пишете программу чуть более сложную, чем helloworld.c и она сбоит, выдавая минимум, или вообще не выдавая информации о том, что с ней случилось, strace может вам помочь обнаружить что вызывает сбой в процессе работы.

Трассирование команды date

Давайте рассмотрим немного более сложный, но и более реальный пример. Программа date, помимо вывода даты и времени, выполняет массу различных системных вызовов. Запустите strace для команды date:

strace date

Вывод команды strace date

Мы видим вызовы execve(), uname() и все ту же информацию компоновщика и информацию о потоках (мы подразумеваем выполнение на одной и той же системе, что видно по вызову uname()), после чего начинается непосредственно работа. Строка 44 показывает нам, что была вызвана local.archive для определения нашей локали из списка доступных (строка 48). В строке 61, date вызывает  clock_gettime() для определения "реального времени", после чего выясняется наш часовой пояс (строка 62) с помощью вызова read в строке 66. Вся эта деятельность венчается вызовом write в строке 73 в том же формате, что и в нашем примере Hello World, за исключением того, что в этом случае date генерирует вывод, основываясь на конфигурации из файла, а не задаваемой пользователем.

Теперь поговорим про вывод компоновщика, потоках и памяти. Мы видим несколько ссылок на разные компоненты ld. ld это компоновщик GNU, а его работа заключается в сборке объектных и ресурсных файлов, перемещении их данных и разрешении символических указателей. Вызов ld, как правило, является последним шагом в процессе компиляции программы. Кроме того, мы видим много вызовов, выделяющих и освобождающих память. Каждый раз, когда мы что-либо читаем или записываем, мы видим вызовы mmap (в нашем случае mmap2) и munmap. Таким образом, наша программа размещает в памяти файлы и устройства. Это особенно удобно для отладки программ, интенсивно использующих память, потому что вы можете по записи и чтению из памяти точно видеть, где конкретно в программе написаны вызовы, выполняющие операции размещения в памяти. Подробная информация по mmap2 доступна на сайте linuxinfor.com.

Трассировка по идентификатору процесса

Если вы системный администратор, то, возможно, вы сидите и думаете: "Какое отношение это все имеет ко мне?". Поскольку вы отвечаете за надежное функционирование систем, то должны тестировать системное ПО до и после обновления. Часто системным администраторам приходится быть внештатными отладчиками: найти ошибку, отправить отчет и все такое. И в этом деле strace может сильно пригодиться.

Одной из наиболее мощных возможностей strace является присоединение к существующему процессу для трассировки его системных вызовов. Делается это с помощью ключа -p с указанием идентификатора процесса, к которому нужно подключиться. Например, однажды вы обнаружили новую версию Bash. Вы обновили свою систему, а на следующий день продолжили выполнять свои рутинные задачи. Но вдруг вы обнаруживаете, что новая версия ведет себя неожиданным образом.

Тогда, находясь в командном интерпретаторе, узнайте его идентификатор:

echo $$

Допустим, он равен 12307. В другом интерпретаторе запустите strace с ключом -p :

strace -p 12307

в ответ вы увидите:

Process 12307 attached - interrupt to quit
read(0,

Незаконченная функция read() ожидает ввода. Допустим, пользователь выполняет команду date. Первое, что мы видим, поскольку уже подсоединены к процессу командного интерпретатора, это вводимые символы:

Process 12307 attached - interrupt to quit
read(0, "d", 1) = 1
write(2, "d", 1) = 1
rt_sigprocmask(SIG_BLOCK, NULL, [], 8) = 0
read(0, "a", 1) = 1
write(2, "a", 1) = 1
rt_sigprocmask(SIG_BLOCK, NULL, [], 8) = 0
read(0, "t", 1) = 1
write(2, "t", 1) = 1
rt_sigprocmask(SIG_BLOCK, NULL, [], 8) = 0
read(0, "e", 1) = 1
write(2, "e", 1) = 1
rt_sigprocmask(SIG_BLOCK, NULL, [], 8) = 0
read(0,

И снова read() ожидает. Пользователь нажимает Enter.

Вывод после нажатия клавиши enter

Пропустив вызовы rt_ и ioctl, в строке 43 мы видим вывод нашего приглашения, записанного в сам командный интерпретатор в строке 18. Этот вызов write служит хорошим разделителем для всех остальных системных вызовов, включая недокументированные вызовы rt_. Вы можете попробовать это дома. Вы разберетесь. С помощью strace вы можете трассировать все типы ошибок.

Заключение

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

Мощь strace заключается в возможности обнаружения ошибок, даже когда исходный код недоступен или невозможна перекомпиляция. Это может помочь при исследовании работы программного обеспечения с закрытым исходным кодом. И хотя некоторые считают это "отрицательным" свойством, в действительности, оно может служить лишь для углубления понимания программистом/администратором работы таких программ. В конце концов, методы и стили программирования, использованные при написании программного, обеспечения остаются в тайне, не требуя дальнейшего судебного рассмотрения. strace-ируйте что-нибудь.

Об авторе

Мэт Фрей является системным администратором Unix/Linux и живет в Серверной Каролине. Он председатель общества системных администраторов Северной Каролины и активный член группы пользователей "Triangle Linux User Group". В свободное время любит ловить рыбу на мух и заниматься умственным кунг-фу.