Глава 4. Техника отладки

About: "По мотивам перевода" Linux Device Driver 2-nd edition.
Перевод: Князев Алексей knzsoft@mail.ru
Дата последнего изменения: 03.08.2004
Размещение: http://lug.kmv.ru/index.php?page=knz_ldd2

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

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

Отладка через печать

Наиболее общей техникой отладки является печать. В приложениях пользовательского уровня, такая техника реализуется через вызов функции printf() в определенных местах программы. При отладке кода ядра используется аналогичная функция printk(), доступная в коде ядра.

Функция printk()

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

printk(KERN_DEBUG "Here I am: %s:%i\n", __FILE__, __LINE_&_);
printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);

В оригинальном английском тексте используется термин loglevel для указания приоритета, передаваемого в функцию printk(). Возможно, термин “приоритет” не совсем точно отражает смысл оригинального термина. В любом случае, необходимо иметь в виду эту информацию при дальнейшем чтении этой книги и просмотре источников ядра.

В заголовочном файле <linux/kernel.h> определены восемь возможных уровней приоритета.

KERN_EMERG
Используется для “непредвиденных”
сообщений, особенно для тех, которые предшествуют краху.
KERN_ALERT
Ситуация, требующая немедленного вмешательства.
KERN_CRIT
Критическая ситуация. Часто связанная с
серьезными неисправностями оборудования или программным
проблемам.
KERN_ERR
Используется для отчета об условиях возникшей
ошибки. Драйвера устройств часто используют KERN_ERR для
сообщениях о проблемах, связанных с оборудованием.
KERN_WARNING
Сообщение о проблемных ситуациях, которые сами
по себе не создают проблем для системы.
KERN_NOTICE
Ситуация не проблемная, но заслуживает
внимания. Часто используется в сообщениях системы безопасности.
KERN_INFO
Информационное сообщение. Используется в
сообщениях о найденном оборудовании при загрузке драйверов.
KERN_DEBUG
Используется для отладочных сообщений.

Каждая строка (при подстановке макроопределения) представляет целое число в угловых скобках. Диапазон изменения целого числа от 0 до 7. Причем, чем меньше значение тем выше приоритет.

Если приоритет не указан явно при вызове функции printk(), то используется значение приоритета принятое по умолчанию — DEFAULT_MESSAGE_LOGLEVEL. Эта величина определена в <kernel/printk.c> как целое. Значение приоритета, используемого по умолчанию, изменялось несколько раз за время разработки Linux ядра.

В зависимости от уровня приоритета, ядро может выводить сообщения на текущую консоль (текстовый терминал), на принтер или в файл. Если значение приоритета меньше значения целой переменной console_loglevel, то сообщение будет изображено на консоли. Если в системе запущены оба демона klogd и syslogd, то сообщения ядра будут выводиться в файл /var/log/messages независимо от значения console_loglevel (вообще, см. конфигурацию syslogd). Если klogd не запущен, то сообщение не попадет в пространство пользователя до тех пор пока не будет обращения к файлу /proc/kmsg.

Значение переменной console_loglevel инициализируется значением DEFAULT_CONSOLE_LOGLEVEL и может быть изменена с помощью системного вызова sys_syslog(). Другим способом задания этого значения является использование ключа -c при запуске klogd, как описано в man-руководстве к klogd. Т.е. для изменения текущего значения, вы должны сначала убить процесс демона klogd, и, затем, перезапустить его с использованием опции -c. В качестве альтернативы, вы можете написать программу для изменения приоритета консоли (console_loglevel). Такую программу вы можете найти в misc-progs/setlevel.c на FTP сайте O’Reilly. Новый уровень определяется целым числом в интервале от 1 до 8. Если вы установите его в 1, то только сообщения уровня 0 (KERN_EMERG) достигнут консоли. Если установите новый уровень в значение 0, то все сообщения, включая отладочные, будут выведены в консоль.

Вероятно, вы захотите понизить приоритет, если вы ожидаете, или экспериментируете с обвалами ядра (см. “Debugging System Faults” позднее в этой главе), и не хотите “засорять” консоль избыточной информацией. С другой стороны, возможно, вы захотите поднять приоритет консоли, если возникнет необходимость вывода в консоль отладочной информации. Особый взгляд на изменение приоритета может понадобиться при удаленной разработке кода ядра.

Начиная с версии 2.1.31 имеется возможность чтения и модификации уровня приоритета консоли используя текстовый файл /proc/sys/kernel/printk. Файл состоит из четырех целых значений. Вы можете быть заинтересованы в первых двух: текущий уровень привилегий консоли и уровень привилегий по умолчанию для сообщений. Так, работая с современными ядрами, вы можете добиться вывода всех сообщений в консоль простой командой:

#echo 8 > /proc/sys/kernel/printk

Работая с ядром 2.0 вам понадобятся специальные инструменты изменения уровня.

Теперь, вам должно быть понятно, почему в примере hello.c используется уровень привилегий <1>. Все сообщения этого уровня будут выведены в консоль.

Linux предлагает некоторую гибкость для политики логов в консоли, позволяя посылать сообщения в заданную виртуальную консоль (если консоль запущена на текстовом экране). По умолчанию, консолью называется текущий виртуальный терминал. Для выбора другого виртуального терминала для приема сообщений, вы можете вызвать ioctl(TIOCLINUX) на любое консольное устройство. Следующая программа позволяет выбрать консоль, которая примет сообщения ядра. Программа носит название setconsole и должна быть запущена суперпользователем. Код программы доступен для скачивания на FTP O’Reilly в каталоге misc-progs.

int main(int argc, char **argv)
{
    char bytes[2] = {11,0}; /* 11 is the TIOCLINUX cmd number */

    if (argc==2) bytes[1] = atoi(argv[1]); /* the chosen console */
    else {
        fprintf(stderr, "%s: need a single arg\n", argv[0]);
	exit(1);
    };
    if (ioctl(STDIN_FILENO, TIOCLINUX, bytes) < 0) {    /* use stdin */
        fprintf(stderr, "%s: ioctl(stdin, TIOCLINUX): %s\n",
                argv[0], strerror(errno));
        exit(1);
    };
    exit(0);
}

Программа setconsole использует специальную команду TIOCLINUX для системного вызова ioctl(), которая реализует Linux-специфичные функции. При использовании команды TIOCLINUX, вы передаете дополнительный аргумент, который представляет собой указатель на массив байтов. Первый байт массива представляет собой число, определяющее номер требуемой подкоманды. Следующие байты зависят от подкоманды. В программе setconsole используется подкоманда 11, а следующий байт (сохраненный в bytes[1]) определяет номер виртуальной консоли. Полное описание команды TIOCLINUX вы найдете в каталоге источников ядра, в подкаталоге drivers/char/tty_io.c.

Как работает система логирования сообщений

Функция printk() пишет сообщение в круговой буфер длиной LOG_BUF_LEN байт, определенный в kernel/printk.c. Затем, просыпается один из процессов ожидающий сообщение. Т.е. это либо процесс, который спит в системном вызове syslog, либо процесс, который читает /proc/kmsg. Эти два интерфейса к log-машине практически эквивалентны, но, заметьте, что чтение из /proc/kmsg “съедает” данные из log-буфера, в то время как системный вызов syslog может опционально вернуть данные в буфер пока они нужны другим процессам. В общем, чтение файла из /proc проще, поэтому оно используется в klogd по умолчанию.

Если вам случалось читать сообщения ядра вручную, после остановки klogd, вы должны были заметить что файл в /proc организован в виде очереди FIFO. Причем, читающий процесс блокируется для ожидания следующих данных. Таким образом, вы не можете прочитать сообщения этим способом, если klogd уже занят чтением буфера, потому что, иначе, это приведет к проблеме конкуренции доступа к данным.

Если круговой буфер переполняется, то функция printk начинает затирать наиболее старые данные буфера. Таким образом, старые данные теряются. Эта проблема не существенна, по сравнению с преимуществами организации кругового буфера. Например, круговой буфер позволяет системе работать даже, если нет процесса, обрабатывающего этот буфер. Другом преимуществом Linux подхода к реализации этого механизма заключается в том, что функция printk() может быть вызвана откуда угодно, даже из обработчика прерываний, без ограничений на количество выводимых данных. Единственным недостатком является возможность потери данных.

Если процесс klogd запущен, то он извлекает из буфера сообщения ядра и диспетчеризует их согласно настройкам в файле /etc/syslog.conf. syslogd различает события согласно их принадлежности и приоритету. Допустимые значения для принадлежности и приоритета определены в заголовочном файле <sys/syslog.h>. Сообщения ядра логируются согласно принадлежности LOG_KERN, и приоритету заданному в printk(). Например, LOG_ERR используется для сообщений KERN_ERR. Если klogd не запущен, то данные остаются в круговом буфере, либо пока их кто-нибудь не прочитает, либо пока буфер не переполнится.

Если вы хотите избежать затирания ваших системных логов сообщениями из ядра, вы можете определить файл записи сообщений, либо через опцию -f (“file”) при запуске процесса klogd, либо внесением изменений в /etc/syslog.conf. Также, можно реализовать ручной вывод сообщений убив процесс klogd, и используя команду cat /proc/kmsg на неиспользуем терминале xterm. Например, установите setlevel 8 и setconsole 10 для использования 10-го терминала для вывода сообщений.

Включение/выключение отладочных сообщений

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

  1. Каждый оператор печати может быть разрешен или запрещен удалением или добавлением одного символа в макроимени.
  2. Все сообщения могут быть запрещены сразу, изменением значения переменной CFLAGS перед компиляцией
  3. Один и тот же оператор печати может быть использован как в коде ядра, так и в коде уровня пользователя, поэтому драйвер и пользовательская программа может управляться
    одинаково.

Следующий фрагмент кода, взятый из заголовочного файла scull.h, реализует эти характеристики.

#undef PDEBUG             /* undef it, just in case */
#ifdef SCULL_DEBUG
#  ifdef __KERNEL__
     /* This one if debugging is on, and kernel space */
#    define PDEBUG(fmt, args...) printk( KERN_DEBUG "scull: " fmt, ## args)
#  else
     /* This one for user space */
#    define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args)
#  endif
#else
#  define PDEBUG(fmt, args...) /* not debugging: nothing */
#endif

#undef PDEBUGG
#define PDEBUGG(fmt, args...) /* nothing: it's a placeholder */

Значение символа PDEBUG зависит от того, определен или или нет символ SCULL_DEBUG, и в зависимости от того, в каком окружении исполняется код. При использовании в ядре, вызывается функция printk(), при использовании в адресном пространстве пользователя вызывается функция fprintf() из библиотеки libc. С другой стороны, символ PDEBUGG не делает ничего. Удалив последний символ “G” в этом определении мы “выключим” все операторы печати, построенные на макроопределении PDEBUG.

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

# Comment/uncomment the following line to disable/enable debugging
DEBUG = y

# Add your debugging flag (or not) to CFLAGS
ifeq ($(DEBUG),y)
  DEBFLAGS = -O -g -DSCULL_DEBUG # "-O" is needed to expand inlines
else
  DEBFLAGS = -O2
endif

CFLAGS += $(DEBFLAGS)

Макроопределения, показанные в этом разделе зависят от gcc расширения к ANSI C препроцессору, который поддерживает макросы с переменным числом аргументов. Эта особенность gcc не должна вызывать проблему, потому что свойства ядра напрямую связаны с характеристиками gcc. Кроме того, GNU make было использовано для сборки ядра, и будет использовано для сборки вашего модуля.

Если вы хорошо знакомы с препроцессором языка Си, вы можете расширить данные определения до реализации концепции “уровни отладки”, определяя различные целые значения или битовые маски каждому уровню.

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

Получение отладочной информации по запросу

В предыдущем разделе мы рассказали о том, как работает, и как используется функция printk(). Пока, мы не касались недостатков такого метода отладки.

Частое использование printk() может значительно замедлить систему, потому что syslogd поддерживает постоянную синхронизацию своих выходных файлов. Таким образом, каждый вызов printk() приводит к дисковой операции. С одной стороны, такая реализация syslogd правильна, потому что позволяет сбросить на диск всю информацию, предшествующую, например, краху системы. Изменить систему логирования можно используя знак минуса в качестве префикса перед именами лог-файлов в /etc/syslog.conf. В этом случае сброс циклического буфера сообщений на диск будет производиться по требованию и по возможности. Подробнее об этом можно прочитать в man-странице по syslog.conf(5). В качестве альтернативы можно посоветовать отказ от klogd. Как уже говорилось ранее, вы можете использовать команду cat /proc/kmsg.

Наиболее часто, лучшим способом получения информации, является запрос требуемой информации у системы тогда, когда она необходима, вместо непрерывного логирования данных. Unix системы обеспечивают множество инструментов для получения системной информации: ps, netstat, vmstat и пр.

Разработчикам драйверов доступны два основных способа запроса информации из драйвера: создание файла в файловой системе /proc и использование метода ioctl() драйвера. Вы можете использовать devfs как альтернативу /proc, но /proc является более простым способом для получения информации о драйвере.

Использование файловой системы /proc

Файловая система /proc представляет собой специальную, программно-реализованную файловую систему, которая связана с функциями ядра, которые генерируют содержание файла на лету, во время чтения файла. Мы уже знакомы с некоторыми файлами этой файловой системы, например, таким как /proc/modules, который возвращает список загруженных, в данный момент, модулей.

Файловая система /proc широко используется в Linux-системе. Многие утилиты современных Linux-дистрибутивов, такие как ps, top и uptime получают свою информацию из /proc. Некоторые драйвера устройств, также используют /proc для передачи информации в пространство пользователя. Файловая система /proc является динамической системой, и ваш модуль может добавлять и удалять файловый элемент из этой системы во время своей работы.

Реализация полнофункционального элемента в /proc может оказаться очень сложной задачей, поэтому, часто, реализовывают элементы, доступные только для чтения. В этом разделе мы опишем только этот случай реализации. Те кто интересуется чем-то более сложным могут познакомиться с источниками ядра, откуда можно почерпнуть полную картину реализации.

Все модули, которые работают с файловой системой /proc должны включать заголовочный файл <linux/proc_fs.h>, где определены соответствующие функции.

Для создания файла в файловой системе /proc, который будет доступен только для чтения, ваш драйвер должен реализовать функцию, которая будет наполнять файл содержанием во время его чтения. Когда некоторый процесс читает файл (используя системный вызов read()), запрос достигает вашего модуля через один из двух возможных интерфейсов, в зависимости от способа регистрации файла. Мы опустим вопросы регистрации для более позднего обсуждения, а сейчас, сразу рассмотрим интерфейсы чтения.

В обоих случаях, ядро распределяет страницу памяти, размером PAGE_SIZE байт, куда драйвер будет писать данные, передаваемые в пользовательское пространство.

Рекомендуемым интерфейсом является интерфейс read_proc, но, также, существует старый интерфейс, имеющий название get_info.

int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);

Параметр page представляет собой указатель на буфер, куда вы пишите свои данные. О параметре start, используемом функцией для указания размещения данных на странице, мы поговорим позднее. Параметры offset и count имеют то же значение, что и в реализации метода read(). Аргумент eof указывает на целое число, которое должно быть установлено драйвером, для сообщения о том, что данные закончились. Параметр data представляет собой драйверо-зависимый указатель, который может быть использован для внутренних целей разработчика драйвера. Мы будем встречаться с подобными указателями на всем протяжении книги. Они представляют “объекты” вовлеченные в это действие, и похожи по смыслу на указатель this в языке Си++. Данная функция доступна в ядре 2.4. Для использования ее в версии ядра 2.2 необходимо воспользоваться нашим заголовочным файлом sysdep.h.

int (*get_info)(char *page, char **start, off_t offset, int count);

get_info представляет собой старый интерфейс, используемый при чтении из файла файловой системы /proc. Все аргументы имеют те же самые значения, что и для интерфейса read_proc. Недостатком этого интерфейса является отсутствие указателя-индикатора eof (“end-of-file”) и указателя data, приносящего в интерфейс объектно-ориентированный “привкус”. Эта функция доступна во всех, описываемых нами версиях ядра Linux (хотя и имеет некоторые дополнительные неиспользуемые аргументы в реализации ядра 2.0).

Обе функции должны возвращать количество байт данных действительно размещенных в буфере страницы. Другими возвращающими параметрами являются параметры *eof и *start. Параметр eof это простой флаг окончания данных. Более сложным является параметр start.

Главной проблемой обычной реализации пользовательского дополнения в файловую систему /proc, является использование одной страницы памяти для передачи данных. Это ограничивает размер файла четырьмя килобайтами (зависит от аппаратной платформы). Аргумент start нужен для поддержки больших файлов данных, и может быть проигнорирован.

Если ваша функция proc_read() не устанавливает указатель *start (т.е. start равен NULL), то ядро предполагает, что параметр offset был проигнорирован, и что страница данных целиком занята файлом, который вы хотите возвратить в пространство пользователя. С другой стороны, если вам нужно передать файл большего размера, состоящий из кусков, то вы можете установить *start равным page, чтобы вызывающая программа знала, что новые данные расположены начиная с начала буфера. Конечно, вы должны пропустить первые offset байт данных, которые уже были возвращены в предыдущем вызове.

Существует еще одна важная проблема, связанная с чтением файла из /proc, хорошо решаемая с помощью указателя start. Иногда, ASCII представление структуры данных ядра изменяется во время чтения, т.е. между последовательностью вызовов чтения нескольких страниц данных. В этом случае, читаемая программа может получить некорректные данные. Если *start установить в маленькое целое значение, то вызывающая программа будет использовать это соглашение для инкрементирования filp->f_pos независимо от количества переданных данных. Таким образом, f_pos используется как внутренний счетчик запросов к процедуре read_proc() или get_info(). Если, например, ваша функция read_proc() возвращает информацию о большом массиве структур, и пять таких структур уже возвращено при первом обращении, то start должен быть установлен в значение 5. При следующем обращении это значение будет передано как значение offset. Таким образом драйвер начнет передавать данные начиная с шестой структуры массива. Этот механизм определен авторами как “hack”. Более подробное ознакомление с этим механизмом можно извлечь из fs/proc/generic.c.

Приведем пример простой реализации read_proc() в драйвере scull:

int scull_read_procmem(char *buf, char **start, off_t offset,
                   int count, int *eof, void *data)
{
    int i, j, len = 0;
    int limit = count - 80; /* Don't print more than this */

    for (i = 0; i < scull_nr_devs && len <= limit; i++) {
        Scull_Dev *d = &scull_devices[i];
        if (down_interruptible(&d->sem))
                return -ERESTARTSYS;
        len += sprintf(buf+len,"\nDevice %i: qset %i, q %i, sz %li\n",
                       i, d->qset, d->quantum, d->size);
        for (; d && len <= limit; d = d->next) { /* scan the list */
            len += sprintf(buf+len, "  item at %p, qset at %p\n", d, 
                                    d->data);
            if (d->data && !d->next) /* dump only the last item 
                                                    - save space */
                for (j = 0; j < d->qset; j++) {
                    if (d->data[j])
                        len += sprintf(buf+len,"    % 4i: %8p\n",
                                                    j,d->data[j]);
                }
        }
        up(&scull_devices[i].sem);
    }
    *eof = 1;
    return len;
}

Это действительно типичный случай реализации read_proc(). В нем предполагается, что нет необходимости в передаче более чем одной страницы данных, и игнорирует значения start и offset. Однако, при такой реализации, необходимо быть осторожным, чтобы не допустить переполнения буфера.

Функция использующая get_info() выглядит аналогично приведенному примеру, за исключением того, что последние два аргумента должны быть опущены. Условие end-of-file, в этом случае реализуется за счет того, что возвращаемое функцией значение меньше чем ожидаемое count.

Теперь, когда мы имеем реализацию функции read_proc() нам необходимо зарегистрировать элемент в иерархии файловой системы /proc. Возможны два способа такой регистрации, в зависимости от версии ядра, в котором вы собираетесь использовать драйвер. Простейший способ, доступный только в ядре 2.4 (в ядре 2.2 придется воспользоваться нашим заголовочным файлом sysdep.h) заключается в простом вызове функции create_proc_read_entry(). Приведем пример регистрации элемента /proc/scullmem для драйвера scull.

create_proc_read_entry("scullmem", 
                       0    /* default mode */,
                       NULL /* parent dir */, 
                       scull_read_procmem, 
                       NULL /* client data */);

Как показано, аргументы передаваемые в функцию представляют собой имя файла в /proc, права доступа к файлу (значение 0 – особый случай – умолчание – всеобщий доступ на чтение), указатель proc_dir_entry, указывающий на родительский каталог для файла (мы используем NULL для размещения драйвера прямо в /proc), указатель на функцию read_proc(), и указатель на произвольные данные, которые будут переданы в функцию read_proc().

proc_dir_entry может быть использован для создания подкаталога в иерархии /proc. Заметьте, что файловый элемент нашего интерфейса может быть размещен в подкаталоге /proc просто указанием имени подкаталога как часть имени файла, при условии, что этот подкаталог уже существует. Например, существующее соглашение, говорит, что элементы в /proc связанные с драйвером устройства должны находится в подкаталоге driver/. Таким образом, драйвер scull должен размещать свой файловый элемент в /proc под именем driver/scullmem.

При выгрузке модуля, его элементы в /proc должны быть удалены. Функция remove_proc_entry() используется для удаления элементов созданных вызовом create_proc_read_entry().

remove_proc_entry("scullmem", NULL /* parent dir */);

Альтернативным методом создания элемента в /proc является создания и инициализации структуры proc_dir_entry и передача ее в proc_register_dynamic() (версия ядра 2.0) или в proc_register() (версия 2.2, в которая определяет динамическую природу файла, если элемент inode в структуре равен 0). В качестве примера приведем код, который используется драйвером scull при компиляции в ядре 2.0:

static int scull_get_info(char *buf, char **start, off_t offset,
                int len, int unused)
{
    int eof = 0;
    return scull_read_procmem (buf, start, offset, len, &eof, NULL);
}

struct proc_dir_entry scull_proc_entry = {
        namelen:    8,
        name:       "scullmem",
        mode:       S_IFREG | S_IRUGO,
        nlink:      1,
        get_info:   scull_get_info,
};

static void scull_create_proc()
{
    proc_register_dynamic(&proc_root, &scull_proc_entry);
}

static void scull_remove_proc()
{
    proc_unregister(&proc_root, scull_proc_entry.low_ino);
}

В этом коде определяется функция использующая интерфейс get_info() и заполняется структура proc_dir_entry, которая регистрируется в файловой системе.

В коде обеспечивается совместимость между версиями 2.0 и 2.2, с минимум поддержки макроопределений из нашего заголовочного файла sysdep.h. Интерфейс get_proc() используется по причине того, что в ядре 2.0 не поддерживается read_proc(). Используя условную компиляцию можно поддержать read_proc() для ядра 2.2, но большой выгоды от этого, здесь, не будет.

Метод ioctl()

Мы подробно рассмотрим работу с методом ioctl() в следующей главе. Данный метод вызывается через соответствующий системный вызов и работает через файловый дискриптор. В него передается целое число, символизирующее запрашиваемую команду, и (опционально, в зависимости от команды) другие аргументы, обычно – указатель.

В качестве альтернативы использования файловой системы /proc вы можете реализовать несколько ioctl-команд для получения отладочной информации из драйвера. С помощью этих команд можно будет скопировать структуры данных из драйвера в пространство пользователя, где ваше front-end приложение отобразит их в нужном формате.

Использование ioctl несколько сложнее использования файловой системы /proc, потому что вам нужна дополнительная программа реализующая системные вызовы ioctl() и отображающая результаты. Эта программа должна быть написана, скомпилирована для использования с тестируемым модулем. С другой стороны поддержка ioctl() в драйвере проще поддержки файловой системы /proc.

Однако, иногда, использование ioctl предпочтительнее, потому что это работает быстрее, чем чтение файлов из /proc. Если необходимо выполнять обработку полученных из драйвера данных прежде, чем они будут выведены на экран, то получение данных в бинарной форме более эффективна, нежели их извлечение из текстового файла /proc. Кроме того, получение данных через ioctl() не связано со страничной фрагментацией.

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

Отладка с использованием strace – трассировка системных вызовов

Некоторые проблемы могут быть отслежены во время наблюдения работы приложения в пользовательском пространстве. Кроме того, только исходя из наблюдения за работой драйвер можно построить заключение о его корректной работе. Например, мы можем получить уверенность о правильной работе scull, после того, как пронаблюдаем за тем как он справляется с передачей разного объема данных.

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

Команда strace представляет собой мощный инструмент, который показывает системные вызовы, выполняемые программой, работающей в пространстве пользователя. Команда может показать не только названия системных вызовов, но и аргументы вызова и возвращаемые значения в символической форме. При ошибочном завершении функции отображается и символическое значение ошибки (например, ENOMEM), и соответствующую строку (Out of memory). Команда strace имеет множество опциональных параметров, принимаемых через командную строку. Наиболее полезными из которых являются -t для отображения времени вызова функции, -T для отображения времени, потраченного на выполнение функции, -e для ограничения типов отображаемых вызовов, и -o для перенаправления вывода в файл. По умолчанию, утилита strace выполняет вывод информации в стандартный поток ошибок stderr.

Команда strace принимает информацию непосредственно из ядра. Это означает, что программа будет трассироваться независимо от того, была ли она скомпилирована с поддержкой отладочной информации (опция -o в gcc), или отладочная информация была удалена из файла после компиляции. Вы можете подключить strace-трассировку к уже запущенному процессу.

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

Например, следующий снимок экрана показывает несколько последних строк запуска команды strace ls /dev > /dev/scull0

[...]
open("/dev", O_RDONLY|O_NONBLOCK)     = 4
fcntl(4, F_SETFD, FD_CLOEXEC)         = 0
brk(0x8055000)                        = 0x8055000
lseek(4, 0, SEEK_CUR)                 = 0
getdents(4, /* 70 entries */, 3933)   = 1260
[...]
getdents(4, /* 0 entries */, 3933)    = 0
close(4)                              = 0
fstat(1, {st_mode=S_IFCHR|0664, st_rdev=makedev(253, 0), ...}) = 0
ioctl(1, TCGETS, 0xbffffa5c)          = -1 ENOTTY (Inappropriate ioctl
                                                     for device)
write(1, "MAKEDEV\natibm\naudio\naudio1\na"..., 4096) = 4000
write(1, "d2\nsdd3\nsdd4\nsdd5\nsdd6\nsdd7"..., 96) = 96
write(1, "4\nsde5\nsde6\nsde7\nsde8\nsde9\n"..., 3325) = 3325
close(1)                              = 0
_exit(0)                              = ?

Мы можем видеть, что первый вызов write() после того, как команда ls завершила обзор заданного каталога, выполнил запись 4000 байт данных. Т.к. требуется запись большего объема данных, то вызов write() повторяется. Вспомните, что наша реализация метода write() в scull позволяет записать не более одного кванта данных за одно обращение, поэтому, мы и наблюдаем многократный вызов write(). Когда весь объем данных записан, программа нормально завершается.

В следующем примере мы произведем чтение из драйвера scull, используя команду wc.

[...]
open("/dev/scull0", O_RDONLY)           = 4
fstat(4, {st_mode=S_IFCHR|0664, st_rdev=makedev(253, 0), ...}) = 0
read(4, "MAKEDEV\natibm\naudio\naudio1\na"..., 16384) = 4000
read(4, "d2\nsdd3\nsdd4\nsdd5\nsdd6\nsdd7"..., 16384) = 3421
read(4, "", 16384)                      = 0
fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(3, 7), ...}) = 0
ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0
write(1, "   7421 /dev/scull0\n", 20)   = 20
close(4)                                = 0
_exit(0)                                = ?

Как и ожидалось, метод read() может передать не более четырех тысяч байт за одно обращение, а общее количество данных соответствует количеству записанных байт из предыдущего примера. Интересно заметить, как реализованы повторные вызовы в этом примере по сравнению с предыдущей трассировкой. Команда wc оптимизирована для быстрого чтения в обход стандартной библиотеки, и пытается прочесть большее количество данных за один системный вызов. Вы можете видеть из строк /read/, что команда wc пытается прочесть 16КБт данных за одно обращение.

Знатоки Linux могут извлечь много полезной информации анализируя выход команды strace. Вы можете погасить вывод части символов, или ограничить вывод определенными методами (open, read, и пр.).

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

Отладка при возникновении “System Fault”

Даже после того, как вы использовали все возможности отладки и мониторинга, некоторые ошибки могут остаться в драйвере и приводить к системным ошибкам (“System fault”). При возникновении такой ситуации важно собрать максимум информации для анализа возникшей проблемы.

Заметьте, что “fault” не то же самое, что “panic”. Код ядра Linux достаточно четко распознает большинство ошибок. “fault” обычно означает разрушение текущего процесса во время работы системы. “panic” может возникнуть как результат того, что ошибка возникла за пределами контекста процесса, или при повреждении некоторой жизненно важной части системы. Когда проблема возникает по вине драйвера, то обычно это приводит к внезапной смерти процесса использовавшего в этот момент драйвер.

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

Сообщение “oops”

Большая часть ошибок связана с разыменованием указателей, содержащих NULL или некоторое некорректное значение. Обычным результатом такой ошибки является сообщение “oops”.

Любой адрес используемый процессором представляет собой виртуальный адрес, отображаемый в физическое адресное пространство через сложную структуру, называемую таблицей страниц (см. “Page Tables” в главе 13 “mmap and DMA”). При разыменовании некорректного указателя, страничный механизм определяет ошибку в отображении указателя в физическое адресное пространство и процессор вызывает исключение “page fault”, обрабатываемое операционной системой. Если такая ошибка возникает на уровне ядра, то генерируется сообщение “oops”.

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

Сообщение “oops” отображает статус процессора во время ошибки. Отображается значения регистров процессора, положение таблиц дескрипторов страниц, и другую, предположительно некорректную, информацию. Это сообщение генерируется оператором printk() в обработчике “fault” (arch/*/kernel/traps.c) и диспетчеризуется по правилам описанным в разделе “printk”.

Давайте рассмотрим одно из таких сообщений. Это результат разыменования NULL указателя на PC работающем с ядром версии 2.4. Наибольший интерес, в этом случае, представляет значение регистра EIP, содержащего адрес ошибочной инструкции.

Unable to handle kernel NULL pointer dereference at virtual address \
     00000000
 printing eip:
c48370c3
*pde = 00000000
Oops: 0002
CPU:    0
EIP:    0010:[<c48370c3>]
EFLAGS: 00010286
eax: ffffffea   ebx: c2281a20   ecx: c48370c0   edx: c2281a40
esi: 4000c000   edi: 4000c000   ebp: c38adf8c   esp: c38adf8c
ds: 0018   es: 0018   ss: 0018
Process ls (pid: 23171, stackpage=c38ad000)
Stack: 0000010e c01356e6 c2281a20 4000c000 0000010e c2281a40 c38ac000 \
            0000010e 
       4000c000 bffffc1c 00000000 00000000 c38adfc4 c010b860 00000001 \
            4000c000 
       0000010e 0000010e 4000c000 bffffc1c 00000004 0000002b 0000002b \
            00000004 
Call Trace: [<c01356e6>] [<c010b860>] 
Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00 

Данное сообщение получено записью в устройство, управляемое драйвером с умышленно допущенной ошибкой. Реализация метода write() в faulty.c примитивна:

ssize_t faulty_write (struct file *filp, const char *buf, size_t count,
                loff_t *pos)
{
    /* make a simple fault by dereferencing a NULL pointer */
    *(int *)0 = 0;
    return 0;
}

Из кода приведенного метода видно, что производится попытка разыменования NULL-указателя. Т.к. 0 не может являться корректным значением для указателя, то возникает ошибка “fault”, которая приводит к генерации ядром сообщения “oops” показанного ранее. При этом, ядро убивает процесс, вызвавший обращение к этому методу модуля.

Демонстрационный модуль имеет более интересное условие ошибки “fault” в реализации метода read():

char faulty_buf[1024];

ssize_t faulty_read (struct file *filp, char *buf, size_t count, 
                     loff_t *pos)
{
    int ret, ret2;
    char stack_buf[4];

    printk(KERN_DEBUG "read: buf %p, count %li\n", buf, (long)count);
    /* the next line oopses with 2.0, but not with 2.2 and later */
    ret = copy_to_user(buf, faulty_buf, count);
    if (!ret) return count; /* we survived */

    printk(KERN_DEBUG "didn't fail: retry\n");
    /* For 2.2 and 2.4, let's try a buffer overflow  */
    sprintf(stack_buf, "1234567\n");
    if (count > 8) count = 8; /* copy 8 bytes to the user */
    ret2 = copy_to_user(buf, stack_buf, count);
    if (!ret2) return count;
    return ret2;
}

Сначала производится передача данных из глобального буфера без проверки его размера, а затем провоцируется переполнение буфера записью в локальный буфер. Первая ошибка приводит к сообщению “oops” только в ядре версии 2.0, потому что в более поздних версиях ядра функции передачи данных из пространства ядра в пространство пользователя автоматически выполняют такую проверку. Переполнение буфера приводит к ошибке в любых версиях ядра (сообщение “oops”). Однако, так как инструкция return устанавливает EIP в несуществующую область, такой сорт ошибок много сложнее для отладки, и вы можете увидеть что-нибудь вроде:

EIP:    0010:[<00000000>]
[...]
Call Trace: [<c010b860>] 
Code:  Bad EIP value.

Основная проблема стоящая перед пользователем, обрабатывающем сообщения “oops” заключается сложной интерпретации шестнадцатеричных значений. Существует пара утилит, выполняющих обработку этих значений к более понятной форме: klogd и ksymoops. Ранние реализации этих инструментов выполняли это преобразование автоматически. Теперь, пользователю следует явно указать необходимость преобразования. В дальнейшем мы обсудим интерпретацию первого приведенного “oops” сообщения, вызванного разыменованием NULL-указателя.

Использование klogd

Демон klogd может декодировать “oops”-сообщения перед тем, как они достигнут log-файлов. В большинстве случаев, klogd может обеспечить всю информацию, требуемую разработчику для решения проблемы.

Рассмотрим вышеприведенный случай “oops”-сообщения достигшего системного log-файла. Обратите внимание на уточнение, произведенное klogd, для EIP и Call Trace.

Unable to handle kernel NULL pointer dereference at virtual address \
     00000000 
 printing eip: 
c48370c3 
*pde = 00000000 
Oops: 0002 
CPU:    0 
EIP:    0010:[faulty:faulty_write+3/576] 
EFLAGS: 00010286 
eax: ffffffea   ebx: c2c55ae0   ecx: c48370c0   edx: c2c55b00 
esi: 0804d038   edi: 0804d038   ebp: c2337f8c   esp: c2337f8c 
ds: 0018   es: 0018   ss: 0018 
Process cat (pid: 23413, stackpage=c2337000) 
Stack: 00000001 c01356e6 c2c55ae0 0804d038 00000001 c2c55b00 c2336000 \
            00000001
       0804d038 bffffbd4 00000000 00000000 bffffbd4 c010b860 00000001 \
            0804d038
       00000001 00000001 0804d038 bffffbd4 00000004 0000002b 0000002b \
            00000004
Call Trace: [sys_write+214/256] [system_call+52/56]  
Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00  

klogd уточняет информацию, делая ее более понятной, упрощая решение проблемы. В приведенном случае, мы видим, что EIP, на момент аварии, находился в теле функции faulty_write(). Строка 3/576 говорит, что процессор находился на третьем байте функции, общий размер которой составляет 576 байт. Обратите внимание, что приведенные значения записаны в десятичном, а не в шестнадцатеричном формате.

Конечно, требуется тренировка, для того, чтобы научиться извлекать полезную информацию из таких сообщений. При запуске, klogd загружает всю доступную символьную информацию из соответствующих символьных таблиц, и использует ее впоследствии. Если вы загружаете модуль после того, как был загружен klogd, то klogd не будет иметь символьной информации по модулю. Для того, чтобы заставить klogd обновить свою символьную информацию, необходимо, после загрузки модуля послать процессу klogd сигнал SIGUSR1, но до возникновения сообщения “oops”.

Можно также запустить klogd демон с параметром -p (“paranoid”), который приведет к перезагрузке символьной информации всякий раз при возникновении сообщения “oops”. man-страница по klogd не рекомендует использовать этот режим, однако, вы должны учитывать особенности загрузки символьной информации.

Для того, чтобы klogd работал правильно, он должен иметь текущую копию символьной таблицы из файла System.map. Обычно, этот файл расположен в каталоге /boot. Если вы построили и инсталлировали ядро из нестандартного каталога, то вы, все-равно, должны разместить System.map данной версии ядра в /boot, или сообщить klogd о размещении этого файла. Если символьная таблица используется для декодирования системных логов, то вы должны быть уверены, что декодирование производится корректно.

Использование ksymoops

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

До серии разработки ядра 2.3 ksymoops распространялся вместе с источниками ядра к каталоге scripts. Теперь, независимую от ядра, версию ksymoops можно найти на нашем FTP сайте. Даже, если вы работаете со старыми версиями ядра, вы можете зайти на ftp://ftp.ocs.com.au/pub/ksymoops и получить усовершенствованную версию инструмента.

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

Файл System.map
Таблица символов, содержащаяся в этом файле
должна соответствовать запущенному ядру, в котором возникло
сообщение “oops”. По умолчанию, этот файл ищется в
каталоге /usr/src/linux/System.map.
Список модулей

ksymoops должен знать, какие модули были загружены в ядро, при
возникновении сообщения “oops”, для того, чтобы извлечь
символьную информацию об этих модулях. Если вы не предоставите этот
список, то ksymoops будет просматривать файл /proc/modules, для
получения этой информации.
Список символов ядра, определенных при возникновении сообщения “oops”
По умолчанию, список берется из файла /proc/ksyms.
Копия имиджа ядра, которое было запущено на момент ошибки
Для правильной работы утилиты ksymoops, требуется оригинальная (не
скомпрессированная версия ядра – vmlinz, zImage или bzImage) с
которой была загружена ваша система. По умолчанию, утилита не
использует этот имидж, потому что, в большинстве систем, он
отсутствует. Если, вы, все-таки, имеете этот имидж, вы должны
передать его расположение программе, с помощью опции -v.
Положение объектных файлов модулей, которые были загружены
Утилита ksymoops будет просматривать стандартный каталог
расположения модулей системы, но во время разработки, скорее всего,
вам понадобиться указать расположение вашего модуля, используя опцию -o.

Несмотря на то, что ksymoops получает дополнительную информацию из файловой системы /proc, результаты не могут быть абсолютно надежными. Безусловно то, что система будет перезагружена между событиями возникновения сообщения “oops” и обработкой этого сообщения в ksymoops, но состояние файловой системы /proc после перезагрузки может не соответствовать состоянию, при котором возникла ошибка. Поэтому, при возможности, необходимо сохранить копии файлов /proc/modules и /proc/ksyms до возникновения ошибки.

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

Последний аргумент командной строки утилиты определяет размещение файла сообщения “oops”. Если этот аргумент пропущен, то в лучших традициях Unix, утилита принимает текст сообщения из стандартного ввода stdin.

The last argument on the tool’s command line is the location of the oops message; if it is missing, the tool will read stdin in the best Unix tradition. The message can be recovered from the system logs with luck; in the case of a very bad crash you may end up writing it down off the screen and typing it back in (unless you were using a serial console, a nice tool for kernel developers).

Заметьте, что ksymoops неправильно обработает “oops”-сообщения, которые уже были обработаны демоном klogd. Если у вас запущен klogd, и ваша система продолжает работать после “oops”-сообщения, то, часто, “чистое” “oops”-сообщение можно получить с помощью команды dmesg.

Если вы не определили явно положение всей требуемой для ksymoops информации, то вы получите соответствующие предупреждающие сообщения. Предупреждения, также, будут выданы по модулям, которые не экпортировали символы в символьную таблицу ядра.

Приведем пример выходной информации утилиты ksymoops:

>>EIP; c48370c3 <[faulty]faulty_write+3/20>   <=====
Trace; c01356e6 <sys_write+d6/100>
Trace; c010b860 <system_call+34/38>
Code;  c48370c3 <[faulty]faulty_write+3/20>
00000000 <_EIP>:
Code;  c48370c3 <[faulty]faulty_write+3/20>   <=====
   0:   c7 05 00 00 00    movl   $0x0,0x0   <=====
Code;  c48370c8 <[faulty]faulty_write+8/20>
   5:   00 00 00 00 00 
Code;  c48370cd <[faulty]faulty_write+d/20>
   a:   31 c0             xorl   %eax,%eax
Code;  c48370cf <[faulty]faulty_write+f/20>
   c:   89 ec             movl   %ebp,%esp
Code;  c48370d1 <[faulty]faulty_write+11/20>
   e:   5d                popl   %ebp
Code;  c48370d2 <[faulty]faulty_write+12/20>
   f:   c3                ret    
Code;  c48370d3 <[faulty]faulty_write+13/20>
  10:   8d b6 00 00 00    leal   0x0(%esi),%esi
Code;  c48370d8 <[faulty]faulty_write+18/20>
  15:   00 

Как вы видите, ksymoops выводит информацию о EIP и стеке ядра в виде похожем на вывод klogd, но более точно и в шестнадцатеричном формате. Вы можете видеть, что функция faulty_write() имеет длину 0x20 байт. Эту и другую доступную информацию ksymoops прочитал из объектного файла вашего модуля.

Кроме того, в данном примере, утилита ksymoops смогла представить ассемблерное представление кода, в котором возникла ошибка. В ассемблерном коде используется мнемоника AT&T. Данная возможность, предоставляемая утилитой очень удобна – вы сразу можете видеть инструкцию, которая пишет 0 по адресу 0.

Еще одной интересной особенностью ksymoops является ее портируемость практически на все платформы, где работает Linux и на которых используется bdf-библиотека (binary format description). Приведем пример того, как тоже самое “oops”-сообщение выглядит на аппаратной платформе SPARC64 (по типографским причинам пропущено несколько строк):

Unable to handle kernel NULL pointer dereference
tsk->mm->context = 0000000000000734
tsk->mm->pgd = fffff80003499000
              \|/ ____ \|/
              "@'/ .. \`@"
              /_| \__/ |_\
                 \__U_/
ls(16740): Oops
TSTATE: 0000004400009601 TPC: 0000000001000128 TNPC: 0000000000457fbc \
Y: 00800000
g0: 000000007002ea88 g1: 0000000000000004 g2: 0000000070029fb0 \
g3: 0000000000000018
g4: fffff80000000000 g5: 0000000000000001 g6: fffff8000119c000 \
g7: 0000000000000001
o0: 0000000000000000 o1: 000000007001a000 o2: 0000000000000178 \
o3: fffff8001224f168
o4: 0000000001000120 o5: 0000000000000000 sp: fffff8000119f621 \
ret_pc: 0000000000457fb4
l0: fffff800122376c0 l1: ffffffffffffffea l2: 000000000002c400 \
l3: 000000000002c400
l4: 0000000000000000 l5: 0000000000000000 l6: 0000000000019c00 \
l7: 0000000070028cbc
i0: fffff8001224f140 i1: 000000007001a000 i2: 0000000000000178 \
i3: 000000000002c400
i4: 000000000002c400 i5: 000000000002c000 i6: fffff8000119f6e1 \
i7: 0000000000410114
Caller[0000000000410114]
Caller[000000007007cba4]
Instruction DUMP: 01000000 90102000 81c3e008 <c0202000> \
30680005 01000000 01000000 01000000 01000000

Заметьте, что список инструкций процессора начинается не с того места, где произошла ошибка, а на три инструкции раньше. Это происходит потому, что RISC платформы выполняют несколько инструкций параллельно, и могут генерировать запоздалые исключения. Поэтому, требуется просмотреть несколько инструкции до точки исключения.

Теперь приведем вывод ksymoops когда входные данные передаются начиная со строки TSTATE:

>>TPC; 0000000001000128 <[faulty].text.start+88/a0>   <=====
>>O7;  0000000000457fb4 <sys_write+114/160>
>>I7;  0000000000410114 <linux_sparc_syscall+34/40>
Trace; 0000000000410114 <linux_sparc_syscall+34/40>
Trace; 000000007007cba4 <END_OF_CODE+6f07c40d/????>
Code;  000000000100011c <[faulty].text.start+7c/a0>
0000000000000000 <_TPC>:
Code;  000000000100011c <[faulty].text.start+7c/a0>
   0:   01 00 00 00       nop 
Code;  0000000001000120 <[faulty].text.start+80/a0>
   4:   90 10 20 00       clr  %o0     ! 0 <_TPC>
Code;  0000000001000124 <[faulty].text.start+84/a0>
   8:   81 c3 e0 08       retl 
Code;  0000000001000128 <[faulty].text.start+88/a0>   <=====
   c:   c0 20 20 00       clr  [ %g0 ]   <=====
Code;  000000000100012c <[faulty].text.start+8c/a0>
  10:   30 68 00 05       b,a   %xcc, 24 <_TPC+0x24> \
                        0000000001000140 <[faulty]faulty_write+0/20>
Code;  0000000001000130 <[faulty].text.start+90/a0>
  14:   01 00 00 00       nop 
Code;  0000000001000134 <[faulty].text.start+94/a0>
  18:   01 00 00 00       nop 
Code;  0000000001000138 <[faulty].text.start+98/a0>
  1c:   01 00 00 00       nop 
Code;  000000000100013c <[faulty].text.start+9c/a0>
  20:   01 00 00 00       nop 

Для того, чтобы получить дизассемблерный код, мы должны передать ksymoops формат целевого файла и архитектуру. Это необходимо, например, потому что в оригинальной архитектуре SPARC 64, пользовательское пространство 32-разрядное. Так, для данного случая, необходимо использовать следующие опции: -t elf64-sparc -a sparc:v9.

Вы можете возразить, что последняя трассировка не принесла никакой интересной информации. Нужно заметить, что процессоры SPARC не сохраняют все системные вызовы в стеке – регистры O7 и I7 удерживают IP (instruction pointer) последних двух вызванных функций. Вот почему значение этих регистров показано перед трассировкой. В последнем случае, ошибочная инструкция оказалась в функции, вызванной sys_write().

Обратите внимание, что на любой платформе, формат используемый для отображения дизассемблированного кода совпадает с форматом, используемым программой objdump. objdump – это мощная утилита, используемая для просмотра кода объектных файлов. Например, если вы хотите просмотреть код ошибочной функции целиком, то вы можете сделать это, набрав команду objdump -d faulty.o. На платформе SPARC64 вам понадобиться еще специальная опция: —target elf64-sparc –architecture sparc:v9. Для получения дополнительной информации об утилите objdump и ее опциях командной строки, смотрите man-руководство по команде.

Анализ “oops”-сообщения требует некоторой практики и понимания работы используемого процессора, так же как и знания соглашений, используемых для представления языка ассемблера. Каждому разработчику ядра необходимо потратить время на получение этих навыков. Потраченное время быстро окупиться, учитывая точность получаемой информации. Даже если вы уже имеете опыт работы с языком ассемблера на персональных компьютерах на не Unix системах, вам, вероятно, понадобиться потратить некоторое время на переучивание. Дело в том, что в Unix, преимущественно используется синтаксис записи команд предложенный компанией AT&T, в то время как в других операционных системах на персональном компьютере преобладает синтаксис, предложенный компанией Intel. Хорошее описание этих отличий можно найти в документации info на команду as, в главе “i386-specific”.

Зависание системы

Хотя большинство ошибок в коде ядра приводят к сообщению “oops”, иногда, вы можете полностью повесить систему. Если система повисла, то никаких сообщений напечатано быть не может. Например, если код ядра вошел в бесконечный цикл, то в ядре перестает работать диспетчер переключения задач, и система перестает реагировать на все внешние воздействия, включая “магическую” комбинацию CTRL+ALT+DEL. Ваше отношение к зависанию системы, как и к всему остальному, может быть выражено одним из двух вариантов – либо вы предотвратите это заблаговременно, либо будете отлаживать это по факту возникновения.

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

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

Если ваш драйвер повесил систему, и вы не знаете где вставить обращения к диспетчеру задач, то лучшим способом найти причину ошибки, является вывод сообщений в консоль (изменив значение console_loglevel).

Иногда может создаться видимость зависания системы. Это может случиться, например, если клавиатура окажется блокированной каким-то странным способом. Такие ложные зависания могут быть определены с помощью специальной программы, которая занимается непрерывным выводом какой-нибудь информации. Например, вывод системных часов, один из удачных вариантов такой программы – если показания часов изменяются, значит диспетчер задач продолжает работать. Если вы не использует графический десктоп, то проверку работы диспетчера можно осуществить с помощью программ, которые вызывают мигание светодиодов на клавиатуре, включают и выключают двигатель на флоппи-дисководе, или управляют спикером. Последнее действует особенно раздражающе, поэтому лучше познакомиться с ioctl-командой KDMKTONE. Пример программы (misc-progs/heartbeat.c), которая моргает светодиодами клавиатуры в ритме сердцебиения доступна в источниках на FTP-сайте O’Reilly.

Если клавиатура не принимает ввод, то можно попробовать соединиться к вашей системе по сети и убить все подозрительные процессы, или сбросить клавиатуру через kdb_mode -a. Однако, если сеть вам не доступна, то от наблюдения зависания вашего компьютера по блокировке клавиатуры не принесет много пользы. В этом случае, вам желательно иметь какое-нибудь альтернативное устройство ввода, чтобы иметь возможность, хотя бы, аккуратно перегрузить систему. Это, в любом случае, лучше, чем вызов перезагрузки компьютера какой-нибудь кнопкой на системном блоке, после которой вы будете вынуждены выждать время отработки программы проверки файловой системы fsck.

Таким альтернативным устройством может быть мышь. Начиная с версии 1.10, сервер консольной мыши gpm, через параметры командной строки предлагает такую возможность, но это работает только в текстовом режиме.

Наверное единственным исключительным инструментом в таких ситуациях является “магическая” клавиша SysRq, которая доступна на большинстве архитектур под ядром 2.2 и выше. Такая возможность вызывается комбинацией ALT+SysRq на клавиатуре PC и комбинацией ALT+Stop на клавиатурах SPARC. Третья клавиша, которую необходимо нажать после этих двух, вызывает то, или иное полезное действие. Приведем список некоторых возможных “третьих” клавиш:

r

Выключает режим “сырого” ввода в ситуациях, когда вы не можете запустить kbd_mode.

k

Вызывает функцию “secure attention” (SAK). SAK убивает все процессы, запущенные с текущей консоли, оставляю вас с “чистым”
терминалом.
s
Выполняет критическую (emergency) синхронизации для всех дисков.
u

Попытка перемонтировать все диски в режиме read-only. Эта операция,
обычно вызывается сразу же после s, и поможет вам избежать потери
времени на проверку файловой системы при возникновении серьезных
проблем в системе.
b

Немедленная перезагрузка системы. Будьте уверены, что предварительно
вы выполнили синхронизацию и перемонтирование дисков.
p
Печатает текущее состояние регистров
t

Печатает текущий список задач
m
Печатает информацию по использованию памяти

Существуют и другие третьи клавиши SysRq. Полных список смотрите в файле sysrq.txt каталога Documentation источников ядра. Заметьте, что такое SysRq-волшебство должно быть явно разрешено в конфигурации ядра. Большинство дистрибутивов включают эту возможность по соображениям безопасности. Эту возможность можно разрешить во время работы ядра следующей командой:

echo 1 > /proc/sys/kernel/sysrq

Другая предосторожность, которую нужно выполнять при воспроизведении зависания системы заключается в монтировании всех дисков в режиме read-only (или не монтировать ненужные файловые системы вообще). Если диски примонтированы в режиме read-only, или не примонтированы вообще, то отсутствует риск повреждения файловой системы. Также, можно монтировать все файловые системы через NFS (network file system – сетевая файловая система). При этом, в конфигурации ядра должно быть разрешено монтирование “NFS-Root” и, необходимо передать специальные параметры ядру при загрузке. В этом случае, вы избежите повреждение файловой системы даже без использования SysRq-комбинаций.

Отладчики и дополнительные отладочные инструменты

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

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

Использование gdb

gdb может оказаться весьма полезным инструментом для просмотра внутренностей системы. Умелое использование отладчика в ядре требует хорошего знания gdb-комманд, понимания ассемблерного кода и представления о трансляции Си-команд в ассемблер и оптимизации.

Отладчик должен быть вызван так, как будто бы ядро представляло собой обычное приложение. В командной строке вызова отладчика необходимо, помимо задания имени файла нескомпрессированного ядра добавить имя core-файла.

   gdb /usr/src/linux/vmlinux /proc/kcore

Первый аргумент представляет собой имя файла нескомпрессированного ядра, которое исполняется в данный момент в системе. Это не zImage, не bzImage, и ни какая-нибудь другая скомпрессированная копия ядра.

Вторым аргументом командной строки запуска gdb является имя core-файла. Как и любой другой файл в файловой системе /proc, файл /proc/kcore генерируется в момент его чтения. Когда системный вызов read() исполняется для файловой системы /proc он отображается на вызов функции генерирующей необходимые данные. Мы уже обсуждали этот механизм в разделе “Использование файловой системы /proc” ранее, в этой главе. kcore используется для представления выполняющегося ядра в формате core-файла. Этот файл очень большой, потому что представляет адресное пространство ядра целиком, соответствующее физической памяти системы. Используя gdb вы можете просмотреть значения переменных ядра с помощью обычных gdb-команд. Например, команда p jiffies отобразит количество системных тиков ядра (системная переменная jiffies) прошедших с момента загрузки системы до текущего момента времени.

При выводе данных из gdb ядро продолжает свою работу, и значение его переменных может изменяться с течением времени. Однако, gdb оптимизирует доступ к core-файлу через кэширование уже прочитанных данных. Поэтому, если вы повторите вывод переменной jiffies вы получите то же самое значение, что и раньше. Кэширование значений позволяет избежать дополнительных дисковых операций, и удобно при работе с обычными core-файлами, но не с динамическими core-образами. Решением является вызов команды core-file /proc/kcore тогда, когда вы хотите сбросить gdb-кэш. Вам не понадобится постоянное использование команды core-file при чтении состояния переменных, т.к. gdb читает core-файл блоками по нескольку килобайт, и кэширует только те блоки, которые уже прочитаны.

Вся мощь gdb не будет доступна при работе с ядром. Например, gdb не может модифицировать данные ядра. Также, вы не сможете установить точки останова и просмотра, и не сможете выполнить функцию ядра одним шагом.

Если вы скомпилируете ядро с опцией компилятора -g (поддержка отладочной информации), то результирующий файл vmlinux будет отлаживать в gdb значительно удобнее, но размер файла будет в три и более раз больше.

Немного по другому обстоят дела на не PC компьютерах. На архитектуре Alpha, ядро разархивируется при загрузке перед созданием boot-имиджа, поэтому вы можете работать как с vmlinux, так и с vmlinux.gz файлами. На архитектуре SPARC ядро (по крайней мере ядро 2.0) не разархивируется по умолчанию.

Когда вы компилируете ядро с опцией компилятора -g и запускаете отладчик используя vmlinux вместе с /proc/kcore, то вы можете получить от gdb множество информации о внутренностях ядра. Вы можете, например, использовать такие команды как p *module_list, p *module_list->next и p *chrdevs[4]->fops для получения информации из структур. Вообще, для удобной работы с командой p, вам необходимо иметь карту символов ядра (kernel map) и его источники.

Другой полезной функцией, которую выполняет gdb на запущенном ядре является дизассемблирование функций, через соответствующую команду дизассемблирования (которая может быть сокращена до disass) или через “examine instructions” (x/i) команды. Команда дизассемблирования может принять в качестве аргумента как имя функции, так и диапазон памяти, несмотря на то, что x/i принимает только адрес памяти в числовой или символической форме. Вы можете, например, ввести x/20i для дизассемблирования 20 инструкций. Заметьте, что вы не можете дизассемблировать функцию модуля, потому что отладчик работает по файлу vmlinux, в котором нет никакой информации о вашем модуле. Если вы попытаетесь дизассемблировать адрес относящийся к модулю, то, в большинстве случаев gdb выдаст вам что-то вроде “Cannot access memory at xxxx”. По той же причине вы не сможете просмотреть элементы данных принадлежащих модулю. Они могут быть прочитаны из /dev/mem если вы знаете адрес ваших переменных, но процесс извлечения “сырых” данных из RAM не самое простое занятие.

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

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

Отладчик ядра kdb

Многие читатели могут быть удивлены, что ядро не имеет более продвинутого специального отладчика. Ответ на этот вопрос очень прост – Линус (создатель Linux) не доверяет интерактивным отладчикам. Он боится, что это приведет к исправлению симптомов, а не ошибок, которые их вызывают. Поэтому, в ядре нет встроенного отладчика.

Другие разработчики ядра, однако, считают, что применение интерактивных отладочных инструментов, в некоторых случаях, уместно. Одним из таких инструментов является kdb – встроенный в ядро отладчик, доступный как неофициальный патч на oss.sgi.com. Для использования kdb вы должны получить патч, соответствующий версии вашего ядра, применить его, перекомпилировать и установить новое ядро. Однако заметьте, что kdb работает только в архитектуре IA-32 (x86). kdb для IA-64 находится пока в стадии разработки.

Если вы работаете с ядром, в котором присутствует kdb, то вы можете воспользоваться двумя способами входа в отладчик. Находясь в консоли, нажмите клавишу Pause (или Break) на клавиатуре для перехода в режим отладки. Также, kdb запускается автоматически при возникновении сообщения “oops”, или при исполнении точки останова. При переходе в режим отладки, вы увидите на экране, например, такое сообщение:

Entering kdb (0xc1278000) on processor 1 due to Keyboard Entry
[1]kdb> 

Заметьте, что при запуске kdb, ядро прекращает всю свою работу. Правильнее сказать, что при передаче управления отладчику kdb останавливаются все процессы в системе. В частности, у вас не будет сетевого доступа, конечно, кроме случая, когда вы отлаживаете сетевой драйвер. Вообще, при использовании kdb, хорошей мыслью является загрузка системы в режиме single-user (см. конфигурационный файл /etc/inittab).

В качестве примера, рассмотрим простую сессию отладки драйвера scull. Предположим, что драйвер уже загружен. Тогда вы можете вызвать отладчик kdb для установки точки останова на вызове функции scull_read() следующим образом:

[1]kdb> bp scull_read
Instruction(i) BP #0 at 0xc8833514 (scull_read)
    is enabled on cpu 1
[1]kdb> go

С помощью команды bp в отладчике kdb устанавливают точки останова. Т.е., согласно вышеприведенному фрагменту, при следующем вызове функции scull_read() ядро остановится и перейдет в режим отладки. Следующей командой go, мы продолжаем выполнение ядра и ждем его останова на scull_read(). Когда это случится, мы увидим на экране следующее:

Entering kdb (0xc3108000) on processor 0 due to Breakpoint @ 0xc8833515
Instruction(i) breakpoint #0 at 0xc8833514
scull_read+0x1:   movl   %esp,%ebp
[0]kdb>

Итак, мы остановились на первой инструкции функции scull_read(). Посмотрим состояние стека:

[0]kdb> bt
    EBP       EIP         Function(args)
0xc3109c5c 0xc8833515  scull_read+0x1
0xc3109fbc 0xfc458b10  scull_read+0x33c255fc( 0x3, 0x803ad78, 0x1000, 0x1000, 0x804ad78)
0xbffffc88 0xc010bec0  system_call
[0]kdb>

kdb пытается вывести аргументы каждой трассируемой функции. Однако, если при компиляции была использована оптимизация то kdb может неверно определить аргументы. Таким образом, в вышеприведенном фрагменте, напечатано пять аргументов вызова scull_read(), хотя, в действительности, функция имеет только четыре.

Теперь посмотрим какие-нибудь данные. С помощью команды mds манипулируют данными. Мы можем запросить значение указателя scull_device с помощью следующей команды:

[0]kdb> mds scull_devices 1
c8836104: c4c125c0 ....

Здесь мы запросили одно четырехбайтовое слово данных расположенное по адресу scull_devices. Согласно полученному ответу, наш массив устройств лежит начиная с адреса c4c125c0. Для того, чтобы просмотреть содержимое массива воспользуемся этим адресом:

[0]kdb> mds c4c125c0 
c4c125c0: c3785000  ....
c4c125c4: 00000000  ....
c4c125c8: 00000fa0  ....
c4c125cc: 000003e8  ....
c4c125d0: 0000009a  ....
c4c125d4: 00000000  ....
c4c125d8: 00000000  ....
c4c125dc: 00000001  ....

Восемь выведенных строк соответствуют восьми полям структуры Scull_Dev. Таким образом, мы видим, что память для первого устройства расположена по адресу 0x3785000, и что нет больше элементов в связанном списке. Также, мы видим, что размер одного кванта 4000 байт (hex fa0), и что размер массива равен 1000 элементов (hex 3e8). Мы видим, что в устройство загружено 154 байта данных (hex 9a), и т.д.

С помощью kdb вы можете изменить данные. Предположим, что вы хотите уменьшить количество загруженных в устройство данных:

[0]kdb> mm c4c125d0 0x50
0xc4c125d0 = 0x50

Теперь, следующий вызов команды cat из устройства выведет меньшее количество данных.

kdb имеет множество других возможностей включая пошаговое выполнение инструкций процессора (именно инструкций, а не команд языка Си файла источника), установку точек останова на доступе к данным, дизассемблирование кода, доступ к данным регистров, проход по связанному списку и прочие. После того, как вы примените kdb патч к источникам своего ядра, полную документацию в виде man-страниц вы найдете в каталоге Documentation/kdb дерева источников ядра.

Патч IKD (Integrated Kernel Debugger)

Некоторые разработчики ядра поддерживают неофициальный патч называемый интегрированным отладчиком ядра, или IKD (Integrated Kernel Debugger). IKD предлагает некоторые интересные возможности по отладке ядра. Первоначально поддерживалась только платформа x86, но сейчас патч поддерживает и другие архитектуры. IKD может быть найдено по адресу ftp://ftp.kernel.org/pub/linux/kernel/people/andrea/ikd. Этот патч должен быть применен к источникам вашего ядра. Для каждой версии ядра выпускается отдельная версия патча.

Одной из интересных характеристик IKD патча является возможность отладки стека ядра. Если вы включили данную возможность, то ядро будет автоматически проверять количество свободного пространства в стеке ядра перед вызовом каждой функции, и выдает сообщение “oops” при нехватке стековой памяти. Если в вашем ядре возникают повреждения стека, то этот инструмент может помочь вам в поиске проблемы. Также имеется характеристика “stack meter”, которая может быть использована вами для просмотра заполнения стека в любой момент отладки.

Также, патч IKD включает некоторые инструменты позволяющие отлавливать “kernel lockups”. Т.е. такого состояния системы при котором она не может полноценно функционировать, и из которого она не может выйти самостоятельно. Так, детектор “soft lockup” вызывает сообщение “oops” если некая функция ядра выполняется слишком долго без обращения к диспетчеру задач. Это реализуется простым подсчетом количества вызовов функции.

The IKD patch also includes some tools for finding kernel lockups. A "soft lockup" detector forces an oops if a kernel procedure goes for too long without scheduling. It is implemented by simply counting the number of function calls that are made and shutting things down if that number exceeds a preconfigured threshold. Another feature can continuously print the program counter on a virtual console for truly last-resort lockup tracking. The semaphore deadlock detector forces an oops if a process spends too long waiting on a down call.

Еще одной полезной характеристикой IKD является возможность такой трассировки ядра, при которой можно записать пути прохождения по коду ядра. Имеется несколько отладочных инструментов по контролю используемости памяти, включая детектор утечки памяти и пару так называемых “poisoners”-инструментов (отравителей), которые могут оказаться полезными при поиске проблем связанных с неправильным использованием памяти.

Наконец, IKD включает в себя версию kdb отладчика, обсужденного в предыдущем разделе. Однако, версия kdb включенная в патч IKD может быть несколько устаревшей. Поэтому, если вам нужен kdb, мы рекомендуем брать получить его напрямую с адреса oss.sgi.com.

Патч kgdb

kgdb представляет собой патч, предлагающий функциональность отладчика kdb для Linux ядра, но только на x86 системах. Он предоставляет отладочные данные по отлаживаемой системе через последовательный порт обычному gdb отладчику, запущенному на другой системе. Таким образом, при использовании kgdb вам необходимыдве системы. Как и kdb, kgdb можно получить с oss.sgi.com.

Установка kgdb, также заключается в установке патча, компиляции и загрузки с модифицированного ядра. Вам необходим соединить две системы последовательным кабелем (нуль-модемный вариант) и инсталлировать некоторые дополнительные файлы на том компьютере где будет использоваться gdb. После применения патча, в файле Documentation/i386/gdb-serial.txt, вы найдете подробные инструкции по использованию патча. Обязательно познакомьтесь с инструкциями на дополнительные компоненты (модули, обеспечивающие отладку). Там вы найдете несколько интересных gdb макросов, которые написаны с целью обеспечения такой отладки.

Анализаторы “Crash Dump” ядра

Анализаторы “crash dump” позволяет системе записывать ее состояние при возникновении “oops”-сообщения, которое можно изучить впоследствии. Они могут быть особенно полезны, если вы занимаетесь поддержкой своих драйверов у разных пользователей. Пользователи, обычно неохотно делают копии “oops”-сообщений для вас, поэтому “crash dump” система позволит вам получить подробную информацию необходимую для определения возникшей у пользователя проблемы не обременяя пользователя дополнительной работой. Поэтому неудивительно, что доступные “crash dump” анализаторы написаны компаниями именно для упрощения поддержки систем у пользователей.

На данный момент для Linux доступны два патча “crash dump” анализаторов. На момент написания этого раздела (имеется в виду оригинальный английский текст) обе эти системы были достаточно новыми и находились в стадии постоянной доработки. Поэтому, вместо описания, наверняка устаревших, версий этих систем, мы поместим информацию о том, где можно найти их современные версии.

Первый из анализаторов – LKCD (Linux Kernel Crash Dumps) доступен на oss.sgi.com. При возникновении “oops”-ситуации в ядре, LKCD пишет копию текущего состояния системы (прежде всего, содержимое памяти) в заданное, при конфигурировании, устройство. Таким dump-устройством должна быть область своппинга. Утилита, называемая LCRASH запускается при следующей загрузке (до того, как своппинг будет разрешен) для генерации crush-информации, которую можно записать в заданный dump-файл. Утилита LCRASH может быть запущена в интерактивном режиме, и обеспечить некоторый набор отладочных команд для получения различной информации по прошедшему краху системы.

LKCD на данный момент поддерживается только для 32-битной Intel архитектуры, и работает только со своп-разделами на SCSI дисках.

Другая “crash dump” система доступна на www.missioncriticallinux.com Эта система создает краховый dump-файл прямо в каталоге /var/dumps и не использует область своппинга. Это упрощает пользование системой, но означает, что вы будете иметь измененную после краха файловую систему, что может привести к неправильной интерпретации полученной информации. “crash dump” генерируется в стандартном core-файл формате, и, в дальнейшем, может быть проанализировано с помощью gdb. Этот пакет, также, предоставляет отдельный анализатор, который позволяет извлечь больше информации из “crash dump” файла, чем gdb.

“User-Mode Linux” порт

User-Mode Linux представляет интересную концепцию. Этот механизм представляется отдельным портом Linux ядра со своей собственной поддиректорией arch/um. Причем, это не является каким-то новым оборудованием системы, а представляет собой виртуальную машину реализованную на интерфейсе системных вызовов Linux. Таким образом, User-Mode Linux позволяет Linux ядру быть запущенным как отдельный процесс пользовательского режима (user-mode) на Linux системе.

Работа с копией ядра, запущенной как процесс пользовательского режима имеет некоторые преимущества. Так как она запускается на контролируемом виртуальном процессоре, то ошибки ядра не повредят реальную систему. Вы можете опробовать различные варианты аппаратно-программной конфигурации на одной и той же виртуальной системе. И, наконец, что особенно важно для разработчиков ядра, ядро работающее в пользовательском режиме может быть легко отлаживаемо через gdb отладчик. Все это только потому, что теперь ядро представляет собой не более, чем просто другой процесс. Понятно, что User-Mode Linux имеет большой потенциал в упрощении разработки ядра.

Как уже говорилось, User-Mode Linux не распространяется в официальных версиях ядра, и должен быть загружен с сайта http://user-mode-linux.sourceforge.net. Правда, обсуждается возможность того, что это будет сделано в ветке ядра 2.4 после версии 2.4.0.

Несмотря на все преимущества User-Mode Linux имеет пока серьезные ограничения по использованию. Так, виртуальный процессор, в текущей реализации, работает только в однопроцессорном режиме. Порт может быть без проблем запущен и на SMP-системе, но эмулироваться будет только один процессор. Однако, серьезнейшей проблемой является то, что ядро работающее в пользовательском режиме не может получить доступ к реальному оборудованию системы. Таким образом, пока, эта система может быть использована только для отладки наиболее простых драйверов. Наконец, пока, User-Mode Linux может работать только на IA-32 архитектуре.

Сейчас идет работа по устранению всех этих проблем, и, в ближайшем будущем, User-Mode Linux будет, наверное, наиболее необходимым инструментом для разработчиков драйверов устройств под Linux.

Набор утилит “Linux Trace Toolkit”

Linux Trace Toolkit (LTT) представляет собой патч к ядру и набор связанных с этим патчем утилит, которые позволяют проводить трассировку событий в ядре. Трассировка включает в себя временную информацию, и, таким образом, можно получить исчерпывающую картину того, что случилось за данный период времени. Таким образом это можно использовать не только для отладки, но и для решения проблем производительности.

LTT, вместе с исчерпывающей документацией можно найти по адресу www.opersys.com/LTT.

Dynamic Probes

Dynamic Probes (или DProbes) представляет собой отладочный иструмент (под лицензией GPL) от компании IBM для Linux на IA-32 архитектуре. Он позволяет разместить “пробу” в любом месте системы, как в пространстве пользователя, так и в пространстве ядра. Проба состоит из некоторого кода, написанного на специальном стек-ориентированном языке, который выполняется тогда, когда система достигнет исполнения данной точки. Этот код может передать информацию в пользовательское пространство, изменить регистры и выполнить еще множество разных полезных вещей. Полезным свойством DProbes является то, что поскольку она встроена в ядро, то “пробы” можно вставлять где угодно в работающей системе без перекомпиляции ядра и перезагрузки. DProbes может работать вместе с Linux Trace Toolkit вставляя трассировку событий в любом месте.

DProbes могут быть загружены с IBM Open Source сайта oss.software.ibm.com.

Добавить комментарий