Linux программирование в примерах

Роббинс Арнольд

Часть 3

Отладка и заключительный проект

 

 

Глава 15

Отладка

 

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

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

Данная глава охватывает ряд тем, начиная с общих методик и советов по отладке (компилирование для отладки и элементарное использование GDB, отладчика GNU), переходя к ряду методик для использования при разработке и отладке программы, упрощающих отладку, и затем рассмотрением ряда инструментов, помогающих в процессе отладки. Глава завершается краткими сведениями по тестированию программного обеспечения и великолепным набором «правил отладки», извлеченных из книги, которую мы весьма рекомендуем.

Большая часть наших советов основана на нашем долгосрочном опыте участия в качестве добровольца в проекте GNU по поддержке gawk (GNU awk). Большинство, если не все, специфические примеры, которые мы представляем, происходят от этой программы. На протяжении главы особые рекомендации помечены словом Рекомендация.

 

15.1. Сначала главное

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

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

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

 

15.2. Компиляция для отладки

Для использования отладчика исходного кода, отлаживаемый исполняемый файл должен быть откомпилирован с опцией компилятора -g. Эта опция заставляет компилятор внедрять в объектный код дополнительные отладочные идентификаторы; то есть дополнительные сведения, содержащие имена и типы переменных, констант, функций и так далее. Отладчик затем использует эту информацию для приведения в соответствие местоположения исходного кода с исполняемым кодом и получения или сохранения значений переменных в работающей программе.

На многих системах Unix опция компилятора -g является взаимно исключающей с опцией -O, которая включает оптимизацию. Это потому, что оптимизации могут вызвать перестановку битов и участков объектного кода, так что больше не будет прямого соответствия с тем, что исполняется, и линейным прочтением исходного кода. Отменив оптимизации, вы значительно облегчаете отладчику установление связи между объектным и исходным кодом, и в свою очередь, пошаговое прохождение программы работает очевидным образом. (Пошаговое исполнение вскоре будет описано.)

GCC, GNU Compiler Collection (коллекция компиляторов GNU), на самом деле допускает совместное использование -g и -O. Однако, это привносит как раз ту проблему, которую мы хотим избежать при отладке: следование исполнению в отладчике становится значительно более трудным. Преимуществом совместного использования опций является то, что вы можете оставить отладочные идентификаторы в конечном оптимизированном исполняемом модуле. Они занимают лишь дисковое пространство, а не память. После этого установленный исполняемый файл все еще можно отлаживать при непредвиденных случаях.

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

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

 

15.3. Основы GDB

 

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

Исторически в V7 Unix был adb, который являлся отладчиком машинного уровня В System III был sdb, который являлся отладчиком исходного кода, a BDS Unix предоставляла dbx, также отладчик исходного кода. (Обе продолжали предоставлять adb.) dbx продолжает существовать на некоторых коммерческих системах Unix.

GDB, отладчик GNU, является отладчиком исходного кода. У него значительно больше возможностей, он значительно более переносим и более практичен, чем любой из sdb или dbx.

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

Имеются графические отладчики; они предоставляют больший обзор исходного кода и обычно предоставляют возможность манипулировать программой как из окна командной строки, так и через компоненты GUI, такие, как кнопки и меню. Отладчик ddd является одним из таких; он построен поверх GDB, так что если вы изучите GDB, вы сразу же сможете начать использовать ddd. (У ddd есть собственное руководство, которое следует прочесть, если вы собираетесь интенсивно его использовать.) Другим графическим отладчиком является Insight, который использует для предоставления поверх GDB графического интерфейса Tcl/Tk. (Следует использовать графический отладчик, если он доступен и нравится вам. Поскольку мы собираемся предоставить введение в отладчики и отладку, мы выбрали использование простого интерфейса, который можно представить в напечатанном виде.)

GDB понимает С и С++, включая поддержку восстановления имен (name demangling), что означает, что вы можете использовать для функций-членов классов и перегруженных функций обычные имена исходного кода С++. В частности, GDB распознает синтаксис выражений С, что полезно при проверке значения сложных выражений, таких, как '*ptr->x.a[1]->q'. Он понимает также Fortran 77, хотя вам может понадобиться добавить к имени функции или переменной Фортрана символ подчеркивания GDB также частично поддерживает Modula-2 и имеет ограниченную поддержку Паскаля.

Если вы работаете на системе GNU/Linux или BSD (и установили средства разработки), у вас, вероятно, уже установлена готовая к использованию последняя версия GDB. Если нет, исходный код GDB можно загрузить с FTP-сайта проекта GNU для GDB и самостоятельно его построить.

GDB поставляется с собственным руководством, которое занимает 300 страниц. В каталоге исходного кода GDB можно сгенерировать печатную версию руководства и самостоятельно его распечатать. Можно также купить в Free Software Foundation (FSF) готовые печатные экземпляры; ваша покупка поможет FSF и непосредственно внесет вклад в производство большего количества свободного программного обеспечения. (Информацию для заказа см. на веб-сайте FSF). Данный раздел описывает лишь основы GDB; мы рекомендуем прочесть руководство, чтобы научиться использовать все преимущества возможностей GDB.

 

15.3.1. Запуск GDB

Основное использование следующее:

gdb [ опции ][ исполняемый файл [ имя файла дампа ]]

Здесь исполняемый файл является отлаживаемой программой. Имя файла дампа, если оно имеется, является именем файла core, созданном при завершении программы операционной системой с созданием снимка процесса. Под GNU/Linux такие файлы (по умолчанию) называются core. pid , где pid является ID процесса запущенной программы, которая была завершена. Расширение pid означает, что в одном каталоге могут находиться несколько дампов ядра, что бывает полезно, но также занимает дисковое пространство!

Если вы забыли указать в командной строке имена файлов, для сообщения GDB имени исполняемого файла можно использовать 'file исполняемый-файл ', а для имени файла дампа — 'core-file имя-файла-дампа '.

При наличии дампа ядра GDB указывает место завершения программы. Следующая программа, ch15-abort.c, делает несколько вложенных вызовов функций, а затем намеренно завершается посредством abort(), чтобы создать дамп ядра:

/* ch15-abort.c --- создает дамп ядра */

#include

#include

/* recurse --- создание нескольких вызовов функций */

void recurse(void)

{

 static int i;

 if (++i == 3)

  abort();

 else

  recurse();

}

int main(int argc, char **argv)

{

 recurse();

}

Вот небольшой сеанс GDB с этой программой:

$ gcc -g ch15-abort.c -o ch15-abort /* Компилировать без -O */

$ ch15-abort /* Запустить программу */

Aborted (core dumped) /* Она печально завершается */

$ gdb ch15-abort core.4124 /* Запустить для нее GDB */

GNU gdb 5.3

Copyright 2002 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 "i686-pc-linux-gnu"...

Core was generated by 'ch15-abort'.

Program terminated with signal 6, Aborted.

Reading symbols from /lib/i686/libc.so.6...done.

Loaded symbols for /lib/i686/libc.so.6

Reading symbols from /lib/ld-linux.so.2...done.

Loaded symbols for /lib/ld-linux.so.2

#0 0x42028ccl in kill() from /lib/i686/libc.so.6

(gdb) where /* Вывести трассировку стека */

#0 0x42028cc1 in kill() from /lib/i686/libc.so.6

#1 0x42028ac8 in raise() from /lib/i686/libc.so.6

#2 0x4202a019 in abort() from /lib/1686/libc.so.6

#3 0x08048342 in recurse() at ch15-abort.c:13

 /* <-- Нам нужно исследовать здесь */

#4 0x08048347 in recurse() at ch15-abort.с:15

#5 0x08048347 in recurse() at ch15-abort.c:15

#6 0x0804835f in main (argc=1, argv=0xbffff8f4) at ch15-abort.c:20

#7 0x420158d4 in __libc_start_main() from /lib/i686/libc.so.6

Команда where выводит трассировку стека, то есть список всех вызванных функций, начиная с самых недавних. Обратите внимание, что имеется три вызова функции recurse(). Команда bt, означающая 'back trace' (обратная трассировка), является другим названием для where; ее легче набирать.

Вызов каждой функции в стеке называется фреймом. Этот термин пришел из области компиляторов, в которой параметры, локальные переменные и адреса возврата каждой функции, сгруппированные в стеке, называются фреймом стека. Команда frame GDB дает вам возможность исследовать определенный фрейм. В данном случае нам нужен фрейм 3. Это последний вызов recurse(), который вызвал abort():

(gdb) frame 3 /* Переместиться в фрейм 3 */

#3 0x08048342 in recurse() at ch15-abort.с:13

13 abort(); /* GDB выводит в фрейме положение в исходном коде */

(gdb) list /* Показать несколько строк исходного кода */

8  void recurse(void)

9  {

10  static int i;

11

12  if (++i == 3)

13   abort();

14  else

15   recurse();

16 }

17

(gdb) /* Нажатие ENTER повторяет последнюю команду */

18 int main(int argc, char **argv)

19 {

20  recurse();

21 }

(gdb) quit /* Выйти из отладчика (пока) */

Как показано, нажатие ENTER повторяет последнюю команду, в данном случае list, для отображения строк исходного кода. Это простой способ прохождения исходного кода.

Для редактирования командной строки GDB использует библиотеку readline, поэтому для повторения и редактирования ранее введенных команд можно использовать команды Emacs или vi. Оболочка Bash использует ту же самую библиотеку, поэтому если вам более знакомо редактирование командной строки в приглашении оболочки, GDB работает таким же образом. Эта особенность дает возможность избежать утомительного ручного ввода.

 

15.3.2. Установка контрольных точек, пошаговое выполнение и отслеживаемые точки

Часто при ошибках программ создается дамп ядра. Первым шагом является использование GDB с файлом core для определения процедуры, в которой произошло завершение программы. Если оригинальный двоичный файл не был откомпилирован для отладки (т.е. без -g), все, что может сообщить GDB, это имя функции, но больше никаких деталей.

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

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

После установки контрольной точки программа запускается с использованием команды run, за которой могут следовать аргументы командной строки, которые должны быть переданы отлаживаемой программе. (GDB удобным образом запоминает за вас аргументы; если нужно снова запустить программу с начала, все что нужно — это напечатать лишь саму команду run, и GDB запустит новую копию с теми же аргументами, как и ранее). Вот короткий сеанс с использованием gawk:

$ gdb gawk /* Запуск GDB для gawk */

GNU gdb 5.3

...

(gdb) break do_print /* Прерывание в do_print */

Breakpoint 1 at 0x805a36a: file builtin.c, line 1504.

(gdb) run 'BEGIN { print "hello, world" }' /* Запуск программы */

Starting program: /home/arnold/Gnu/gawk/gawk-3.1.3/gawk 'BEGIN { print "hello, world" }'

Breakpoint 1, do_print (tree=0x8095290) at builtin.c:1504

1504 struct redirect *rp = NULL; /* Исполнение достигает контрольной точки */

(gdb) list /* Показать исходный код */

1499

1500 void

1501 do_print(register NODE *tree)

1502 {

1503  register NODE **t;

1504  struct redirect *rp = NULL;

1505  register FILE *fp;

1506  int numnodes, i;

1507  NODE *save;

1508  NODE *tval;

По достижении контрольной точки вы проходите программу в пошаговом режиме. Это означает, что GDB разрешает программе исполнять лишь по одному оператору исходного кода за раз. GDB выводит строку, которую собирается выполнить, и выводит приглашение. Чтобы выполнить оператор, используется команда next:

(gdb) next /* Выполнить текущий оператор (строка 1504 выше) */

1510 fp = redirect_to_fp(tree->rnode, &rp); /* GDB выводит следующий оператор */

(gdb) /* Нажмите ENTER для его выполнения и перехода к следующему */

1511 if (fp == NULL)

(gdb) /* снова ENTER */

1519 save = tree = tree->lnode; (gdb) /* И снова */

1520 for (numnodes = 0; tree != NULL; tree = tree->rnode)

Команда step является альтернативной командой для пошагового исполнения. Между next и step есть важное различие, next выполняет следующий оператор. Если этот оператор содержит вызов функции, эта функция вызывается и возвращается до того, как GDB вернет себе управление от работающей программы.

С другой стороны, когда вы используете с содержащим вызов функции оператором step, GDB входит в вызываемую функцию, позволяя вам продолжить пошаговое исполнение (или трассировку) программы. Если оператор не содержит вызов функции, step аналогична next.

ЗАМЕЧАНИЕ . Легко забыть, какая команда была использована, и продолжать нажимать ENTER для выполнения последующих операторов. Если вы используете step , вы случайно можете войти в библиотечную функцию, такую как strlen() или printf() , с которой на самом деле не хотите возиться. В таком случае можно использовать команду finish , которая вызывает исполнение программы до возврата из текущей функции

Вывести содержимое памяти можно с использованием команды print. GDB распознает синтаксис выражений С, что упрощает и делает естественным проверку структур, на которые ссылаются указатели:

(gdb) print *save /* Вывести структуру, на которую указывает save */

$1 = {sub = {nodep = {l = {lptr = 0x8095250, param_name = 0x8095250 "pR\t\b",

 l1 = 134828624}, r = {rptr = 0x0, pptr = 0, preg = 0x0,

 hd = 0x0, av = 0x0, r_ent =0}, x = {extra = 0x0, x1 = 0,

 param_list = 0x0},

 name = 0x0, number = 1, reflags = 0}, val = {

 fltnum = 6.6614191194446594e-316, sp = 0x0, slen = 0, sref = 1,

 idx = 0}, hash = {next = 0x8095250, name = 0x0, length = 0, value = 0x0,

 ref = 1}}, type = Node_expression_list, flags = 1}

В заключение, команда cont (continue — продолжить) дает возможность продолжить выполнение программы. Она будет выполняться до следующей контрольной точки или до нормального завершения, если других контрольных точек нет. Этот пример продолжается с того места, на котором остановился предыдущий:

1520 for (numnodes = 0; tree != NULL; tree = tree->rnode)

(gdb) cont /* Продолжить *!

Continuing.

hello, world

Program exited normally. /* Сообщение от GDB */

(gdb) quit /* Выйти из отладчика */

Отслеживаемая точка (watchpoint) подобна контрольной точке, но используется для данных, а не для кода. Отслеживаемые точки устанавливаются для переменной (или поля структуры или объединения или элемента массива), при их изменении GDB посылает уведомления. GDB проверяет значение отслеживаемой точки по мере пошагового исполнения программы и останавливается при изменении значения. Например, переменная do_lint_old в gawk равна true, когда была использована опция --lint_old. Эта переменная устанавливается в true функцией getopt_long(). (Мы рассмотрели getopt_long() в разделе 2.1.2 «Длинные опции GNU»). В файле main.c программы gawk:

int do_lint_old = FALSE;

 /* предупредить о материале, не имевшейся в V7 awk */

...

static const struct option optab[] = {

 ...

 { "lint-old", no_argument, &do_lint_old, 1 },

 ...

};

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

$ gdb gawk /* Запустить GDB с gawk */

GNU gdb 5.3

...

(gdb) watch do_lint_old

 /* Установить отслеживаемую точку для переменной */

Hardware watchpoint 1: do_lint_old

(gdb) run --lint-old 'BEGIN { print "hello, world" }'

 /* Запустить программу */

Starting program: /home/arnold/Gnu/gawk/gawk-3.1.4/gawk —lint-old

'BEGIN { print "hello, world" }'

Hardware watchpoint 1: do_lint_old

Hardware watchpoint 1: do_lint_old

Hardware watchpoint 1: do_lint_old

 /* Проверка отслеживаемой точки при работе программы */

Hardware watchpoint 1: do_lint_old

Hardware watchpoint 1: do_lint_old

Old value = 0 /* Отслеживаемая точка останавливает программу */

New value = 1

0x420c4219 in _getopt_internal() from /lib/i686/libc.so.6

(gdb) where /* Трассировка стека */

#0 0x420c4219 in _getopt_internal() from /lib/i686/libc.so.6

#1 0x420c4e83 in getopt_long() from /lib/i686/libc.so.6

#2 0x080683a1 in main (argc=3, argv=0xbffff8a4) at main.c:293

#3 0x420158d4 in __libc_start_main() from /lib/i686/libc.so.6

(gdb) quit /* На данный момент мы закончили */

The program is running. Exit anyway? (y or n) y /* Да */

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

Стоит также распечатать справочную карточку GDB, которая поставляется в дистрибутиве GDB в файле gdb/doc/refcard.tex. Создать печатную версию справочной карточки для PostScript после извлечения исходника и запуска configure можно с помощью следующих команд:

$ cd gdb/doc /* Перейти о подкаталог doc */

$ make refcard.ps /* Отформатировать справочную карточку */

Предполагается, что справочная карточка будет распечатана с двух сторон листа бумаги 8,5×11 дюймов (размер «letter») в горизонтальном (landscape) формате. В ней на шести колонках предоставлена сводка наиболее полезных команд GDB. Мы рекомендуем распечатать ее и поместить под своей клавиатурой при работе с GDB.

 

15.4. Программирование для отладки

 

Имеется множество методик для упрощения отладки исходного кода, от простых до сложных. В данном разделе мы рассмотрим ряд из них.

 

15.4.1. Код отладки времени компилирования

 

Несколько методик относятся к самому исходному коду.

 

15.4.1.1. Использование отладочных макросов

Возможно, простейшей методикой времени компилирования является использование препроцессора для создания условно компилируемого кода. Например:

#ifdef DEBUG

fprintf(stderr, "myvar = %d\n", myvar);

fflush(stderr);

#endif /* DEBUG */

Добавление -DDEBUG к командной строке компилятора вызывает fprintf() при выполнении программы.

Рекомендация: сообщения отладки посылайте в stderr, чтобы они не были потеряны в канале и чтобы их можно было перехватить при помощи перенаправления ввода/вывода. Убедитесь, что использовали fflush(), чтобы сообщения были выведены как можно скорее

ЗАМЕЧАНИЕ . Идентификатор DEBUG , хотя он и очевидный, также часто злоупотребляется. Лучшей мыслью является использование специфического для вашей программы идентификатора, такого как MYAPPDEBUG . Можно даже использовать различные идентификаторы для отладки кода в различных частях программы, таких, как файловый ввод/вывод, верификация данных, управление памятью и т.д.

Разбрасывание больших количеств операторов #ifdef по всему коду быстро становится утомительным. Большое количество #ifdef скрывают также логику программы. Должен быть лучший способ, и в самом деле, часто используется методика с условным определением специального макроса для вывода:

/* МЕТОДИКА 1 --- обычно используемая, но не рекомендуемая, см. текст */

/* В заголовочном файле приложения: */ #ifdef MYAPPDEBUG

#define DPRINT0(msg) fprintf(stderr, msg)

#define DPRINT1(msg, v1) fprintf(stderr, msg, v1)

#define DPRINT2(msg, v1, v2) fprintf(stderr, msg, v1, v2)

#define DPRINT3(msg, v1, v2, v3) fprintf(stderr, msg, v1, v2, v3)

#else /* ! MYAPPDEBUG */

#define DPRINT0(msg)

#define DPRINT1(msg, v1)

#define DPRINT2(msg, v1, v2)

#define DPRINT3(msg, v1, v2, v3)

#endif /* ! MYAPPDEBUG */

/* В исходном файле приложения: */

DPRINT1("myvar = %d\n", myvar);

...

DPRINT2("v1 = %d, v2 = %f\n", v1, v2);

Имеется несколько макросов, по одному на каждый имеющийся аргумент, число которых определяете вы сами. Когда определен MYAPPDEBUG, вызовы макросов DPRINT x () развертываются в вызовы fprintf(). Когда MYAPPDEBUG не определен, эти вызовы развертываются в ничто. (Так, в сущности, работает assert(); мы описали assert() в разделе 12.1 «Операторы проверки: assert()».)

Эта методика работает; мы сами ее использовали и видели, как ее рекомендуют в учебниках. Однако, она может быть усовершенствована и дальше с уменьшением количества макросов до одного:

/* МЕТОДИКА 2 --- наиболее переносима; рекомендуется */

/* В заголовочном файле приложения: */

#ifdef MYAPPDEBUG

#define DPRINT(stuff) fprintf stuff

#else

#define DPRINT(stuff)

#endif

/* В исходном файле приложения: */

DPRINT((stderr, "myvar = %d\n", myvar));

 /* Обратите внимание на двойные скобки */

Обратите внимание на то, как макрос извлекается с двумя наборами скобок! Поместив весь список аргументов для fprintf() в один аргумент, вам больше не нужно определять произвольное число отладочных макросов.

Если вы используете компилятор, удовлетворяющий стандарту С 1999 г., у вас есть дополнительный выбор, который дает наиболее чистый отладочный код:

/* МЕТОДИКА 3 --- самая чистая, но только для C99 */

/* В заголовочном файле приложения: */

#ifdef MYAPPDEBUG

#define DPRINT(mesg, ...) fprintf(stderr, mesg, __VA_ARGS__)

#else

#define DPRINT(mesg, ...)

#endif

/* В исходном файле приложения: */

DPRINT("myvar = %d\n", myvar);

DPRINT("v1 = %d, v2 = %f\n", v1, v2);

Стандарт С 1999 г. предусматривает варьирующий макрос (variadic macros); т.е. макрос, который может принимать переменное число аргументов. (Это похоже на варьирующую функцию, наподобие printf()). В макроопределении три точки '...' означают, что будет ноль или более аргументов. В теле макроса специальный идентификатор __VA_ARGS__ замещается предусмотренными аргументами, сколько бы их ни было.

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

Рекомендация: Текущие версии GCC поддерживают варьирующие макросы. Таким образом, если вы знаете, что никогда не будете использовать для компилирования своих программ что-то, кроме GCC (или какого-нибудь другого компилятора C99), можете использовать механизм C99. Однако, на момент написания, компиляторы C99 все еще не являются обычным явлением. Поэтому, если ваш код должен компилироваться разными компиляторами, следует использовать макрос в стиле с двумя парами скобок.

 

15.4.1.2. По возможности избегайте макросов с выражениями

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

Обычно для эффективности или ясности можно видеть такие макросы:

#define RS_is_null (RS_node->var_value == Nnull_string)

...

if (RS_is_null || today == TUESDAY) ...

На первый взгляд, он выглядит замечательно. Условие 'RS_is_null' ясно и просто для понимания и абстрагирует внутренние детали проверки. Проблема возникает, когда вы пытаетесь вывести значение в GDB:

(gdb) print RS_is_null

No symbol "RS_is_null" in current context.

В таком случае нужно разыскать определение макроса и вывести развернутое значение.

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

Вот сокращенный пример из io.c в дистрибутиве gawk:

void set_RS() {

 ...

 RS_is_null = FALSE;

 if (RS->stlen == 0) {

  ...

  RS_is_null = TRUE;

  ...

  matchrec = rsnullscan;

 }

}

После установки и сохранения RS_is_null ее можно протестировать в коде и вывести из-под отладчика.

ЗАМЕЧАНИЕ . Начиная с GCC 3.1 и версии 5 GDB, если вы компилируете свою программу с опциями -gdwarf-2 и -g3 , вы можете использовать макросы из-под GDB. В руководстве по GDB утверждается, что разработчики GDB надеются найти в конце концов более компактное представление для макросов, и что опция -g3 будет отнесена к группе -g .

Однако, использовать макросы таким способам позволяет лишь комбинация GCC, GDB и специальных опций: если вы не используете GCC (или если вы используете более старую версию), у вас все еще есть проблема. Мы придерживаемся своей рекомендации избегать по возможности таких макросов.

Проблема с макросами распространяется также и на фрагменты кода. Если макрос определяет несколько операторов, вы не можете установить контрольную точку в середине макроса. Это верно также для inline-функций C99 и С++: если компилятор заменяет тело inline-функции сгенерированным кодом, снова невозможно или трудно установить внутри него контрольную точку. Это имеет связь с нашим советом компилировать лишь с одной опцией -g; в этом случае компиляторы обычно не используют inline-функции.

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

/* Различные состояния, в которых можно

   находиться при поиске конца записи. */

#define NOSTATE  1 /* сканирование еще не началось (все) */

#define INLEADER 2 /* пропуск начальных данных (RS = "") */

#define INDATA   3 /* в теле записи (все) */

#define INTERM   4 /* терминатор сканирования (RS = RS = regexp) */

int state;

...

state = NOSTATE;

...

state = INLEADER;

...

if (state != INTERM) ...

На уровне исходного кода это выглядит замечательно. Но опять-таки, есть проблема, когда вы пытаетесь просмотреть код из GDB:

(gdb) print state

$1 = 2

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

Рекомендация: Для определения именованных констант используйте вместо макросов перечисления (enum). Использование исходного кода такое же, а значения enum может выводить также и отладчик.

Пример, тоже из io.c в gawk:

typedef enum scanstate {

 NOSTATE,  /* сканирование еще не начато (все) */

 INLEADER, /* пропуск начальных данных (RS = "") */

 INDATA,   /* в теле записи (все) */

 INTERM,   /* терминатор сканирования (RS = "", RS = regexp) */

} SCANSTATE;

SCANSTATE state;

/* ... остальной код без изменений! ... */

Теперь при просмотре state из GDB мы видим что-то полезное:

(gdb) print state

$1 = NOSTATE

 

15.4.1.3. При необходимости переставляйте код

Довольно часто условие в if или while состоит из нескольких проверок, разделенных && или ||. Если эти проверки являются вызовами функций (или даже не являются ими), невозможно осуществить пошаговое прохождение каждой отдельной части условия. Команды GDB step и next работают на основе операторов (statements), а не выражений (expressions). (Разнесение их по нескольким строкам все равно не помогает).

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

Вот конкретный пример: функция do_input() из файла io.c gawk:

1  /* do_input --- главный цикл обработки ввода */

2

3  void

4  do_input()

5  {

6   IOBUF *iop;

7   extern int exiting;

8   int rval1, rval2, rval3;

9

10  (void)setjmp(filebuf); /* for 'nextfile' */

11

12  while ((iop = nextfile(FALSE)) != NULL) {

13   /*

14    * Здесь было:

15    if (inrec(iop) == 0)

16     while (interpret(expression_value) && inrec(iop) == 0)

17      continue;

18    * Теперь развернуто для простоты отладки.

19    */

20   rvall = inrec(iop);

21   if (rvall == 0) {

22    for (;;) {

23     rval2 = rval3 = -1; /* для отладки */

24     rval2 = interpret(expression_value);

25     if (rval2 != 0)

26      rval3 = inrec(iop);

27     if (rval2 == 0 || rval3 != 0)

28      break;

29    }

30   }

31   if (exiting)

32    break;

33  }

34 }

(Номера строк приведены относительно начала этой процедуры, а не файла.) Эта функция является основой главного цикла обработки gawk. Внешний цикл (строки 12 и 33) проходит через файлы данных командной строки. Комментарий в строках 13–19 показывает оригинальный код, который читает из текущего файла каждую запись и обрабатывает ее

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

Строки 20–30 представляют переписанный код, который вызывает каждую функцию отдельно, сохраняя возвращаемые значения в локальных переменных, чтобы их можно было напечатать из отладчика. Обратите внимание, как в строке 23 этим переменным каждый раз присваиваются известные, ошибочные значения: в противном случае они могли бы сохранить свои значения от предыдущих итераций цикла. Строка 27 является тестом завершения, поскольку код изменился, превратившись в бесконечный цикл (сравните строку 22 со строкой 16), тест завершения цикла является противоположным первоначальному.

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

/* Возможная замена для строк 22 - 29 */

do {

 rval2 = rval3 = -1; /* для отладки */

 rval2 = interpret(expression_value);

 if (rval2 != 0)

  rval3 = inrec(iop);

} while (rval2 != 0 && rval3 == 0);

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

Наконец, мы обращаем внимание, что не все программисты-эксперты согласились бы здесь с нашим советом. Когда каждый компонент условия является вызовом функции, можно установить на каждую контрольную точку, использовать step для входа в каждую функцию, а затем использовать finish для ее завершения. GDB сообщит вам возвращаемое функцией значение, и с этого места вы можете использовать для продолжения cont или step. Нам нравится наш подход, поскольку результаты сохраняются в переменных, которые можно проверить (и неоднократно) после вызова функции и даже спустя несколько операторов.

 

15.4.1.4. Используйте вспомогательные функции отладки

Типичной методикой, применимой во многих случаях, является использование набора значений флагов; когда флаг установлен (т.е. равен true), имеет место определенный факт или применяется определенное условие. Обычно это осуществляется при помощи именованных констант #define и битовых операторов С. (Использование битовых флагов и операторы работы с битами мы обсуждали во врезке к разделу 8.3.1 «Стиль POSIX: statvfs() и fstatvfs()».)

Например, главная структура данных gawk называется NODE. У нее большое количество полей, последнее из которых является набором значений флагов. Из файла awk.h:

typedef struct exp_node {

 /* ... Куча материала опущена */

 unsigned short flags;

#define MALLOC       1 /* может быть освобожден */

#define TEMP         2 /* должен быть освобожден */

#define PERM         4 /* не может быть освобожден */

#define STRING       8 /* назначен в виде строки */

#define STRCUR      16 /* текущее значение строковое */

#define NUMCUR      32 /* текущее значение числовое */

#define NUMBER      64 /* назначен в виде числа */

#define MAYBE_NUM  128 /* ввод пользователя: если NUMERIC, тогда

                        * NUMBER */

#define ARRAYMAXED 256 /* размер массива максимальный */

#define FUNC       512 /* параметр представляет имя функции;

                        * см. awkgram.y */

#define FIELD     1024 /* это является полем */

#define INTLSTR   2048 /* использовать локализованную версию */

} NODE;

Причина для использования значений флагов заключается в том, что они значительно экономят пространство данных. Если бы структура NODE для каждого флага использовала отдельное поле char, потребовалось бы 12 байтов вместо 2, используемых unsigned short. Текущий размер NODE (на Intel x86) 32 байта. Добавление лишних 10 байтов увеличило бы ее до 42 байтов. Поскольку gawk может потенциально выделять сотни и тысячи (или даже миллионы) NODE, сохранение незначительного размера является важным.

Что это должно делать с отладкой? Разве мы не рекомендовали только что использовать для именованных констант enum? Ну, в случае объединяемых побитовыми ИЛИ значений enum не помогают, поскольку они больше не являются индивидуально распознаваемыми!

Рекомендация: предусмотрите функцию для преобразования флагов в строки. Если у вас есть несколько независимых флагов, установите процедуру общего назначения.

ЗАМЕЧАНИЕ . Необычность этих функций отладки заключается в том, что код приложения никогда их не вызывает. Они существуют лишь для того, чтобы их можно было вызывать из отладчика. Такие функции всегда должны быть откомпилированы с кодом, даже без окружающих #ifdef , чтобы их можно было использовать. не предпринимая никаких дополнительных шагов. Увеличение (обычно минимальное) размера кода оправдывается экономией времени разработчика

Сначала мы покажем вам, как мы это делали первоначально. Вот (сокращенная версия) flags2str() из ранней версии gawk (3.0.6):

1  /* flags2str --- делает значения флагов удобочитаемыми */

2

3  char *

4  flags2str(flagval)

5  int flagval;

6  {

7   static char buffer[BUFSIZ];

8   char *sp;

9

10  sp = buffer;

11

12  if (flagval & MALLOC) {

13   strcpy(sp, "MALLOC");

14   sp += strlen(sp);

15  }

16  if (flagval & TEMP) {

17   if (sp >= buffer)

18   *sp++ = '|';

19   strcpy(sp, "TEMP");

20   sp += strlen(sp);

21  }

22  if (flagval & PERM) {

23   if (sp != buffer)

24    *sp++ = '|';

25   strcpy(sp, "PERM");

26   sp += strlen(sp);

27  }

    /* ...многое то же самое, опущено для краткости... */

82

83  return buffer;

84 }

(Номера строк даны относительно начала функции.) Результатом является строка, что- то наподобие "MALLOC | PERM | NUMBER". Каждый флаг тестируется отдельно, и если он присутствует, действие каждый раз одно и то же: проверка того, что он не в начале буфера и что можно добавить символ '|', скопировать строку на место и обновить указатель. Сходные функции существовали для форматирования и отображения других видов флагов в программе.

Этот код является повторяющимся и склонным к ошибкам, и для gawk 3.1 мы смогли упростить и обобщить его. Вот как gawk делает это сейчас. Начиная с этого определения в awk.h:

/* для целей отладки */

struct flagtab {

 int val;          /* Целое значение флага */

 const char *name; /* Строковое имя */

};

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

/* flags2str --- делает значения флагов удобочитаемыми */

const char *flags2str(int flagval) {

 static const struct flagtab values[] = {

  { MALLOC, "MALLOC" },

  { TEMP, "TEMP" },

  { PERM, "PERM" },

  { STRING, "STRING" },

  { STRCUR, "STRCUR" },

  { NUMCUR, "NUMCUR" },

  { NUMBER, "NUMBER" },

  { MAYBE_NUM, "MAYBE_NUM" },

  { ARRAYMAXED, "ARRAYMAXED" },

  { FUNC, "FUNC" },

  { FIELD, "FIELD" },

  { INTLSTR, "INTLSTR" },

  { 0, NULL },

 };

 return genflags2str(flagval, values);

}

flags2str() определяет массив сопоставлений флагов со строками. По соглашению, значение флага 0 означает конец массива. Код вызывает для осуществления работы genflags2str() («общий флаг в строку»). getflags2str() является процедурой общего назначения, которая преобразует значение флага в строку. Из eval.c:

1  /* genflags2str --- общая процедура для преобразования значения флага в строковое представление */

2

3  const char *

4  genflags2str(int flagval, const struct flagtab *tab)

5  {

6   static char buffer(BUFSIZ];

7   char *sp;

8   int i, space_left, space_needed;

9

10  sp = buffer;

11  space_left = BUFSIZ;

12  for (i = 0; tab[i].name != NULL; i++) {

13   if ((flagval & tab[i].val) != 0) {

14    /*

15     * обратите внимание на уловку, нам нужны 1 или 0, чтобы

16     * определить, нужен ли нам символ '|'.

17     */

18    space_needed = (strlen(tab[i].name) + (sp != buffer));

19    if (space_left < space_needed)

20     fatal(_("buffer overflow in genflags2str"));

21

22    if (sp >= buffer) {

23     *sp++ = '|';

24     space_left--;

25    }

26    strcpy(sp, tab[i].name);

27    /* обратите внимание на расположение! */

28    space_left -= strlen(sp);

29    sp += strlen(sp);

30   }

31  }

32

33  return buffer;

34 }

(Номера строк приведены относительно начала функции, а не файла.) Как и в предыдущей версии, идея заключалась в заполнении статического буфера строковыми значениями, такими, как "MALLOC | PERM | STRING | MAYBE_NUM", и возвращении адреса этого буфера. Мы вскоре обсудим причины использования статического буфера; сначала давайте исследуем код.

Указатель sp отслеживает положение следующего пустого слота в буфере, тогда как space_left отслеживает количество оставшегося места; это уберегает нас от переполнения буфера.

Основную часть функции составляет цикл (строка 12), проходящий через массив значений флагов. Когда флаг найден (строка 13), код вычисляет, сколько места требуется строке (строка 18) и проверяет, осталось ли столько места (строки 19–20).

Тест 'sp ! = buffer' для первого значения флага завершается неудачей, возвращая 0. Для последующих флагов тест дает значение 1. Это говорит нам, что между значениями должен быть вставлен разделительный символ '|'. Добавляя результат (1 или 0) к длине строки, мы получаем правильное значение space_needed. Тот же тест с той же целью проводится в строке 22 для проверки строк 23 и 24, которые вставляют символ '|'.

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

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

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

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

На практике фиксированный размер также не является проблемой; мы знаем, что размер BUFSIZ достаточен для представления всех флагов, которые мы используем. Тем не менее, поскольку мы опытные и знаем, что вещи могут измениться, в getflags2str() есть код, предохраняющий себя от переполнения буфера. (Переменная space_left и код в строках 18–20.)

В качестве отступления, использование BUFSIZ спорно. Эта константа должна использоваться исключительно для буферов ввода/вывода, но часто она используется также для общих строковых буферов. Такой код лучше убрать, определив явные константы, такие, как FLAGVALSIZE, и использовав в строке 11 'sizeof (buffer)'.

Вот сокращенный сеанс GDB, показывающий использование flags2str():

$ gdb gawk /* Запустить GDB с gawk */

GNU gdb 5.3

...

(gdb) break do_print /* Установить контрольную точку */

Breakpoint 1 at 0x805a584: file builtin.c, line 1547.

(gdb) run 'BEGIN { print "hello, world" }' /* Запустить программу */

Starting program: /home/arnold/Gnu/gawk/gawk-3.1.4/gawk 'BEGIN { print "hello, world" }'

Breakpoint 1, do_print (tree=0x80955b8) at builtin.c: 1547 /* Останова в контрольной точке */

1547 struct redirect *rp = NULL;

(gdb) print *tree /* Вывести NODE */

$1 = {sub = {nodep =

 {1 = {lptr = 0x8095598, param_name = 0x8095598 "xU\t\b",

 ll = 134629464}, r = {rptr = 0x0, pptr = 0, preg = 0x0, hd = 0x0,

 av = 0x0, r_ent =0}, x = {extra = 0x0, xl = 0, param_list = 0x0},

 name = 0x0, number = 1, reflags = 0), val = {

 fltnum = 6.6614606209589101e-316, sp = 0x0, slen = 0, sref = 1,

 idx = 0}, hash = {next = 0x8095598, name = 0x0, length = 0, value = 0x0,

 ref = 1}}, type = Node_K_print, flags = 1}

(gdb) print flags2str(tree->flags) /* Вывести значение флага */

$2 = 0x80918a0 "MALLOC"

(gdb) next /* Продолжить */

1553 fp = redirect_to_fp(tree->rnode, &rp);

...

1588 efwrite(t[i]->stptr, sizeof(char), t[i]->stlen, fp, "print", rp, FALSE);

(gdb) print *t[i] /* Снова вывести NODE */

$4 = {sub = {nodep =

 {l = {lptr = 0x8095598, parm_name = 0x8095598 "xU\t\b",

 ll = 134829464}, r = {rptr = 0x0, pptr = 0, preg = 0x0, hd = 0x0,

 av = 0x0, r_ent =0), x = {extra = 0x8095ad8, xl = 134830808,

 param_list = 0x8095ad8}, name = 0xc

,

 number = 1, reflags = 4294967295}, val = {

 fltnum = 6.6614606209589101e-316, sp = 0x8095ad8 "hello, world",

 slen = 12, sref = 1, idx = -1}, hash = {next = 0x8095598, name = 0x0,

 length = 134830808, value = 0xc, ref = 1}}, type = Node_val, flags = 29}

(gdb) print flags2str(t[i]->flags) /* Вывести значение флага */

$5 = 0x80918a0 "MALLOC|PERM|STRING|STRCUR"

Надеемся, вы согласитесь, что настоящий механизм общего назначения значительно более элегантный и более простой в использовании, чем первоначальный.

Тщательное проектирование и использование массивов структур часто может заменить или слить воедино повторяющийся код.

 

15.4.1.5. По возможности избегайте объединений

union С относительно эзотерическая возможность. Она помогает экономить память, сохраняя различные элементы в одном и том же физическом пространстве; как программа интерпретирует его, зависит от способа доступа:

/* ch15-union.c --- краткая демонстрация использования union. */

#include

int main(void) {

 union i_f {

  int i;

  float f;

 } u;

 u.f = 12.34; /* Присвоить значение с плавающей точкой */

 printf("%f also looks like %#x\n", u.f, u.i};

 exit(0);

}

Вот что происходит, когда программа запускается на системе Intel x86 GNU/Linux:

$ ch15-union

12.340000 also looks like 0x414570a4

Программа выводит битовый паттерн, который представляет число с плавающей точкой в виде шестнадцатеричного целого. Оба поля занимают одно и то же место в памяти; разница в том, как этот участок памяти интерпретируется: u.f действует, как число с плавающей точкой, тогда как эти же биты в u.i действуют, как целое число.

Объединения особенно полезны в компиляторах и интерпретаторах, которые часто создают древовидные структуры, представляющие структуру файла с исходным кодом (которая называется деревом грамматического разбора (parse tree)). Это моделирует то, как формально описаны языки программирования: операторы if, операторы while, операторы присваивания и так далее для всех экземпляров более общего типа «оператора». Таким образом, в компиляторе могло бы быть нечто подобное этому:

struct if_stmt { ... }; /* Структура для оператора IF */

struct while_stmt { ... }; /* Структура для оператора WHILE */

struct for_stmt { ... }; /* Структура для оператора */

/* ...структуры для других типов операторов... */

typedef enum stmt_type {

 IF, WHILE, FOR, ...

} TYPE; /* Что у нас есть в действительности */

/* Здесь содержатся тип и объединения отдельных видов операторов. */

struct statement {

 TYPE type;

 union stmt {

  struct if_stmt if_st;

  struct while_stmt while_st;

  struct for_stmt for_st;

  ...

 } u;

};

Вместе с объединением удобно использовать макрос, который представляет компоненты объединения, как если бы они были полями структуры. Например:

#define if_s u.if_st /* Так что можно использовать s->if_s вместо s->u.if_st */

#define while_s u.while_st /* И так далее... */

#define for_s u.for_st

...

На только что представленном уровне это кажется разумным и выглядит осуществимым. В действительности, однако, все сложнее, и в реальных компиляторах и интерпретаторах часто есть несколько уровней вложенных структур и объединений. Сюда относится и gawk, в котором определение NODE, значение его флагов и макросов для доступа к компонентам объединения занимают свыше 120 строк! Здесь достаточно определений, чтобы дать вам представление о том, что происходит:

typedef struct exp_node {

 union {

  struct {

   union {

    struct exp_node *lptr;

    char *param_name;

    long ll;

   } l;

   union {

    ...

   } r;

   union {

    ...

   } x;

   char *name;

   short number;

   unsigned long reflags;

   ...

  } nodep;

  struct {

   AWKNUM fltnum;

   char *sp;

   size_t slen;

   long sref;

   int idx;

  } val;

  struct {

   struct exp_node *next;

   char *name;

   size_t length;

   struct exp_node *value;

   long ref;

  } hash;

#define hnext sub.hash.next

#define hname sub.hash.name

#define hlength sub.hash.length

#define hvalue sub.hash.value

  ...

 } sub;

 NODETYPE type;

 unsigned short flags;

 ...

} NODE;

#define vname sub.nodep.name

#define exec_count sub.nodep.reflags

#define lnode sub.nodep.l.lptr

#define nextp sub.nodep.l.lptr

#define source_file sub.nodep.name

#define source_line sub.nodep.number

#define param_cnt sub.nodep.number

#define param sub.nodep.l.param_name

#define stptr sub.val.sp

#define stlen sub.val.slen

#define stref sub.val.sref

#define stfmt sub.val.idx

#define var_value lnode

...

В NODE есть объединение внутри структуры внутри объединения внутри структуры! (Ой.) Поверх всего этого многочисленные «поля» макросов соответствуют одним и тем же компонентам struct/union в зависимости от того, что на самом деле хранится в NODE! (Снова ой.)

Преимуществом такой сложности является то, что код С сравнительно ясный. Нечто вроде 'NF_node->var_value->slen' читать просто.

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

Например, сравните 'NF_node->var_value->slen' с развернутой формой: 'NF_node->sub.nodep.l.lptr->sub.val.slen'! Чтобы увидеть значение данных, вы должны набрать последнее в GDB. Взгляните снова на это извлечение из приведенного ранее сеанса отладки GDB:

(gdb) print *tree /* Вывести NODE */

$1 = {sub = {nodep =

 {1 = {lptr = 0x8095598, param_name = 0x8095598 "xU\t\b",

 ll = 134829464}, r = {rptr = 0x0, pptr = 0, preg = 0x0,

 hd = 0x0, av = 0x0, r_ent =0), x = {extra = 0x0, xl = 0,

 param_list = 0x0}, name = 0x0, number = 1, reflags = 0},

 val = { fltnum = 6.6614606209589101e-316, sp = 0x0,

 slen = 0, sref = 1, idx = 0),

 hash = {next = 0x8095598, name = 0x0, length = 0,

 value = 0x0, ref = 1}}, type = Node_K_print, flags = 1}

Это куча вязкой массы. Однако, GDB все же несколько упрощает ее обработку. Вы можете использовать выражения вроде '($1).sub.val.slen', чтобы пройти через дерево и перечислить структуры данных.

Есть другие причины для избегания объединений. Прежде всего, объединения не проверяются. Ничто, кроме внимания программиста, не гарантирует, что когда вы получаете доступ к одной части объединения, вы получаете доступ к той части, которая была сохранена последней. Мы видели это в ch15-union.c, в котором доступ к обоим «элементам» объединения осуществлялся одновременно.

Вторая причина, связанная с первой, заключается в осторожности с перекрытиями вложенных комбинаций struct/union. Например, в предыдущей версии gawk был такой код.

/* n->lnode перекрывает размер массива, не вызывайте unref, если это массив */

if (n->type != Node_var_array && n->type != Node_array_ref)

unref(n->lnode);

Первоначально if не было, был только вызов unref(), которая освобождает NODE, на которую указывает n->lnode. Однако, в этот момент gawk могла создать аварийную ситуацию. Можете себе представить, сколько времени потребовало отслеживание в отладчике того факта, что то, что рассматривалось как указатель, на самом деле было размером массива!

В качестве отступления, объединения значительно менее полезны в С++. Наследование и объектно-ориентированные возможности создают при управлении структурами данных совсем другую ситуацию, которая значительно безопаснее.

Рекомендация: по возможности избегайте объединений (union). Если это невозможно, тщательно проектируйте и программируйте их!

 

15.4.2. Отлаживаемый код времени исполнения

 

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

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

 

15.4.2.1. Добавляйте отладочные опции и переменные

Простейшей методикой является наличие опции командной строки, делающих возможным отладку. Такая опция может быть условно откомпилированной для отладки. Однако более гибким подходом является оставить опцию в готовой версии программы. (Вы можете также решить, оставлять или не оставлять эту опцию не документированной. Здесь есть различные компромиссы: ее документирование может дать возможность вашим покупателям или клиентам больше изучить внутренности вашей системы, чего вы можете не хотеть С другой стороны, не документирование ее кажется довольно подлым. Если вы пишете для Open Source или Free Software, лучше документировать опцию.)

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

struct option options[] = {

 ...

 { "debug", required_argument, NULL, 'D' },

 ...

};

int main(int argc, char **argv) {

 int c;

 while ((c = getopt_long(argc, argv, "...D:")) != -1) {

  switch (c) {

   ...

  case 'D':

   parse_debug(optarg);

   break;

   ...

  }

 }

 ...

}

Функция parse_debug() считывает строку аргументов. Например, это может быть строка разделенных запятыми или пробелами подсистем, вроде "file,memory,ipc". Для каждого действительного имени подсистемы функция устанавливает бит в отладочной переменной:

extern int debugging;

void parse_debug(const char *subsystems) {

 char *sp;

 for (sp = subsystems; *sp != '\0';) {

  if (strncmp(sp, "file", 4) == 0) {

   debugging |= DEBUG_FILE;

   sp += 4;

  } else if (strncmp(sp, "memory", 6) == 0) {

   debugging |= DEBUG_MEM;

   sp += 6;

  } else if (strncmp(sp, "ipc", 3) == 0) {

   debugging |= DEBUG_IPC;

   sp += 3;

   ...

  }

  while (*sp == ' ' || *sp == ',') sp++;

 }

}

В конечном счете код приложения может затем проверить флаги:

if ((debugging & DEBUG_FILE) != 0) ...

 /* В части программы для ввода/вывода */

if ((debugging & DEBUG_MEM) != 0) ... /* В менеджере памяти */

Использовать ли одну переменную с флаговыми битами, различные переменные или даже отладочный массив, индексируемый именованными константами (желательно из enum), зависит от вас.

Ценой оставления отладочного кода в исполняемом файле изделия является увеличение размера программы. В зависимости от размещения отладочного кода он может быть также более медленным, поскольку каждый раз осуществляются проверки, которые все время оказываются ложными, пока не будет включен режим отладки. И, как упоминалось, кто-нибудь может изучить вашу программу, что может быть неприемлемым для вас. Или еще хуже, недоброжелательный пользователь может включить столько отладочных возможностей, что программа замедлится до невозможности работать с ней! (Это называется атакой отказа в обслуживании (denial of service attack).)

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

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

 

15.4.2.2. Используйте специальные переменные окружения

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

gawk использует функцию с названием optimal_bufsize() для получения оптимального размера буфера для ввода/вывода. Для небольших файлов функция возвращает размер файла. В противном случае, если файловая система определяет размер для использования при вводе/выводе, возвращается это значение (член st_blksize структуры struct stat, см. раздел 5.4.2 «Получение информации о файле»). Если этот член недоступен, optimal_bufsize() возвращает константу BUFSIZ из . Оригинальная функция (в posix/gawkmisc.c) выглядела следующим образом:

1  /* optimal_bufsize --- определяет оптимальный размер буфера */

2

3  int

4  optimal_bufsize(fd, stb) /* int optimal_bufsize(int fd, struct stat *stb); */

5  int fd;

6  struct stat *stb;

7  {

8   /* инициализировать все члены нулями на случай, если ОС не использует их все. */

9   memset(stb, '\0', sizeof(struct stat));

10

11 /*

12  * System V.n, n < 4, не имеет в структуре stat размера

13  * системного блока файла. Поэтому нам нужно сделать разумную

14  * догадку. Мы используем BUFSIZ, поскольку именно это имелось

15  * в виду на первом месте.

16  */

17 #ifdef HAVE_ST_BLKSIZE

18 #define DEFBLKSIZE (stb->st_blksize ? stb->st_blksize : BUFSIZ)

19 #else

20 #define DEFBLKSIZE BUFSIZ

21 #endif

22

23  if (isatty(fd))

24   return BUFSIZ;

25  if (fstat(fd, stb) == -1)

26   fatal("can't stat fd %d (%s)", fd, strerror(errno));

27  if (lseek(fd, (off_t)0, 0) == -1) /* не обычный файл */

28   return DEFBLKSIZE;

29  if (stb->st_size > 0 && stb->st_size < DEFBLKSIZE) /* маленький файл */

30   return stb->st_size;

31  return DEFBLKSIZE;

32 }

Константа DEFBLKSIZE является «размером блока по умолчанию»; то есть значением из struct stat или BUFSIZ. Для терминалов (строка 23) или файлов, которые не являются обычными файлами (lseek() завершается неудачей, строка 27) возвращаемое значение также равно BUFSIZ. Для небольших обычных файлов используется размер файла. Во всех других случаях возвращается DEFBLKSIZE. Знание «оптимального» размера буфера особенно полезно в файловых системах, в которых размер блока больше BUFSIZ.

У нас была проблема, когда один из наших контрольных примеров отлично работал на нашей рабочей системе GNU/Linux и на любой другой системе Unix, к которой у нас был доступ. Однако, этот тест последовательно терпел неудачу на других определенных системах.

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

Нам был нужен способ воспроизведения проблемы на своей машине разработки, система с неудачей находилась в стороне за девять часовых поясов, а интерактивный запуск GDB через Атлантический океан мучителен. Мы воспроизвели проблему, заставив optimal_bufsize() проверять значение специальной переменной окружения AWKBUFSIZE. Когда ее значение равно "exact", optimal_bufsize() всегда возвращает размер файла, каким бы он ни был. Если значением AWKBUFSIZE является какое-нибудь целое число, функция возвращает это число. В противном случае, функция возвращается к прежнему алгоритму. Это дает нам возможность запускать тесты, не требуя постоянной перекомпиляции gawk. Например,

$ AWKBUFSIZE=42 make check

Это запускает тестовый набор gawk с использованием размера буфера в 42 байта. (Тестовый набор проходит.) Вот модифицированная версия optimal_bufsize():

1  /* optimal_bufsize --- определение оптимального размера буфера */

2

3  /*

4   * В целях отладки усовершенствуйте это следующим образом:

5   *

6   * Всегда используйте stat для файла, буфер stat используется кодом

7   * более высокого уровня.

8   * if (AWKBUFSIZE == "exact")

9   *  return the file size

10  * else if (AWKBUFSIZE == число)

11  *  всегда возвращать это число

12  * else

13  *  if размер < default_blocksize

14  *   return размер

15  *  else

16  *   return default_blocksize

17  *  end if

18  * end if

19  *

20  * Приходится повозиться, чтобы иметь дело с AWKBUFSIZE лишь

21  * однажды, при первом вызове этой процедуры, а не при каждом

22  * ее вызове. Производительность, знаете ли.

23  */

24

25 size_t

26 optimal_bufsize(fd, stb)

27 int fd;

28 struct stat *stb;

29 {

30  char *val;

31  static size_t env_val = 0;

32  static short first = TRUE;

33  static short exact = FALSE;

34

35  /* обнулить все члены, на случай, если ОС их не использует. */

36  memset(stb, '\0', sizeof(struct stat));

37

38  /* всегда использовать stat на случай, если stb используется кодом более высокого уровня */

39  if (fstat(fd, stb) == -1)

40   fatal("can't stat fd %d (%s)", fd, strerror(errno));

41

42  if (first) {

43   first = FALSE;

44

45   if ((val = getenv("AWKBUFSIZE")) != NULL) {

46    if (strcmp(val, "exact") == 0)

47     exact = TRUE;

48    else if (ISDIGIT(*val)) {

49     for (; *val && ISDIGIT(*val); val++)

50     env_val = (env_val * 10) + *val - '0';

51

52     return env_val;

53    }

54   }

55  } else if (!exact && env_val > 0)

56   return env_val;

57  /* else

58     обрабатывать дальше */

59

60  /*

61   * System V.n, n < 4, не имеет в структуре stat размера системного

62   * блока файла. Поэтому нам нужно осуществить разумную догадку.

63   * Мы используем BUFSIZ из stdio, поскольку именно это имелось

64   * в виду прежде всего.

65   */

66 #ifdef HAVE_ST_BLKSIZE

67 #define DEFBLKSIZE (stb->st_blksize > 0 ? stb->st_blksize : BUFSIZ)

68 #else

69 #define DEFBLKSIZE BUFSIZ

70 #endif

71

72  if (S_ISREG(stb->st_mode) /* обычный файл */

73   && 0 < stb->st_size /* ненулевой размер */

74   && (stb->st_size < DEFBLKSIZE /* маленький файл */

75   || exact)) /* или отладка */

76   return stb->st_size; /* использовать размер файла*/

77

78  return DEFBLKSIZE;

79 }

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

Строки 42–54 выполняются лишь при первом вызове функции. Строка 43 обеспечивает это условие, устанавливая в first значение false. Строки 45–54 обрабатывают переменную окружения, разыскивая либо строку "exact", либо число. В последнем случае оно преобразуется из строкового значения в десятичное, сохраняясь в env_val. (Возможно, нам следовало бы использовать здесь strtoul(); в свое время это не пришло нам на ум.)

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

Строки 60–70 определяют DEFBLKSIZE; эта часть не изменилась. Наконец, строки 72–76 возвращают размер файла, если это приемлемо. Если нет (строка 78), возвращается DEGBLKSIZE.

Мы действительно устранили проблему, но между тем оставили на месте новую версию optimal_bufsize(), чтобы можно было убедиться, что проблема не возникнет вновь.

Незначительное увеличение размера кода и его сложности более чем компенсируется возросшей гибкостью, которая есть теперь у нас для тестирования. Более того, поскольку это код изделия, пользователь в полевых условиях может с легкостью использовать эту особенность для тестирования, чтобы определить, не появилась ли сходная проблема. (До сих пор нам не приходилось просить проделать этот тест, но приятно осознавать, что мы могли бы это сделать, если бы пришлось.)

 

15.4.2.3. Добавьте код журналирования

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

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

Недостаток в том, что в какой-то момент регистрационный файл займет все дисковое пространство. Следовательно, у вас должны быть несколько файлов журналов, причем программа периодически должна переключаться между ними. Брайан Керниган рекомендует называть файлы журнала по дням недели: myapp.log.sun, myapp.log.mon и т.д. Преимуществом здесь является то, что вам не придется вручную удалять старые файлы; вы бесплатно получаете недельную стоимость файлов журналов.

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

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

• В качестве альтернативы можно создать какую-нибудь разновидность XML, который является самоописывающимся и допускающим преобразование в другие форматы. (Мы не являемся большими поклонниками XML, но вас это не должно останавливать).

• Для журналирования используйте syslog(); конечное расположение сообщений журналирования может контролироваться системным администратором, (syslog() является довольно продвинутым интерфейсом; см. справочную страницу syslog(3)).

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

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

 

15.4.2.4. Файлы отладки времени исполнения

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

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

struct stat sbuf;

extern int do_logging; /* инициализировано нулями */

if (stat("/path/to/magic/.file", &sbuf) == 0)

 do_logging = TRUE;

...

if (do_logging) {

 /* здесь код журналирования: открытие файла, запись, закрытие и

  * т.д. * /

}

Вызов stat() происходил для каждого задания, которое выполняла программа. Таким образом, мы могли динамически включать и отключать журналирование без необходимости останавливать и повторно запускать приложение!

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

ЗАМЕЧАНИЕ . Не все то золото, что блестит. Специальные отладочные файлы являются лишь одним примером методик, известных как лазейки (back doors) — один или более способов выполнения разработчиками недокументированных вещей с программой, обычно с бесчестными намерениями. В нашем примере лазейка была исключительно доброкачественной. Но беспринципный разработчик легко мог бы устроить создание и загрузку скрытой копии списка клиентов, картотеки персонала или других важных данных. По одной этой причине вы должны серьезно подумать, применима ли эта методика в вашем приложении.

 

15.4.2.5. Добавьте специальные ловушки для контрольных точек

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

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

Решение заключается в добавлении специальных отладочных функций «ловушек» («hook»), которые ваша программа может вызвать при приближении к интересующему вас состоянию.

Например, предположим, что вы знаете, что функция check_salary() вызывает сбой, но лишь когда она вызвана 1427 раз. (Мы не смеемся над вами; в свое время нам пришлось наблюдать довольно странные вещи.)

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

/* debug_dummy --- отладочная функция-ловушка */

void debug_dummy(void) { return; }

struct salary *check_salary(void) {

 /* ...здесь описания настоящих переменных... */

 static int count = 0; /* для отладки */

 if (++count == 1426)

  debug_dummy();

 /* ...оставшаяся часть кода... */

}

Теперь из GDB установите контрольную точку в debug_dummy(), а затем запустите программу обычным способом:

(gdb) break debug_dummy /* Установить контрольную точку для фиктивной функции */

Breakpoint 1 at 0x8055885: file whizprog.c, line 3137.

(gdb) run /* Запуск программы */

По достижении контрольной точки для debug_dummy() вы можете установить вторую контрольную точку для check_salary() и продолжить исполнение:

(gdb) run /* Запуск программы */

Starting program: /home/arnold/whizprog

Breakpoint 1, debug_dummy() at whizprog.c, line 3137

3137 void debug_dummy(void) { return; } /* Достижение контрольной точки */

(gdb) break check_salary

 /* Установить контрольную точку для интересующей функции */

Breakpoint 2 at 0x8057913: file whizprog.c, line 3140.

(gdb) cont

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

Вместо использования фиксированной константы ('++count == 1426') можно использовать глобальную переменную, которая устанавливается отладчиком в любое нужное вам значение. Это дает возможность избежать перекомпилирования программы

Для gawk мы пошли на один шаг дальше и внесли возможность отладочной ловушки в язык, так что функция ловушки могла быть вызвана из программы awk. При компилировании для отладки доступна специальная ничего не делающая функция stopme(). Эта функция, в свою очередь, вызывает функцию С с тем же названием. Это позволяет нам поместить вызовы stopme() в завершающуюся неудачей программу awk непосредственно перед сбойным участком. Например, если gawk выдает ошибочные результаты для программы awk в 1200-й вводимой записи, мы можем добавить в программу awk строку, подобную этой:

NR == 1198 { stopme() } # Остановиться для отладки, когда число записей == 1198

/* ...оставшаяся часть программы как ранее... */

Затем из GDB мы можем установить контрольную точку на функции С stopme() и запустить программу awk. Когда контрольная точка срабатывает, мы можем затем установить контрольные точки на другие части gawk, где, как мы ожидаем, находится действительная проблема.

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

 

15.5. Отладочные инструменты

 

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

Имеются коммерческие инструменты, которые делают множество (или все) из тех вещей, что и описываемые нами программы, но не все они доступны для GNU/Linux, а многие довольно дороги. Все пакеты, обсуждающиеся в данном разделе, являются свободно доступными.

 

15.5.1. Библиотека

dbug

— усовершенствованный

printf()

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

Библиотека dbug, написанная Фредом Фишем (Fred Fish) в начале 1980-х, была с тех пор несколько усовершенствована. Теперь она явным образом является общим достоянием, поэтому ее можно использовать без всяких проблем как в свободном, так и частном программном обеспечении. Она доступна через архив FTP Фреда Фиша как в виде сжатого файла tar, так и в виде архива ZIP. Документация хорошо резюмирует dbug:

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

Пакет dbug лишь незначительно снижает скорость выполнения программ, обычно значительно менее 10%, и немного увеличивает их размеры, обычно от 10 до 20%. Определив особый идентификатор препроцессора С, можно снизить оба этих показателя до нуля без необходимости изменений в исходном коде.

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

• Трассировка исполнения, отображающая уровень потока управления полуграфическим способом с использованием отступов, обозначающих глубину вложения

• Вывод значений всех или любого набора ключевых внутренних переменных.

• Ограничение действий определенным набором указанных функций.

• Ограничение трассировки функций указанной глубиной вложения.

• Пометку каждой выводимой строки названием исходного файла и номером строки.

• Пометку каждой выводимой строки названием текущего процесса.

• Сохранение в стеке или восстановление состояния отладки для обеспечения исполнения со встроенными значениями по умолчанию для отладки.

• Перенаправление потока вывода отладки в стандартный вывод ( stdout ) или указанный файл. По умолчанию поток вывода направляется в стандартную ошибку ( stderr ). Механизм перенаправления полностью независим от обычного перенаправления командной строки, чтобы избежать конфликтов вывода.

Пакет dbug требует от вас использования определенного порядка при написании своего кода. В частности, нужно использовать его макросы при возвращении из функции или вызове setjmp() и longjmp(). Нужно добавлять один вызов макроса в качестве первого исполняемого оператора каждой функции и вызвать несколько дополнительных макросов из main(). Наконец, нужно добавить отладочную опцию командной строки, по соглашению, это -#, которая редко используется в качестве действительной опции, если вообще используется. В обмен на дополнительную работу вы получаете все только что очерченные преимущества. Давайте взглянем на пример в руководстве:

1  #include

2  #include "dbug.h"

3

4  int

5  main(argc, argv)

6  int argc;

7  char *argv[];

8  {

9   register int result, ix;

10  extern int factorial(), atoi();

11

12  DBUG_ENTER("main");

13  DBUG_PROCESS(argv[0]);

14  DBUG_PUSH_ENV("DBUG");

15  for (ix = 1; ix < argc && argv[ix][0] == '-'; ix++) {

16   switch (argv[ix][1]) {

17   case '#':

18    DBUG_PUSH(&(argv[ix][2]));

19    break;

20   }

21  }

22  for (; ix < argc; ix++) {

23   DBUG_PRINT("args", ("argv[%d] = %s", ix, argv[ix]));

24   result = factorial(atoi(argv(ixj));

25   printf("%d\n", result);

26   fflush(stdout);

27  }

28  DBUG_RETURN(0);

29 }

Эта программа иллюстрирует большинство важных моментов. Макрос DBUG_ENTER() (строка 12) должен быть вызван после объявлений переменных и перед любым другим кодом. (Это потому, что он сам объявляет несколько частных переменных.)

Макрос DBUG_PROCESS() (строка 13) устанавливает имя программы, главным образом, для использования в выводимых библиотекой сообщениях. Этот макрос должен вызываться лишь однажды, из main().

Макрос DBUG_PUSH_ENV() (строка 14) заставляет библиотеку проверить указанную переменную окружения (в данном случае DBUG) на предмет управляющей строки (Управляющие строки dbug вскоре будут рассмотрены.) Библиотека может, сохранив свое текущее состояние и использовав новое, создавать стек сохраненных состояний. Таким образом, этот макрос помещает в стек сохраненных состояний полученное от данной переменной окружения состояние. В данном примере использован случай, когда макрос создает первоначальное состояние. Если такой переменной окружения нет, ничего не происходит. (В качестве отступления, DBUG является довольно общей переменной, возможно, GAWK_DBUG было бы лучше [для gawk].)

Макрос DBUG_PUSH (строка 18) передает значение управляющей строки, полученной из опции командной строки -#. (Новый код должен использовать getopt() или getopt_long() вместо ручного анализа аргументов.) Таким образом обычно включается режим отладки, но использование переменной окружения предоставляет также дополнительную гибкость.

Макрос DBUG_PRINT() (строка 23) осуществляет вывод. Второй аргумент использует методику, которую мы описали ранее (см. раздел 15.4.1.1 «Используйте отладочные макросы»), по включению в скобки всего списка аргументов printf(), делая его простым аргументом, насколько это касается препроцессора С. Обратите внимание, что завершающий символ конца строки в форматирующей строке не указывается; библиотека dbug вставляет его за вас.

При печати dbug по умолчанию выводит все операторы DBUG_PRINT(). Первый аргумент является строкой, которая может использоваться для ограничения вывода лишь теми макросами DBUG_PRINT(), которые используют эту строку.

Наконец, макрос DBUG_RETURN() (строка 28) используется вместо обычного оператора return для возврата значения. Для использования с функциями void имеется соответствующий макрос DBUG_VOID_RETURN.

Оставшаяся часть программы заполнена функцией factorial():

1  #include

2  #include "dbug.h"

3

4  int factorial (value)

5  register int value;

6  {

7   DBUG_ENTER("factorial");

8   DBUG_PRINT("find", ("find %d factorial", value));

9   if (value > 1) {

10   value *= factorial(value — 1);

11  }

12  DBUG_PRINT("result", ("result is %d", value));

13  DBUG_RETURN(value);

14 }

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

$ factorial 1 2 3 /* Обычный запуск, без отладки */

1

2

6

$ factorial -#t 1 2 3 /* Вывести трассировку вызовов функций, обратите внимание на вложенность */

| >factorial

|

1 /* Обычный вывод в stdout */

| >factorial

| | >factorial

| |

|

2

| >factorial

| | >factorial

| | | >factorial

| | |

| |

|

6

$ factorial -#d 1 2 /* Показать отладочные сообщения DBUG_PRINT() */

?func?: args: argv[2] = 1

factorial: find: find 1 factorial

factorial: result: result is 1

1

?func?: args: argv[3] = 2

factorial: find: find 2 factorial

factorial: find: find 1 factorial

factorial: result: result is 1

factorial: result: result is 2

2

Опция -# управляет библиотекой dbug. Она «особая» в том смысле, что DBUG_PUSH() будет принимать всю строку, игнорируя ведущие символы '-#', хотя вы могли бы использовать при желании другую опцию, передав DBUG_PUSH() лишь строку аргументов опций (если вы используете getopt(), это optarg).

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

$ myprog -#d,mem,ipc:f,check_salary,check_start_date -f infile -o outfile

Опция d включает вывод DBUG_PRINT(), но лишь если первая строка аргумента является "mem" или "ipc". (Если аргументов нет, выводятся все сообщения DBUG_PRINT().) Сходным образом опция f ограничивает трассировку вызовов функций лишь указанными функциями, check_salary() и check_start_date().

Следующий список опций и аргументов воспроизведен из руководства библиотеки dbug. Квадратные скобки заключают необязательные аргументы. Мы включаем здесь лишь те, которые находим полезными; полный список см. в документации.

d [,ключевые слова]

Разрешает вывод от макросов с указанными ключевыми словами. Пустой список ключевых слов предполагает, что выбраны все ключевые слова.

F

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

i

Идентифицирует процесс, выводящий каждую отладочную или трассировочную строку номером ID для этого процесса.

L

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

о[,файл]

Перенаправляет поток вывода отладчика в указанный файл. Потоком вывода по умолчанию является stderr. Пустой список аргументов перенаправляет вывод в stdout.

t[,N]

Включает трассировку потока управления функций. Максимальная глубина вложения определяется N, по умолчанию используется 200.

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

DBUG_EXECUTE(строка, код)

Этот макрос похож на DBUG_PRINT(): первый аргумент является строкой, выбранной с помощью опции d, а второй — код для исполнения:

DBUG_EXECUTE("abort", abort());

DBUG_FILE

Это значение типа FILE* для использования с процедурами . Оно позволяет осуществлять собственный вывод в поток файла отладки.

DBUG_LONGJMP(jmp_buf env, int val)

Этот макрос заключает в оболочку вызов longjmp(), принимая те же самые аргументы, так что библиотека dbug будет знать, когда вы сделали нелокальный переход.

DBUG_POP()

Этот макрос выталкивает из стека один уровень сохраненного состояния отладки, созданный макросом DBUG_PUSH(). Он довольно эзотерический; вы скорее всего не будете его использовать.

DBUG_SETJMP(jmp_buf env)

Этот макрос заключает в оболочку вызов setjmp(), принимая те же самые аргументы. Он позволяет библиотеке dbug обрабатывать нелокальные переходы.

В другом воплощении, в первой начинающей компании, для которой мы работали, мы использовали в своем продукте библиотеку dbug. Она была неоценимой при разработке, а опустив -DDBUG в конечной сборке, мы смогли построить готовую версию без других изменений исходного кода.

Чтобы извлечь максимальную выгоду от библиотеки dbug, нужно использовать ее последовательно, по всей программе. Это проще, если вы используете ее с начала проекта, но в качестве эксперимента мы обнаружили, что с помощью простого сценария awk мы смогли включить библиотеку в программу с 30 000 строк кода за несколько часов работы. Если вы можете позволить себе накладные расходы, лучше всего оставить ее в конечной сборке вашей программы, чтобы можно было ее отлаживать без необходимости предварительной перекомпиляции.

Мы нашли, что библиотека dbug является удачным дополнением к внешним отладчикам, таким, как GDB; она обеспечивает организованный и последовательный способ применения поддержки к коду С. Она также довольно элегантно сочетает многие из методик, которые мы ранее в данной главе очертили отдельно. Особенно полезна особенность динамической трассировки вызовов функций, и она доказывает свою бесценность в качестве помощи в изучении поведения программы, если вы незнакомы с ней.

 

15.5.2. Отладчики выделения памяти

 

Игнорируя такие проблемы, как плохой дизайн программы, для любого крупномасштабного практического приложения единственной сложной задачей программиста на С является управление динамической памятью (посредством malloc(), realloc() и free()).

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

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

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

• Обнаружение неправильных освобождений: память, которая освобождается дважды, или функции free() передаются указатели, которые не были получены с помощью malloc().

• Обнаружение использования уже освобожденной памяти: память, которая освобождена, используется через висячий указатель.

• Обнаружение выхода за границы выделенной памяти: получение доступа или сохранение в память за пределами выделенной границы.

• Предупреждение об использовании неинициализированной памяти. (Многие компиляторы могут выдавать такие предупреждения.)

• Динамическая трассировка функций: когда появляется ошибочный доступ к памяти, вы получаете трассировку от того места, где память используется, до того места, где она была выделена.

• Управление инструментами посредством использования переменных окружения.

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

Некоторые утилиты просто записывают эти события. Другие организуют жуткое завершение программы приложения (посредством SIGSEGV), чтобы на код-нарушитель можно было точно указать из отладчика. Вдобавок, большинство спроектированы для работы вместе с GDB.

Некоторые инструменты требуют изменения исходного кода, такого, как вызов специальных функций или использование особого заголовочного файла, дополнительных #define и статической библиотеки. Другие работают посредством использования специального механизма библиотек общего пользования Linux/Unix для прозрачной установки себя в качестве заместителя стандартных библиотечных версий malloc() и free().

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

 

15.5.2.1. GNU/Linux

mtrace

Системы GNU/Linux, использующие GLIBC, предоставляют две функции для включения и отключения трассировки памяти во время исполнения.

#include /* GLIBC */

void mtrace(void);

void muntrace(void);

Когда вызывается mtrace(), библиотека проверяет переменную окружения MALLOC_TRACE. Ожидается, что она указывает на записываемый файл (существующий или нет). Библиотека открывает файл и начинает записывать сведения о выделениях и освобождениях памяти (Если файл не может быть открыт, запись не производится. Файл урезается каждый раз при запуске программы.) Когда вызывается muntrace(), библиотека закрывает файл и больше не регистрирует выделения и освобождения.

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

Когда приложение завершается, вы используете программу mtrace для анализа файла журнала. (Файл журнала в формате ASCII, но информацию нельзя использовать непосредственно.) Например, gawk включает трассировку, если определена TIDYMEM:

$ export TIDYMEM=1 MALLOC_TRACE=trace.out /* Экспортировать переменные окружения */

$ ./gawk 'BEGIN { print "hello, world" }' /* Запустить программу */

hello, world

$ mtrace ./gawk mtrace.out /* Создать отчет */

Memory not freed:

-----------------

Address Size Caller

0x08085858 0x20  at /home/arnold/Gnu/gawk/gawk-3.1.3/main.c:1102

0x08085880 0xc80 at /home/arnold/Gnu/gawk/gawk-3.1.3/node.c:398

0x08086508 0x2   at /home/arnold/Gnu/gawk/gawk-3.1.3/node.c:337

0x08086518 0x6   at /home/arnold/Gnu/gawk/gawk-3.1.3/node.c:337

0x08086528 0x10  at /home/arnold/Gnu/gawk/gawk-3.1.3/eval.c:2082

0x08086550 0x3   at /home/arnold/Gnu/gawk/gawk-3.1.3/node.с:337

0x08086560 0x3   at /home/arnold/Gnu/gawk/gawk-3.1.3/node.c:337

0x080865e0 0x4   at /home/arnold/Gnu/gawk/gawk-3.1.3/field.c:76

0x08086670 0x78  at /home/arnold/Gnu/gawk/gawk-3.1.3/awkgram.y:1369

0x08086700 0xe   at /home/arnold/Gnu/gawk/gawk-3.1.3/node.c:337

0x08086718 0x1f  at /home/arnold/Gnu/gawk/gawk-3.1.3/awkgram.y:1259

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

 

15.5.2.2. Electric Fence

В разделе 3.1 «Адресное пространство Linux/Unix» мы описали, как динамическая память выделяется из кучи, которая может расти и сокращаться (с помощью вызовов brk() или sbrk(), описанных в разделе 3.2.3 «Системные вызовы: brk() и sbrk()»).

Ну, картина, которую мы там представили, является упрощением действительности. Более развитые системные вызовы (не рассматриваемые в данной книге) позволяют добавлять в адресное пространство процесса дополнительные, необязательно смежные сегменты памяти. Многие отладчики malloc() работают с использованием этих системных вызовов для добавления новых областей адресного пространства при каждом выделении. Преимуществом этой схемы является то, что операционная система и аппаратное обеспечение защиты памяти компьютера взаимодействуют для обеспечения недействительности доступа к памяти за пределами этих изолированных сегментов, генерируя сигнал SIGSEGV. Эта схема изображена на рис. 15.1.

Рис. 15.1. Адресное пространство Linux/Unix, включая специальные области

Первым пакетом отладки, реализовавшим эту схему, был Electric Fence. Electric Fence является вставляемым заместителем для malloc() и др. Он работает на многих системах Unix и GNU/Linux; он доступен с FTP архива его авторов. Он поставляется также со многими дистрибутивами GNU/Linux, хотя, возможно, вам придется выбрать ею явным образом при установке системы.

После компоновки программы с Electric Fence любой доступ за пределами выделенной памяти генерирует SIGSEGV. Electric Fence также перехватывает попытки использования уже освобожденной памяти. Вот простая программа, которая иллюстрирует обе проблемы.

1  /* ch15-badmem1.с --- плохо обращается с памятью */

2

3  #include

4  #include

5

6  int main(int argc, char **argv)

7  {

8   char *p;

9   int i;

10

11  p = malloc(30);

12

13  strcpy(p, "not 30 bytes");

14  printf("p = <%s>\n", p);

15

16  if (argc ==2) {

17   if (strcmp(argv[1], "-b") == 0)

18    p[42] = 'a'; /* коснуться за пределами границы */

19   else if (strcmp(argv[1], "-f") == 0) {

20    free(p); /* освободить память, затем использовать ее */

21    p[0] = 'b';

22   }

23  }

24

25  /* освобождение (p); */

26

27  return 0;

28 }

Эта программа осуществляет простую проверку опций командной строки, чтобы решить, как вести себя плохо: -b вызывает доступ к памяти за ее выделенными страницами, а -f пытается использовать освобожденную память. (Строки 18 и 21 являются соответственно опасными.) Обратите внимание, что без опций указатель никогда не освобождается (строка 25), Electric Fence не перехватывает этот случай.

Одним из способов использования Electric Fence, способом, который гарантированно работает на различных системах Unix и GNU/Linux, является статическая компоновка с ним вашей программы. Затем программа должна быть запущена из отладчика. (Документация Electric Fence явно указывает, что Electric Fence не следует компоновать с двоичным файлом готового изделия.) Следующий сеанс демонстрирует эту процедуру и показывает, что происходит для обеих опций командной строки:

$ cc -g ch15-badmem1.c -lefence -о ch15-badmem1 /* Откомпилировать; компоновка статическая */

$ gdb ch15-badmem1 /* Запустить из отладчика */

GNU gdb 5.3

...

(gdb) run -b /* Попробовать опцию -b */

Starting program: /home/arnold/progex/code/ch15/ch15-badmem1 -b

[New Thread 8192 (LWP 28021)]

Electric Fence 2.2.0 Copyright (C) 1987-1999 Bruce Perens

p =

Program received signal SIGSBGV, Segmentation fault.

SIGSBGV: GDB prints where

[Switching to Thread 8192 (LWP 28021)]

0x080485b6 in main (argc=2, argv=0xbffff8a4) at ch15-badmem1.c:18

18 p[42] = 'a'; /* коснуться за пределами границы */

(gdb) run -f /* Теперь попробовать опцию -f */

The program being debugged has been started already.

Start it from the beginning? (y or n) y /* Да */

Starting program: /home/arnold/progex/code/ch15/ch15-badmem1 -f

[New Thread 8192 (LWP 28024)]

Electric Fence 2.2.0 Copyright (C) 1987-1999 Bruce Perens

p =

Program received signal SIGSEGV, Segmentation fault. /* Снова SIGSEGV */

[Switching to Thread 8192 (LWP 28024)]

0x080485e8 in main (argc=2, argv=0xbffff8a4) at ch15-badmem1.c:21

21 p[0] = 'b';

На системах, которые поддерживают разделяемые библиотеки и переменную окружения LD_PRELOAD (в том числе и на GNU/Linux), вам не нужно явным образом компоновать библиотеку efence. Вместо этого сценарий оболочки ef организует запуск программы с соответствующей настройкой.

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

Переменная окружения LD_PRELOAD заставляет системный загрузчик программ (который загружает исполняемые файлы в память) связаться со специальной библиотекой до стандартных библиотек. Сценарий ef использует эту особенность для связывания набора функций malloc() в Electric Fence. Таким образом, повторная компоновка даже не нужна. Этот пример демонстрирует ef:

$ cc -g ch15-badmem1.c -о ch15-badmem1 /* Компилировать как обычно */

$ ef ch15-badmem1 -b /* Запустить с использованием ef, создает дамп ядра */

Electric Fence 2.2.0 Copyright (С) 1987-1999 Bruce Perens

p =

/usr/bin/ef: line 20: 28005 Segmentation fault (core dumped)

( export LD_PRELOAD=libefence.so.0.0; exec $* )

$ ef ch15-badmem1 -f /* Запустить с использованием ef, снова создает дамп ядра */

Electric Fence 2.2.0 Copyright (С) 1987-1999 Bruce Perens

p =

/usr/bin/ef: line 20: 28007 Segmentation fault (core dumped)

( export LD_PRELOAD=libefence.so.0.0; exec $* )

$ ls -l core* /* Linux создает для нас разные файлы core */

-rw------- 1 arnold devel 217088 Aug 28 15:40 core.28005

-rw------- 1 arnold devel 212992 Aug 28 15:40 core.28007

GNU/Linux создает файлы core, которые включают в свое имя ID процесса. В данном случае такое поведение полезно, поскольку мы можем отдельно отлаживать каждый файл core:

$ gdb ch15-badmem1 core.28005 /* От опции -b */

GNU gdb 5.3

...

Core was generated by 'ch15-badmem1 -b'.

Program terminated with signal 11, Segmentation fault.

...

#0 0x08048466 in main (argc=2, argv=0xbffff8c4) at ch15-badmem1.c:18

18 p[42] = 'a'; /* touch outside the bounds */

(gdb) quit

$ gdb ch15-badmem1 core.28007 /* От опции -f */

GNU gdb 5.3

...

Core was generated by 'ch15-badmem1 -f'.

Program terminated with signal 11, Segmentation fault.

...

#0 0x08048498 in main (argc=2, argv=0xbffff8c4) at ch15-badmem1.с:21

21 p[0] = 'b';

Справочная страница efence(3) описывает несколько переменных окружения, которые должны быть установлены, чтобы настроить поведение Electric Fence. Следующие три наиболее примечательны.

EF_PROTECT_BELOW

Установка этой переменной в 1 заставляет Electric Fence проверять «недоборы» (underruns) вместо «переборов» (overruns) при выходе за пределы отведенной памяти. «Перебор», т.е. доступ к памяти в области за выделенной, был продемонстрирован ранее. «Недобор» является доступом к памяти, расположенной перед выделенной областью памяти.

EF_PROTECT_FREE

Установка этой переменной в 1 предотвращает повторное использование Electric Fence памяти, которая была корректно освобождена. Это полезно, когда вы думаете, что программа может получать доступ к освобожденной памяти; если освобожденная память впоследствии была выделена заново, доступ к ней через предыдущий висячий указатель остался бы в противном случае незамеченным.

EF_ALLOW_MALLOC_0

При наличии ненулевого значения Electric Fence допускает вызовы 'malloc(0)'. Такие вызовы в стандартном С технически действительны, но могут представлять программную ошибку. Соответственно Electric Fence по умолчанию их запрещает.

Вдобавок к переменным окружения Electric Fence предоставляет глобальные переменные с такими же названиями. Вы можете изменить их значения из отладчика, так что можно динамически изменять поведение программы, которая уже начала выполнение. Подробности см. в efence(3).

 

15.5.2.3. Отладка Malloc:

dmalloc

Библиотека dmalloc предоставляет большое число опций отладки. Ее автором является Грей Ватсон (Gray Watson), есть также и свой веб-сайт. Как и в случае с Electric Fence, она может быть уже установленной на вашей системе, или же вы можете ее извлечь и построить самостоятельно.

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

$ echo $DMALLOC_OPTIONS

debug=0x4e40503,inter=100,log=dm-log

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

Первый шаг заключается в определении функции оболочки с названием dmalloc, которая вызывает программу драйвера dmalloc:

$ dmalloc() {

> eval 'command dmalloc -b $*' /* Команда 'command' обходит функции оболочки */

> }

После того, как это сделано, вы можете передать функции опции для установки файла журнала (-1), указать число итераций, после которых dmalloc должна проверить свои внутренние структуры данных (-1), и указать уровень отладки или другой тэг ('low').

$ dmalloc -1 dm-log -i 100 low

Как и Electric Fence, библиотека dmalloc может быть скомпонована с приложением статически или связана динамически при помощи LD_PRELOAD. Последнее демонстрирует следующий пример:

$ LD_PRELOAD=libdmalloc.so ch15-badmem1 -b /* Запустить с проверкой */

p = /* Показан нормальный вывод */

ЗАМЕЧАНИЕ . Не используйте ' export LD_PRELOAD=libdmalloc.so '! Если вы это сделаете, каждая программа, которую вы запустите, такая как ls , будет выполняться со включенной проверкой malloc() . Ваша система быстро станет непригодной. Если вы сделали это случайно, можете использовать ' unset LD_PRELOAD ', чтобы восстановить обычное поведение.

Результаты записываются в файл dm-log следующим образом:

$ cat dm-log

1062078174: 1: Dmalloc version '4.8.1' from 'http://dmalloc.com/'

1062078174: 1: flags = 0x4e40503, logfile 'dm-log'

1062078174: 1: interval = 100, addr = 0, seen # = 0

1062078174: 1: starting time = 1062078174

1062078174: 1: free bucket count/bits: 63/6

1062078174: 1: basic-block 4096 bytes, alignment 8 bytes, heap grows up

1062078174: 1: heap: 0x804a000 to 0x804d000, size 12288 bytes (3 blocks)

1062078174: 1: heap checked 0

1062078174: 1: alloc calls: malloc 1, calloc 0, realloc 0, free 0

1062078174: 1: alloc calls: recalloc 0, memalign 0, valloc 0

1062078174: 1: total memory allocated: 30 bytes (1 pnts)

1062078174: 1: max in use at one time: 30 bytes (1 pnts)

1062078174: 1: max alloced with 1 call: 30 bytes

1062078174: 1: max alloc rounding loss: 34 bytes (53%)

1062078174: 1: max memory space wasted: 3998 bytes (98%)

1062078174: 1: final user memory space: basic 0, divided 1, 4062 bytes

1062078174: 1: final admin overhead: basic 1, divided 1, 8192 bytes (66%)

1062078174: 1: final external space: 0 bytes (0 blocks)

1062078174: 1: top 10 allocations:

1062078174: 1: total-size count in-use-size count source

1062078174: 1:         30     1          30     1 ra=0x8048412

1062078174: 1:         30     1          30     1 Total of 1

1062078174: 1: dumping not-freed pointers changed since 0:

1062078174: 1: not freed: '0x804c008|s1' (30 bytes) from 'ra=0x8048412'

1062078174: 1: total-size count source

1062078174: 1:         30     1 ra=0x8048412 /* Выделение здесь */

1062078174: 1:         30     1 Total of 1

1062078174: 1: unknown memory: 1 pointer, 30 bytes

1062078174: 1: ending time = 1062078174, elapsed since start = 0:00:00

Вывод содержит много статистических данных, которые нам пока не интересны. Интересна строка, в которой указывается не освобожденная память, с адресом возврата, указывающим на выделившую память функцию ('ra=0х8048412'). Документация dmalloc объясняет, как получить расположение в исходном коде этого адреса с использованием GDB.

$ gdb ch15-badmem1 /* Запустить GDB */

GNU gdb 5.3

...

(gdb) x 0x8048412 /* Проверить адрес */

0x8048412 : 0х8910с483

(gdb) info line *(0x8048412) /* Получить сведения о строке */

Line 11 of "ch15-badmem1.с" starts at address 0x8048408

and ends at 0x8048418 .

Это трудно, но выполнимо, если нет другого выбора. Однако, если вы включите в свою программу заголовочный файл "dmalloc.h" (после всех остальных операторов #include), вы можете получить сведения из исходного кода непосредственно в отчете.

...

1062080258: 1: top 10 allocations:

1062080258: 1: total-size count in-use-size count source

1062080258: 1:        30      1          30     1 ch15-badmem2.c:13

1062080258: 1:        30      1          30     1 Total of 1

1062080258: 1: dumping not-freed pointers changed since 0:

1062080258: 1: not freed: '0x804c008|s1' (30 bytes) from 'ch15-badmem2.c:13'

1062080258: 1: total-size count source

1062080258: 1:         30     1 ch15-badmem2.с:13

1062080258: 1:         30     1 Total of 1

...

(Файл ch15-badmem2.c является аналогичным ch15-badmem1.с, за исключением того, что он включает "dmalloc.h", поэтому мы не стали беспокоиться с его отображением).

Отдельные возможности отладки включаются или выключаются посредством использования лексем (tokens) — специально распознаваемых идентификаторов — и опций -р для добавления лексем (свойств) или -m для их удаления. Имеются предопределенные комбинации, 'low', 'med' и 'high'. Чем являются эти комбинации, вы можете увидеть с помощью 'dmalloc -Lv'.

$ dmalloc low /* Установить low */

$ dmalloc -Lv /* Показать установки */

Debug Malloc Utility: http://dmalloc.com/

For a list of the command-line options enter: dmalloc --usage

Debug-Flags 0x4e40503 (82052355) (low) /* Текущие лексемы */

log-stats, log-non-free, log-bad-space, log-elapsed-time, check-fence,

free-blank, error-abort, alloc-blank, catch-null

Address not-set

Interval 100

Lock-On not-set

Logpath 'log2'

Start-File not-set

Полный список лексем вместе с кратким объяснением и соответствующим каждой лексеме числовым значением можно получить с помощью 'dmalloc -DV':

$ dmalloc -DV

Debug Tokens:

none (nil) -- no functionality (0)

log-stats (lst) -- log general statistics (0x1)

log-non-free (lnf) -- log non-freed pointers (0x2)

log-known (lkn) -- log only known non-freed (0x4)

log-trans (ltr) -- log memory transactions (0x8)

log-admin (lad) -- log administrative info (0x20)

log-blocks (lbl) -- log blocks when heap-map (0x40)

log-bad-space (lbs) -- dump space from bad pnt (0x100)

log-nonfree-space (lns) -- dump space from non-freed pointers (0x200)

log-elapsed-time (let) -- log elapsed-time for allocated pointer (0x40000)

log-current-time (let) -- log current-time for allocated pointer (0x80000)

check-fence (cfe) -- check fence-post errors (0x400)

check-heap (che) -- check heap adm structs (0x800)

check-lists (cli) -- check free lists (0x1000)

check-blank (cbl) -- check mem overwritten by alloc-blank, free-blank (0x2000)

check-funcs (cfu) -- check functions (0x4000)

force-linear (fli) -- force heap space to be linear (0x10000)

catch-signals (csi) -- shutdown program on SIGHUP, SIGINT, SIGTERM (0x20000)

realloc-copy (rco) -- copy all re-allocations (0x100000)

free-blank (fbl) -- overwrite freed memory space with BLANK_CHAR (0x200000)

error-abort (eab) -- abort immediately on error (0x400000)

alloc-blank (abl) -- overwrite newly alloced memory with BLANK_CHAR (0x800000)

heap-check-map (hem) -- log heap-map on heap-check (0x1000000)

print-messages (pme) -- write messages to stderr (0x2000000)

catch-null (cnu) -- abort if no memory available (0x4000000)

never-reuse (nre) -- never re-use freed memory (0x8000000)

allow-free-null (afn) -- allow the frees of NULL pointers (0x20000000)

error-dump (edu) -- dump core on error and then continue (0x40000000)

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

 

15.5.2.4. Valgrind: многосторонний инструмент

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

Руководство по Valgrind описывает программу также или лучше, чем можем мы, поэтому мы будем цитировать (и сокращать) его по мере продвижения вперед.

Valgrind является гибким инструментом для отладки и профилирования исполняемых файлов Linux-x86. Инструмент состоит из ядра, которое программно обеспечивает искусственный процессор x86, и ряда «оболочек», каждая из которых является отладочным или профилирующим инструментом. Архитектура модульная, так что можно легко создавать новые «оболочки», не нарушая существующую структуру.

Наиболее полезной «оболочкой» является memcheck .

«Оболочка» memcheck обнаруживает в ваших программах проблемы с управлением памятью. Проверяются все чтения и записи памяти, а вызовы malloc/new/free/delete перехватываются. В результате memcheck может обнаружить следующие проблемы

• Использование неинициализированной памяти.

• Чтение/запись в память после ее освобождения.

• Чтение/запись за границей выделенного malloc блока.

• Чтение/запись в ненадлежащие области стека.

• Утечки памяти, когда указатели на выделенные malloc теряются навсегда.

• Несоответствующее использование malloc/new/new[] против free/delete/delete[] .

• Некоторые неправильные употребления pthreads API POSIX.

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

Другие «оболочки» более специализированы:

•  cachegrind осуществляет обстоятельную имитацию кэшей I1, D1 и L2 процессора, поэтому может точно указать источники осечек кэшей в вашем коде.

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

Но положительная сторона значительна: программы работают почти в два раза быстрее, чем с memcheck , используя значительно меньше памяти. Утилита по-прежнему находит чтения/записи освобожденной памяти, памяти за пределами выделенных блоков и в других недействительных местах, ошибки, которые вы действительно хотите обнаружить до выпуска программы в свет!

•  helgrind является отладочной оболочкой, предназначенной для обнаружения состязания данных в многопоточных программах.

Наконец, руководство отмечает:

Valgrind тесно связан с особенностями процессора, операционной системы и, в меньшей степени, компилятора и основных библиотек С. Это затрудняет его переносимость, поэтому мы с самого начала сконцентрировались на том, что мы считаем широко использующейся платформой: Linux на x86. Valgrind использует стандартный механизм Unix ' ./configure ', ' make ', ' make install ', и мы попытались обеспечить его работу на машинах с ядром 2.2 или 2.4 и glibc 2.1.X, 2.2.X или 2.3.1. Это должно охватить значительное большинство современных установок Linux. Обратите внимание, что glibc-2.3.2+ с пакетом NPTL (Native POSIX Thread Library — собственная библиотека потоков POSIX) не будет работать. Мы надеемся исправить это, но это будет нелегко.

Если вы используете GNU/Linux на другой платформе или используете коммерческую систему Unix, Valgrind не окажет вам большой помощи. Однако, поскольку системы GNU/Linux на x86 довольно обычны (и вполне доступны), вполне вероятно, что вы сможете приобрести ее с умеренным бюджетом, или по крайней мере, занять на время! Что еще, когда Valgrind нашел для вас проблему, она исправляется для любой платформы, для которой компилируется ваша программа. Таким образом, разумно использовать систему x86 GNU/Linux для разработки, а какую-нибудь другую коммерческую систему Unix для развертывания высококачественного продукта.

Хотя из руководства Valgrind у вас могло сложиться впечатление, что существуют отдельные команды memcheck, addrcheck и т.д., это не так. Вместо этого программа оболочки драйвера с именем valgrind запускает отладочное ядро с соответствующей «оболочкой», указанной в опции --skin=. Оболочкой по умолчанию является memcheck; таким образом, запуск просто valgrind равносильно 'valgrind --skin=memcheck' (Это обеспечивает совместимость с более ранними версиями Valgrind, которые осуществляли лишь проверку памяти, это имеет также больший смысл, поскольку оболочка memcheck предоставляет большую часть сведений.)

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

--gdb-attach=no/yes

Запускается с подключенным к процессу GDB для интерактивной отладки. По умолчанию используется no.

--help

Перечисляет опции.

--logfile= файл

Записывает сообщения в файл.pid .

--num-callers= число

Выводит число вызывающих в трассировке стека. По умолчанию 4.

--skin= оболочка

Использует соответствующую оболочку. По умолчанию memcheck.

--trace-children=no|yes

Запускает трассировку также в порожденных процессах. По умолчанию используется no.

-V, --verbose

Использует более полный вывод. Это включает перечисление загруженных библиотек, а также подсчеты всех различных видов ошибок.

Из опций для оболочки memcheck мы полагаем, что эти являются наиболее полезными.

--leak-check=no|yes

Искать утечки памяти после завершения программы. По умолчанию используется no.

--show-reachable=no|yes

Показать доступные блоки после завершения программы. Если используется --show-reachable=yes, Valgrind ищет динамически выделенную память, на которую все еще есть указывающий на нее указатель. Такая память не является утечкой, но о ней все равно следует знать. По умолчанию используется no.

Давайте посмотрим на Valgrind в действии. Помните ch15-badmem.c? (См. раздел 15.5.2.2 «Electric Fence».) Опция -b записывает в память, находящуюся вне выделенного malloc() блока. Вот что сообщает Valgrind:

$ valgrind ch15-badmem1 -b

1  ==8716== Memcheck, a.k.a. Valgrind, a memory error detector for x86-linux.

2  ==8716== Copyright (C) 2002-2003, and GNU GPL'd, by Julian Seward.

3  ==8716== Using valgrind-20030725, a program supervision framework for x86-linux.

4  ==8716== Copyright (C) 2000-2003, and GNU GPL'd, by Julian Seward.

5  ==8716== Estimated CPU clock rate is 2400 MHz

6  ==8716== For more details, rerun with: -v

7  ==8716==

8  p =

9  ==8716== Invalid write of size 1

10 ==8716== at 0x8048466: main (ch15-badmem1.c:18)

11 ==8716== by 0x420158D3: __libc_start_main (in /lib/i686/libc-2.2.93.so)

12 ==8716== by 0x8048368: (within /home/arnold/progex/code/ch15/ch15-badmem1)

13 ==8716== Address 0x4104804E is 12 bytes after a block of size 30 alloc'd

14 ==8716== at 0x40025488: malloc (vg_replace_malloc.с:153)

15 ==8716== by 0x8048411: main (ch15-badmem1.c:11)

16 ==8716== by 0x420158D3: __libc_start_main (in /lib/i686/libc-2.2.93.so)

17 ==8716== by 0x8048368: (within /home/arnold/progex/code/ch15/ch15-badmem1)

18 ==8716==

19 ==8716== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

20 ==8716== malloc/free: in use at exit: 30 bytes in 1 blocks.

21 ==8716== malloc/free: 1 allocs, 0 frees, 30 bytes allocated.

22 ==8716== For a detailed leak analysis, rerun with: --leak-check=yes

23 ==8716== For counts of detected errors, rerun with: -v

(Были добавлены номера строк в выводе, чтобы облегчить обсуждение.) Строка 8 является выводом программы; остальные от Valgrind в стандартную ошибку. Сообщение об ошибке находится в строках 9–17. Она указывает, сколько байтов было записано неверно (строка 9), где это случилось (строка 10), и показывает трассировку стека. Строки 13–17 описывают, откуда была выделена память. Строки 19–23 подводят итоги.

Опция -f программы ch15-badmem1 освобождает выделенную память, а затем записывает в нее через висячий указатель. Вот что сообщает Valgrind в этом случае:

$ valgrind ch15-badmem1 -f

==8719== Memcheck, a.k.a. Valgrind, a memory error detector for x86-linux.

...

p =

==8719== Invalid write of size 1

==8719== at 0x8048498: main (ch15-badmem1.с:21)

==8719== by 0x420158D3: __libc_start_main (in /lib/i686/libc-2.2.93.so)

==8719== by 0x8048368: (within /home/arnold/progex/code/ch15/ch15-badmem1)

==8719== Address 0x41048024 is 0 bytes inside a block of size 30 free'd

==8719== at 0x40025722: free (vg_replace_malloc.с:220)

==8719== by 0x8048491: main (ch15-badmem1.c:20)

==8719== by 0x420158D3: __libc_start_main (in /lib/i686/libc-2.2.93.so)

==8719== by 0x8048368: (within /home/arnold/progex/code/ch15/ch15-badmem1)

...

На этот раз в отчете указано, что запись была осуществлена в освобожденную память и что вызов free() находится в строке 20 ch15-badmem1.c.

При вызове без опций ch15-badmem1.c выделяет и использует память, но не освобождает ее. О таком случае сообщает опция —leak-check=yes:

$ valgrind --leak-check=yes ch15-badmem1

1  ==8720== Memcheck, a.k.a. Valgrind, a memory error detector for x86-linux.

...

8  p =

9  ==8720==

10 ==8720== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

11 ==8720== malloc/free: in use at exit: 30 bytes in 1 blocks.

12 ==8720== malloc/free: 1 allocs, 0 frees, 30 bytes allocated.

...

16 ==8720==

17 ==8720== 30 bytes in 1 blocks are definitely lost in loss record 1 of 1

18 ==8720== at 0x40025488: malloc (vg_replace_malloc.c:153)

19 ==8720== by 0x8048411: main (ch15-badmem1.c:11)

20 ==8720== by 0x420158D3: __libc_start_main (in /lib/i686/libc-2.2.93.so)

21 ==8720== by 0x8048368: (within /home/arnold/progex/code/ch15/ch15-badmem1)

22 ==8720==

23 ==8720== LEAK SUMMARY:

24 ==8720== definitely lost: 30 bytes in 1 blocks.

25 ==8720== possibly lost: 0 bytes in 0 blocks.

26 ==8720== still reachable: 0 bytes in 0 blocks.

27 ==8720== suppressed: 0 bytes in 0 blocks.

28 ==8720== Reachable blocks (those to which a pointer was found) are not shown.

29 ==8720== To see them, rerun with: --show-reachable=yes

Строки 17–29 предоставляют отчет об утечке; эта память была выделена в строке 11 ch15-badmem1.с.

Помимо отчетов о неправильном использовании динамической памяти, Valgrind может диагностировать использование неинициализированной памяти. Рассмотрим следующую программу, ch15-badmem3.c:

1  /* ch15-badmem3.c --- плохое обращение с нединамической памятью */

2

3  #include

4  #include

5

6  int main(int argc, char **argv)

7  {

8   int a_var; /* Обе не инициализированы */

9   int b_var;

10

11  /* Valgrind не отметит это; см. текст. */

12  a_var = b_var;

13

14  /* Использование неинициализированной памяти; это отмечается. */

15  printf("a_var = %d\n", a_var);

16

17  return 0;

18 }

При запуске Valgrind выдает этот (сокращенный) отчет:

==29650== Memcheck, a.k.a. Valgrind, a memory error detector for x86-linux.

...

==29650== Use of uninitialised value of size 4

==29650== at 0x42049D2A: _IO_vfprintf_internal (in /lib/i686/libc-2.2.93.so)

==29650== by 0x420523C1: _IO_printf (in /lib/1686/libc-2.2.93.so)

==29650== by 0x804834D: main (ch15-badmem3.с:15)

==29650== by 0x420158D3: __libc_start_main (in /lib/i686/libc-2.2.93.so)

==29650==

==29650== Conditional jump or move depends on uninitialised value(s)

==29650== at 0X42049D32: _IO_vfprintf_internal (in /lib/i686/libc-2.2.93.so)

==29650== by 0x420523C1: _IO_printf (in / lib/i686/libc-2.2.93.so)

==29650== by 0x804834D: main (ch15-badmem3.c:15)

==29650== by 0x420158D3: __libc_start_main (in /lib/i686/libc-2.2.93.so)

...

a_var = 1107341000

==29650==

==29650== ERROR SUMMARY: 25 errors from 7 contexts (suppressed: 0 from 0)

==29650== malloc/free: in use at exit: 0 bytes in 0 blocks.

==29650== malloc/free: 0 allocs, 0 frees, 0 bytes allocated.

==29650== For a detailed leak analysis, rerun with: --leak-check=yes

==29650== For counts of detected errors, rerun with: -v

В документации Valgrind объясняется, что копирование неинициализированных данных не выдает сообщений об ошибках. Оболочка memcheck отмечает состояние данных (неинициализированные) и отслеживает его при перемещениях данных. Таким образом, a_var считается неинициализированной, поскольку это значение было получено от b_var, которая была неинициализированной.

memcheck сообщает о проблеме лишь тогда, когда неинициализированное значение используется. Здесь это происходит в библиотеке С (_IO_vfprintf_internal()), которая должна преобразовать значение в строку, для этого, она проводит с этим значением вычисления.

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

В заключение, Valgrind является мощным инструментом отладки памяти. Он использовался в таких крупномасштабных, многопоточных производственных программах, как KDE 3, OpenOffice и веб-браузер Konqueror. Он конкурирует с несколькими коммерческими предложениями, а другая его версия была даже использована (совместно с эмулятором WINE) для отладки программ, написанных для Microsoft Windows с использованием Visual С++! Вы можете получить Valgrind с его веб-сайта.

 

15.5.2.5. Другие отладчики malloc

Две статьи Cal Ericson в Linux Journal описывают mtrace и dmalloc, а также большинство других перечисленных ниже инструментов. Эти статьи Memory Leak Detection in Embedded Systems, выпуск 101, сентябрь 2002 г., и Memory Leak Detection in C++, выпуск 110, июнь 2003 г. Обе статьи доступны на веб-сайте Linux Journal.

Другие инструменты сходны по природе с описанными ранее.

ccmalloc

Замещающая malloc() библиотека, которая не нуждается в особой компиляции и может использоваться с С++. См. http://www.inf.ethz.ch/personal/biere/projects/ccmalloc.

malloc Марка Мораеса (Mark Moraes)

Старинная, но полнофункциональная библиотека замещения malloc(), предоставляющая возможности профилирования, трассировки и отладки. Вы можете получить ее с ftp://ftp.cs.toronto.edu/pub/moraes/malloc-1.18.tar.gz.

mpatrol

Пакет с большими возможностями настройки для отладки памяти и тестирования. См http://www.cbmamiga.demon.со.uk/mpatrol.

memwatch

Пакет, требующий использования специального заголовочного файла и опций времени компилирования. См. http://www.linkdata.se/sourcecode.html.

njamd

«Не просто еще один отладчик malloc» (Not Just Another Malloc Debugger). Эта библиотека не требует специальной компоновки с приложением; вместо этого она использует LD_PRELOAD для замены стандартных процедур. См. http://sourceforge.net/projects/njamd.

yamd

Похож на Electric Fence, но со многими дополнительными опциями. См. http://www3.hmc.edu/~neldredge/yamd.

Почти все из этих пакетов используют для точной настройки своего поведения переменные окружения. В таблице 15.1 на основе статей из Linux Journal сделана сводка различных пакетов.

Таблица 15.1. Сводка особенностей инструментов памяти

Инструмент ОС Заголовочный файл Модуль/ программа Многопоточность
ccmalloc Многотипная Нет Программа Нет
dmalloc Многотипная Необязательно Программа Да
efence Многотипная Нет Программа Нет
memwatch Многотипная Да Программа Нет
Moraes Многотипная Необязательно Программа Нет
mpatrol Многотипная Нет Программа Да
mtrace Linux (GLIBC) Да Модуль Нет
njamd Многотипная Нет Программа Нет
valgrind Linux (GLIBC) Нет Программа Да
yamd Linux, DJGPP Нет Программа Нет

Как видно, для отладки проблем динамической памяти доступен ряд выборов. На системах GNU/Linux и BSD один или более из этих инструментов, возможно, уже установлены, что избавляет вас от хлопот по их загрузке и построению.

Полезно также использовать для своей программы несколько инструментов подряд. Например, mtrace для обнаружения не освобождаемой памяти, a Electric Fence для перехвата доступа к недействительной памяти.

 

15.5.3. Современная

lint

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

if (argc < 2)

 fprintf ("usage: %s [ options ] files\n", argv[0]);

  /* отсутствует stderr */

Если программа, содержащая этот фрагмент, никогда не вызывается с ошибочным числом аргументов, fprintf(), в которой отсутствует первый аргумент FILE*, также никогда не вызывается.

Программа V7 lint была предназначена для решения таких проблем. Она делала два прохода через все файлы программы, сначала собирая сведения об аргументах функций, а затем сравнивая вызовы функций с собранной информацией. Особые файлы «библиотеки lint» предоставляли сведения о функциях стандартных библиотек, так что их также можно было проверить, lint проверяла также другие сомнительные конструкции.

С появлением в стандартном С прототипов необходимость в lint уменьшилась, но не исчезла совсем, поскольку C89 все еще допускает объявления функций в старом стиле.

extern int some_func(); /* Список аргументов неизвестен */

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

Программа splint (Secure Programming Lint — Lint для безопасного программирования) является современным обновлением lint. Она предусматривает слишком много опций и возможностей, чтобы перечислять их здесь, но ее стоит исследовать.

Следует знать об одной особенности подобных lint программ, которая заключается в том, что они выводят целый поток предупреждающих сообщений. Многие из сообщаемых предупреждений в действительности безвредны. В таких случаях инструменты допускают использование специальных комментариев, сообщающих. «Да, я знаю об этом, это не проблема», splint лучше всего работает, когда вы предоставляете в своем коде большое количество подобных примечаний.

splint является мощным, но сложным инструментом; выделение некоторого времени на изучение его использования, а затем частое его использование поможет сохранить ваш код ясным.

 

15.6. Тестирование программ

Разработка программного обеспечения содержит элементы и искусства, и науки, это одна сторона того, что делает ее такой восхищающей и стимулирующей профессией. Данный раздел вводит в тему тестирования программного обеспечения, которая также включает в себя и искусство, и науку; таким образом, это несколько более общий и высокий уровень (читай: «на который можно махнуть рукой»), чем остальная часть данной главы.

Тестирование программ является неотъемлемой частью процесса разработки программного обеспечения. Весьма маловероятно, что программа заработает правильно на 100 процентов при первой компиляции. Программа не несет ответственности за свою правильность; за это отвечает автор программы. Одним из самых важных способов проверки того, что программа работает так, как предполагалось, является ее тестирование.

Один из способов классификации различных видов тестов следующий:

Тесты модулей (Unit tests)

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

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

Комплексные тесты (Integration tests)

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

Возвратные тесты (Regression tests)

Неизбежно вы (или ваши пользователи!) обнаружат проблемы. Это могут быть действительные ошибки, или ограничения дизайна, или неизбежные отказы в «пограничных случаях». Когда вы смогли воспроизвести и исправить проблему, сохраните первоначальные условия отказа в качестве возвратного теста.

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

Тестирование следует по возможности автоматизировать. Это особенно легко сделать для программ, не содержащих графического пользовательского интерфейса (GUI), написанных в стиле инструментов Linux/Unix: читающих стандартный ввод или указанные файлы и записывающих в стандартный вывод и стандартную ошибку. По меньшей мере, тестирование можно осуществить с помощью простых сценариев оболочки. Более сложное тестирование осуществляется обычно с помощью отдельного подкаталога test и программы make.

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

• Проектируйте тест вместе с функциональностью

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

• Используйте в своем коде операторы проверки (см. раздел 12.1 «Операторы проверки assert()») и проведите свои тесты с разрешенными операторами проверки.

• Создайте и используйте повторно тестовое окружение.

• Сохраняйте условия сбоев для возвратного тестирования

• Как можно больше автоматизируйте тестирование.

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

• Используйте инструменты обзора кода, такие, как gcov, чтобы удостовериться, что ваш набор тестов охватывает весь ваш код.

• Тестируйте с самого начала и тестируйте часто.

• Изучите литературу по тестированию программного обеспечения, чтобы совершенствовать свою способность разрабатывать и тестировать программное обеспечение.

 

15.7. Правила отладки

Отладка не является «черной магией». Ее принципы и методики могут быть изучены и последовательно применены каждым. С этой целью мы рекомендуем книгу Debugging Дэвида Эганса (David J. Agans; ISBN: 0-8144-7168-4). У книги есть веб-сайт, на котором обобщены правила и представлен плакат для загрузки, чтобы вы могли его распечатать и повесить на стену в своем офисе.

Чтобы завершить наше обсуждение, мы представляем следующий материал. Он был адаптирован Дэвидом Эгансом по разрешению из Debugging, Copyright © 2002 David J. Agans, опубликованной AMACOM, отделением American Management Association, New York. Мы благодарим его.

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

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

2. Вызовите сбой. Для того, чтобы увидеть ошибку, вы должны быть способны постоянно воспроизводить сбой. Задокументируйте свои процедуры и начните с известного состояния, чтобы вы всегда могли снова вызвать сбой. Ищите ошибку в системе, которая дает сбой, не старайтесь имитировать проблему на другой системе. Не доверяйте статистике непостоянных проблем; они скорее скроют ошибку, чем проявят ее. Вместо этого постарайтесь сделать ее устойчивой, изменяя вводимые данные, начальные условия и координацию действий.

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

3. Прекратите думать и смотрите. Имеется больше способов появления ошибок, чем вы можете себе представить. Поэтому не представляйте, что могло бы случиться, смотрите на это — оснастите систему инструментарием, чтобы вы действительно смогли увидеть механизм ошибки. Используйте любой инструментарий, который можете — отладчики, printf(), assert(), анализаторы логики и даже светодиоды и звуковые сигнализаторы. Проверяйте достаточно глубоко, пока ошибка не станет очевидной для глаз, а не только для мозга.

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

4. Разделяй и властвуй. Каждый это знает. Вы делаете последовательное приближение — начинаете с одного конца, перескакиваете полпути, смотрите, с какой стороны ошибка, затем перескакиваете оставшиеся полпути в направлении ошибки. Бинарный поиск, вы оказываетесь так за несколько прыжков. Трудной частью является определение того, прошли вы ошибку или нет. Одной из полезных уловок является помещение в систему известных, простых данных, так чтобы можно было легче узнать мусор. Начните также с плохого конца и работайте по направлению к хорошему: если вы начнете с хорошего конца, имеется слишком много хороших путей для исследования. Известные ошибки исправляйте сразу, поскольку иногда две ошибки взаимодействуют (хотя вы могли бы поклясться, что они не должны этого делать), и последовательное приближение не работает с двумя целевыми значениями.

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

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

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

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

Не следует также непременно доверять своим инструментам. Производители инструментов также являются инженерами; у них есть ошибки, и вы можете оказаться тем, кто их обнаружит.

8. Оцените свежим взглядом. Есть три причины попросить помощи при отладке.

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

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

9. Если вы не исправили это, это не исправлено. Так вы думаете, это исправлено? Испытайте. Раз вы могли заставить ошибку повторяться постоянно, создайте ту же самую ситуацию и убедитесь, что ошибки нет. Не думайте, что все исправлено лишь потому, что проблема была очевидной. Может, она не была такой очевидной. Может, ваше исправление не было сделано правильно. Может, ваше исправление даже не находится в новом выпуске! Проверьте! Заставьте ошибку исчезнуть.

Вы уверены, что именно ваш код исправил проблему? Или это произошло из-за изменения теста, или туда был внесен какой-то другой код? Когда вы видите, что ваше исправление работает, уберите его и заставьте ошибку появиться снова. Затем верните исправление на место и убедитесь, что ошибки нет. Этот шаг гарантирует, что именно ваше исправление решило проблему.

Дополнительные сведения о книге Debugging и плакат с правилами отладки можно найти для свободной загрузки по адресу http://www.debuggingrules.com.

 

15.8. Рекомендуемая литература

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

1. Debugging, David J. Agans. AMACOM, New York, New York. USA 2003. ISBN: 0-8144-7168-4.

Настоятельно рекомендуем эту книгу. У нее легкий стиль, удивительное звучание, чтение — одно удовольствие!

2. Programming Pearls, 2nd edition, by Jon Louis Bentley. Addison-Wesley, Reading, Massachusetts, USA, 2000, ISBN: 0-201-63788-0. См. также веб-сайт этой книги.

В главе 5 этой книги приведено хорошее обсуждение тестирования элементов и построения тестовой среды.

3. Literate Programming, by Donald E. Knuth. Center for the Study of Language and Information (CSLI), Stanford University, USA, 1992. ISBN: 0-9370-7380-6.

Эта восхитительная книга содержит ряд статей Дональда Кнута по грамотному программированию (literate programming) — методике программирования, которую он изобрел и использовал для создания ТеХ и Metafont. Особый интерес представляет статья, озаглавленная «Ошибки ТеХ», которая описывает, как он разрабатывал и отлаживал ТеХ, включая его журнал всех найденных и исправленных ошибок.

4. Writing Solid Code, by Steve Maguire. Microsoft Press, Redmond, Washington, USA, 1993. ISBN 1-55615-551-4.

5. Code Complete: A Practical Handbook of Software Construction, by Steve McConnell Microsoft Press, Redmond, Washington, USA, 1994. ISBN: 1-55615-484-4.

6. The Practice of Programming, by Brian W. Kernighan and Rob Pike. Addison-Wesley, Reading. Massachusetts, USA, 1999. ISBN: 0-201-61585-X.

 

15.9. Резюме

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

• Программы должны компилироваться без оптимизации и с включенными идентификаторами отладки, чтобы упростить отладку под отладчиком. На многих системах компиляция с оптимизацией и компиляция с идентификаторами отладки несовместимы. Это не относится к GCC, вот почему разработчикам GNU/Linux нужно знать об этой проблеме.

• Отладчик GNU GDB является стандартом на системах GNU/Linux и может использоваться также почти на любой коммерческой системе Unix. (Также доступны и легко переносимы графические отладчики на основе GDB.) Контрольные точки, отслеживаемые точки и пошаговое исполнение с посредством next, step и cont предоставляют базовый контроль над программой при ее работе. GDB позволяет также проверять данные и вызывать функции внутри отлаживаемой программы.

 • Имеется множество вещей, которые вы можете сделать при написании программы для ее упрощения, когда неизбежно придется ее отлаживать. Мы рассмотрели следующие темы:

 • Отладочные макросы для вывода состояния.

 • Избегание макросов с выражениями.

 • Перестановку кода для облегчения пошагового выполнения.

 • Написание вспомогательных функций для использования их из отладчика.

 • Избегание объединений.

 • Помещение отладочного кода времени исполнения в готовую версию программы и обеспечение различных способов включения вывода этого кода.

 • Добавление фиктивных функций для упрощения установки контрольных точек.

• Для помощи при отладке помимо простых отладчиков общего назначения существует ряд инструментов и библиотек. Библиотека dbug предоставляет элегантный внутренний отладчик, который использует многие из описанных нами методик последовательным, связанным образом.

• Существует множество отладочных библиотек для динамической памяти, имеющие сходные свойства. Мы рассмотрели три из них (mtrace, Electric Fence и dmalloc) и предоставили ссылки на несколько других. Программа Valgrind идет еще дальше, обнаруживая проблемы, относящиеся к неинициализированной памяти, а не только к динамической памяти.

• splint является современной альтернативой многоуважаемой программе V7 lint. Она доступна по крайней мере на системе одного из поставщиков GNU/Linux и легко может быть загружена и построена из исходных кодов.

• Помимо инструментов отладки, неотъемлемой частью процесса разработки программного обеспечения является также тестирование программ. Ее следует понять, запланировать и управлять ею с самого начала любого проекта разработки программного обеспечения, даже индивидуального.

• Отладка является умением, которому можно научиться. Мы рекомендуем прочесть книгу Debugging Дэвида Дж. Эганса и научиться применять его правила.

 

Упражнения

1. Откомпилируйте одну из ваших программ с помощью GCC, используя как -g, так и -O. Запустите ее под GDB, установив контрольную точку в main(). Выполните программу пошагово и посмотрите, насколько близко соответствует (или не соответствует) исполнение оригинальному исходному коду. Это особенно хорошо делать с кодом, использующим циклы while или for.

2. Прочитайте об особенности GDB условной контрольной точки. Насколько это упрощает работу с проблемами, которые появляются лишь после того, как будет сделано определенное число операций?

3. Перепишите функцию parse_debug() из раздела 15.4.2.1 «Добавляйте отладочные опции и переменные», чтобы использовать таблицу строк опций отладки, значений флагов и длин строк

4. (Трудное.) Изучите исходный код gawk; в частности, структуру NODE в awk.h. Напишите вспомогательную отладочную функцию, которая выводит содержимое NODE, основываясь на значении в поле type.

5. Возьмите одну из своих программ и измените ее так, чтобы использовать библиотеку dbug. Откомпилируйте ее сначала без -DDBUG, чтобы убедиться, что она компилируется и работает нормально. (Есть ли у вас для нее набор возвратных тестов? Прошла ли ваша программа все тесты?)

Убедившись, что добавление библиотеки dbug не нарушает работу вашей программы, перекомпилируйте ее с -DDBUG. По-прежнему ли проходит ваша программа все свои тесты? Какова разница в производительности при включенной и отключенной библиотеке? Запустите ваш тестовый набор с опцией -#t, чтобы увидеть трассировку вызовов функций. Как вы думаете, это поможет вам в будущем, когда придется иметь дело с отладкой? Почему да или почему нет?

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

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

8. Разработайте набор тестов для программы mv. (Прочтите mv(1): убедитесь, что охватили все ее опции.)

9. Поищите в Интернете ресурсы по тестированию программного обеспечения. Какие интересные вещи вы нашли?

 

Глава 16

Проект, связывающий все воедино

 

В первой половине этой книги мы довольно аккуратно связали все, что было представлено, рассмотрев V7 ls.c. Однако, нет достаточно небольшой программы, насколько бы это нам хотелось, чтобы связать воедино все концепции и API, представленные начиная с главы 8 «Файловые системы и обходы каталогов».

 

16.1. Описание проекта

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

Настоящие оболочки являются большими и беспорядочными творениями. Они должны иметь дело со многими проблемами переносимости, такими, которые мы обрисовывали по всей книге, а помимо этого, они часто должны обходить различные ошибки в различных версиях Unix Более того, чтобы быть полезными, оболочки делают множество вещей, которые не затрагивают API системных вызовов, такие, как хранение переменных оболочки, историю сохраненных команд и т.д. Предоставление завершенного обзора полноценной оболочки, такой как Bash, ksh93 или zsh, потребовало бы отдельной книги.

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

1. Спроектируйте свой командный «язык», чтобы его было легко интерпретировать с помощью простого кода. Хотя технология компиляторов и интерпретаторов полезна при написании оболочки как изделия, для вас на данный момент это, вероятно, излишне.

Рассмотрите следующие моменты:

 • Собираетесь ли вы использовать возможности интернационализации?

 • Какие команды должны быть встроены в оболочку?

 • Чтобы быть полезной, в вашей оболочке должен быть механизм пути поиска команд, аналогичный $PATH в обычной оболочке. Как вы его установите?

 • Какие перенаправления ввода/вывода вы хотите поддержать? Только файлы? Также и каналы? Хотите ли вы иметь возможность перенаправлять нет только дескрипторы файлов 0, 1 и 2?

 • Решите, как будут работать кавычки: одинарные и двойные? Или лишь одна разновидность? Как вы поместите в кавычки сами кавычки? Как кавычки будут взаимодействовать с перенаправлениями ввода/вывода?

 • Как вы обработаете вызов команд в фоновом режиме? Что насчет ожидания завершения работы команды в фоновом режиме?

 • Решите, будут ли у вас переменные оболочки.

 • Какую разновидность символов подстановки или других расширений будете вы поддерживать? Как это взаимодействует с кавычками? С переменными оболочки?

 • Вы должны запланировать по крайней мере операторы if и while. Спроектируйте синтаксис. Мы будем называть их блочными операторами.

 • Решите, хотите ли вы разрешить перенаправления ввода/вывода для блочных операторов. Если да, как будет выглядеть синтаксис?

 • Решите, как язык вашей оболочки должен обрабатывать сигналы, если он вообще это делает.

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

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

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

4. Добавьте кавычки так, чтобы отдельные «слова» могли содержать разделители. Реализует ли код для кавычек ваш проект?

5. Заставьте работать ваши встроенные команды. (По крайней мере две нужные встроенные команды см. в разделах 4.6 «Создание файлов» и 8.4.1 «Смена каталога: chdir() и fchdir()».) Как вы собираетесь их тестировать?

6. Первоначально используйте фиксированный путь поиска, такой как "/bin:/usr/bin:/usr/local/bin". Добавьте создание процесса при помощи fork() и его исполнение при помощи exec(). (См. главу 9 «Управление процессами и каналы».) Запустив новую программу, оболочка должна ждать ее завершения.

7. Добавьте фоновое исполнение и, в качестве отдельной команды, ожидание завершения выполнения процесса (см. главу 9 «Управление процессами и каналы»).

8. Добавьте устанавливаемый пользователем путь поиска (см. раздел 2.4 «Переменные окружения»).

9. Добавьте перенаправление ввода/вывода для файлов (см. раздел 9.4 «Управление дескрипторами файлов»).

10. Добавьте переменные оболочки. Протестируйте их взаимодействие с кавычками.

11. Добавьте символы подстановки и другие расширения (см. раздел 12.7 «Расширения метасимволов»). Протестируйте их взаимодействие с переменными оболочки. Протестируйте их взаимодействие с кавычками.

12. Добавьте конвейеры (см. раздел 9.3 «Базовое межпроцессное взаимодействие: каналы и очереди FIFO»). С этого момента начинаются настоящие сложности. Вам может потребоваться тщательно рассмотреть то, как вы управляете данными, представляющими запускаемые команды.

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

13. Если вы принимаете дальнейший вызов, добавьте операторы if и/или while.

14. Добавьте обработку сигналов (см. главу 10 «Сигналы»).

15. Если вы хотели бы использовать свою оболочку для настоящей работы, изучите библиотеку GNU Readline (наберите в системе GNU/Linux 'info readline' или посмотрите исходный код для оболочки Bash). Эта библиотека дает вам возможность добавлять к интерактивным программам возможность редактирования командной строки в стиле Emacs или vi.

Постоянно держите в уме две вещи- всегда имейте возможность протестировать то, что вы делаете; и «никаких произвольных ограничений»!

Когда все это сделано, проделайте анализ сделанного проекта. Как вы сделали бы его по-другому во второй раз? Удачи!

 

16.2. Рекомендуемая литература

1. The UNIX Programming Environment, by Brian W. Kernighan and Rob Pike. Prentice-Hall, Englewood Cliffs, New Jersey, USA, 1984. ISBN: 0-13-937699-2.

Эта классическая книга по программированию на Unix, описывающая целостную структуру окружения Unix, от интерактивного использования до программирования оболочки, программирования с помощью функций и низкоуровневых системных вызовов, разработки программ с помощью make, yacc и lex, и документирования с помощью nroff и troff.

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

2. The Art of UNIX Programming, by Eric S. Raymond. Addison-Wesley, Reading, Massachusetts, USA, 2004. ISBN: 0-13-142901-9.

Это книга на более высоком уровне фокусируется на проблемах проектирования при программировании в Unix: как работают программы Unix и как разрабатывать свои собственные программы, чтобы уютно вписаться в окружение Linux/Unix. Хотя мы не всегда согласны со многим из того, что хочет сказать автор, книга действительно содержит значительное количество важного материала, и ее стоит прочесть.