Текст книги "Linux программирование в примерах"
Автор книги: Арнольд Роббинс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 49 (всего у книги 55 страниц)
Помимо тех вещей, которые вы добавляете к своему коду для времени компиляции, можно также добавить дополнительный код для обеспечения возможностей отладки времени исполнения. Это особенно полезно для приложений, которые устанавливаются в полевых условиях, когда в системе клиента не будет установленного исходного кода (а может быть, даже и компилятора!)
В данном разделе представлены некоторые методики отладки, которые мы использовали в течение ряда лет, от простых до более сложных. Обратите внимание, что наше рассмотрение ни в коем случае не является исчерпывающим. Это область, в которой стоит иметь некоторое воображение и использовать его!
Простейшей методикой является наличие опции командной строки, делающих возможным отладку. Такая опция может быть условно откомпилированной для отладки. Однако более гибким подходом является оставить опцию в готовой версии программы. (Вы можете также решить, оставлять или не оставлять эту опцию не документированной. Здесь есть различные компромиссы: ее документирование может дать возможность вашим покупателям или клиентам больше изучить внутренности вашей системы, чего вы можете не хотеть С другой стороны, не документирование ее кажется довольно подлым. Если вы пишете для 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 != ' ';) {
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).)
Преимуществом, которое может быть большим, является то, что вашу уже установленную программу можно запустить с включенным режимом отладки без необходимости сначала построить, а затем загрузить специальную версию на сайт заказчика. Когда программное обеспечение установлено в удаленных местах, в которых может не быть людей и все, что вы можете сделать, это получить удаленный доступ к системе через Интернет (или, еще хуже, через медленное модемное соединение!), такая возможность может оказаться спасительным средством.
Наконец, можно использовать смешанную методику: условно компилируемый отладочный код для детальной, точной отладки, а постоянно присутствующий код для более грубого вывода.
Другой полезной уловкой является проверка вашим приложением специальных переменных окружения (документированных или иных). Это может быть особенно полезным для тестирования. Вот другой пример из нашего опыта с 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, ' ', 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, ' ', 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
.
Мы действительно устранили проблему[174]174
Переписав код управления буфером! – Примеч. автора.
[Закрыть], но между тем оставили на месте новую версию optimal_bufsize()
, чтобы можно было убедиться, что проблема не возникнет вновь.
Незначительное увеличение размера кода и его сложности более чем компенсируется возросшей гибкостью, которая есть теперь у нас для тестирования. Более того, поскольку это код изделия, пользователь в полевых условиях может с легкостью использовать эту особенность для тестирования, чтобы определить, не появилась ли сходная проблема. (До сих пор нам не приходилось просить проделать этот тест, но приятно осознавать, что мы могли бы это сделать, если бы пришлось.)
Часто бывает так, что программа вашего приложения работает на системе, на которой вы не можете использовать отладчик (как в случае сайта клиента). В таком случае вашей целью является возможность проверки внутреннего состояния программы, но извне. Единственным способом сделать это является заставить саму программу предоставить для вас эту информацию. Для этого существует множество способов.
• Всегда записывайте сведения в специфический файл. Это простейший способ: программа всегда записывает регистрационную информацию. Затем вы можете при возможности просмотреть файл.
Недостаток в том, что в какой-то момент регистрационный файл займет все дисковое пространство. Следовательно, у вас должны быть несколько файлов журналов, причем программа периодически должна переключаться между ними. Брайан Керниган рекомендует называть файлы журнала по дням недели: myapp.log.sun
, myapp.log.mon
и т.д. Преимуществом здесь является то, что вам не придется вручную удалять старые файлы; вы бесплатно получаете недельную стоимость файлов журналов.
• Записывайте в файл журнала лишь тогда, когда он уже существует. При запуске ваша программа записывает сведения в файл журнала, если он существует. В противном случае записи не происходит. Чтобы включить журналирование, сначала создайте пустой файл журнала.
• Используйте для сообщений фиксированный формат, который можно легко анализировать с помощью языков сценариев, таких, как awk
или Perl, для создания сводок и отчетов.
• В качестве альтернативы можно создать какую-нибудь разновидность XML, который является самоописывающимся и допускающим преобразование в другие форматы. (Мы не являемся большими поклонниками XML, но вас это не должно останавливать).
• Для журналирования используйте syslog()
; конечное расположение сообщений журналирования может контролироваться системным администратором, (syslog()
является довольно продвинутым интерфейсом; см. справочную страницу syslog(3)).
Выбор того, как регистрировать сведения, является, конечно, легкой частью. Трудной частью является выбор того, что регистрировать. Как и в остальных случаях разработки программ, стоит подумать, прежде чем программировать. Записывайте сведения о критических переменных. Проверьте их значения, чтобы убедиться, что они в приемлемом диапазоне или в других отношениях ведут себя, как ожидается. Записывайте исключительные условия; если появляется что-то, что не должно было, зарегистрируйте это и при возможности продолжайте исполнение.
Ключом является регистрация лишь тех сведений, которые вам нужны для отслеживания проблем, не больше и не меньше.
В предыдущей жизни мы работали для начинающей компании с двоичными исполняемыми файлами продукта, установленными на сайтах клиентов. Подключить отладчик к запущенной копии программы или запустить ее из отладчика на системе клиента было невозможно. Главный компонент продукта запускался не непосредственно из командной строки, а опосредованно, через сценарии оболочки, которые проделывали значительную первоначальную настройку.
Чтобы заставить программу выдавать при запуске журналируемые сведения, мы пришли к идее специальных отладочных файлов. Когда файл с определенным именем находился в определенном каталоге, программа выдавала бы информационные сообщения в файл журнала, который мы могли бы затем загрузить и проанализировать. Такой код выглядит следующим образом:
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) – один или более способов выполнения разработчиками недокументированных вещей с программой, обычно с бесчестными намерениями. В нашем примере лазейка была исключительно доброкачественной. Но беспринципный разработчик легко мог бы устроить создание и загрузку скрытой копии списка клиентов, картотеки персонала или других важных данных. По одной этой причине вы должны серьезно подумать, применима ли эта методика в вашем приложении.
Часто проблема может быть воспроизводимой, но лишь после того, как программа сначала обработает многие мегабайты вводимых данных. Или, хотя вы и знаете, какая функция вызывает сбой, он возникает лишь после вызова этой функции сотни или даже тысячи раз.
Это большая проблема, когда вы работаете в отладчике. Если вы установите контрольную точку на вызывающую сбой процедуру, вам придется набирать команду 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
, где, как мы ожидаем, находится действительная проблема.
Методика функции-ловушки полезна сама по себе. Однако, возможность переместить ее на уровень приложения умножает ее полезность, и она сохранила нам бесчисленное число часов отладки при отслеживании непонятных проблем.