Брайан Керниган - UNIX — универсальная среда программирования
Из-за ограниченного объема книги мы лишь частично покажем вам применение каждой программы, а именно распечатаем содержимое стека, т.е. выведем функцию, выполнявшуюся при аварийном завершении программы, функцию, которая ее вызывала, и т.д. Первая функция, указанная в распечатке стека, это то место, где находилась программа, когда она была аварийно завершена.
Чтобы получить распечатку стека с помощью adb, нужно ввести команду $C:
$ adb pick core Вызывает adb
$C Запрос содержимого стека
~_strout(0175722,011,0,011200)
adjust: 0
fillch: 060542
__doprnt(0177345,0176176,011200)
~fprintf(011200,0177345)
iop: 01120
fmt: 0177345
args: 0
~pick(0177345)
s: 0177345
~main(035,0177234)
argc: 035
argv: 0177234
i: 01
buf: 0
ctl-d Завершение
$
Здесь речь идет о том, что main была вызвана из pick, которая вызвала fprintf, а она в свою очередь вызвала __doprnt, вызвавшую _strout. Так как __doprnt не упомянута где-либо в pick.с, ошибка должна быть где-то в fprintf или выше. (Строки после каждой функции в распечатке показывают значения локальных переменных. $С подавляет данную информацию так же, как сама $С делает это в некоторых версиях adb.) Попытаемся теперь сделать то же самое с помощью sdb:
$ sdb pick core
Предупреждение: 'a.out не компилируется с -g
lseek: address 0xa64 Функция, где программа аварийно завершилась
*t Запрос распечатки стека
lseek()
fprintf(6154,2147479154)
pick(2147479154)
main(30,2147478988,2147479112)
*q Выход
$
Информация размещена по-иному, но есть общая основа: fprintf. (Распечатка стека другая, так как это сделано на машине VAX-11/750, на которой стандартная библиотека ввода вывода реализована иначе.) И если мы взглянем на вызов fprintf в неправильной версии pick, то обнаружим некорректность:
fprintf("%s?", s);
Здесь нет stderr, так что строка формата используется как ссылка к FILE, и, конечно, получается хаос.
Мы показали вам типичную ошибку, которая является скорее результатом просмотра, а не неправильного программирования. Искать подобные ошибки при вызове функции с неверными аргументами можно также с помощью верифицирующей программы для Си lint(1). Эта программа рассматривает Си-программы с точки зрения наличия ошибок, аспектов переносимости и сомнительных конструкций. Если мы запустим lint с файлом pick.с, ошибка идентифицируется:
$ lint pick.с
...
fprintf, arg. 1 несовместим "llib-lc"(69) :: "pick.c"(28)
...
$
Это означает, что первый аргумент в стандартной библиотеке определен иначе, чем в строке 28 вашей программы. Таким образом дана точная информация о том, что неверно.
Программа lint, с одной стороны, указывает на недостатки в вашей программе, а с другой выдает много не относящихся к делу сообщений, которые мы выше опустили, и нужен некоторый опыт, чтобы уметь разбираться, какие из них необходимы, а какие следует игнорировать. Однако имеет смысл постараться, так как lint помогает обнаружить некоторые ошибки, которые человек увидеть практически не может. После длительного редактирования всегда стоит запустить lint и убедиться в том, что каждое выдаваемое этой программой предупреждение вам понятно.
6.7 Пример: zap
Программа zap, которая избирательно уничтожает процессы, отличается от той, что была представлена в виде файла shell в гл. 5. Главная проблема данной версии скорость. Она создает много процессов и поэтому работает медленно, что недопустимо для программы, уничтожающей процессы с ошибками. Если переписать zap на Си, ее быстродействие повысится. Мы, однако, снова воспользуемся ps, чтобы найти информацию о процессе. Это намного легче, чем выуживать информацию из ядра, и, кроме того, мы имеем переносимый вариант. Программа zap открывает программный канал, входной поток для которого берется из ps, и читает из него, как из файла. Функция popen(3) аналогична fopen, за исключением того, что первый аргумент является командой, а не именем файла. То же самое справедливо и для pclose, но здесь она нам не нужна.
/* zap: interactive process killer */
#include <stdio.h>
#include <signal.h>
char *progname; /* program name for error message */
char *ps = "ps -ag"; /* system dependent */
main(argc, argv)
int argc;
char *argv[];
{
FILE *fin, *popen();
char buf[BUFSIZ];
int pid;
progname = argv[0];
if ((fin = popen(ps, "r")) == NULL) {
fprintf(stderr, "%s: can't run %sn", progname, ps);
exit(1);
}
fgets(buf, sizeof buf, fin); /* get header line */
fprintf(stderr, "%s", buf);
while (fgets(buf, sizeof buf, fin) != NULL)
if (argc == 1 || strindex(buf, argv[1]) >= 0) {
buf[strlen(buf)-1] = ' '; /* suppress n */
fprintf(stderr, "%s? ", buf);
if (ttyin() == 'y') {
sscanf(buf, "%d", &pid);
kill(pid, SIGKILL);
}
}
exit(0);
}
Мы писали программу, чтобы использовать ps -ag (этот флаг системно зависим), но если вы не являетесь привилегированным пользователем, то можете уничтожать лишь свои собственные процессы.
Первый вызов fgets выбирает заголовок из ps; интересно выяснить, что случится, если попытаться уничтожить "процесс", соответствующий данному заголовку.
Функция sscanf представляет собой член семейства scanf(3) для форматного преобразования входной строки. Она преобразует строку, а не файл. Вызов kill из системы посылает специальный сигнал процессу; сигнал SIGKILL, определенный в <signal.h>, не может быть перехвачен или проигнорирован. Вы можете вспомнить пятую главу, где его численное значение равно девяти, но лучше использовать символические константы из файлов макроопределений, чем включать в свои программы загадочные числа.
Если аргументы отсутствуют, zap предоставляет каждую строку выходного потока ps как возможность для выбора. При наличии аргумента zap предлагает только те выходные строки ps, которые ему соответствуют. Функция strindex(s1, s2) проверяет, соответствует ли аргумент какой-либо части строки выходного потока ps, используя strncmp (см. табл. 6.2). Функция strindex возвращает позицию s2 в s1 или -1, если ее там нет.
strindex(s, t) /* return index of t in s, -1 if none */
char *s, *t;
{
int i, n;
n = strlen(t);
for (i = 0; s[i] != ' '; i++)
if (strncmp(s+i, t, n) == 0)
return i;
return -1;
}
В табл. 6.4 представлены широко используемые функции из стандартной библиотеки ввода вывода.
fp=fopen(s, mode) Открыть файл s; значения mode "r", "w", "a" соответствуют чтению, записи и добавлению (при ошибке возвращается NULL) c=gets(fp) Читать символ: getchar() это getc(stdin) putc(c, fp) Записать символ: putchar(c) это putc(c, stdout) ungetc(c, fp) Вернуть символ во входной файл fp; можно вернуть не более одного символа за раз scanf(fmt, a1, ...) Читать символы из stdin в a1, ... в соответствии с fmt. Каждый ai должен быть указателем fscanf(fp,...) Читать из файла fp sscanf(s,...) Читать из строки s printf(fmt, a1, ...) Форматировать a1, ... в соответствии с fmt; печатать в stdout fprintf(fp, ...) Печатать ... в файл fp sprintf(s, ...) Печатать ... в строку s fqets(s, n, fp) Читать не более n символов в s из fp (возвращается NULL по концу файла) fputs(s, fp) Печатать строку s в файл fp fflush(fp) Занести буферизованные данные выходного потока в файл fp fclose(fp) Закрыть файл fp fp=popen(s, mode) Открыть программный канал для команды s (см. fopen) pclose(fp) Закрыть программный канал fp system(s) Запустить команду s и ждать ее окончанияТаблица 6.4: Полезные стандартные функции ввода-вывода