Текст книги "Основы программирования в Linux"
Автор книги: Нейл Мэтью
Соавторы: Ричард Стоунс
Жанры:
Программирование
,сообщить о нарушении
Текущая страница: 40 (всего у книги 67 страниц)
Отладчик проекта GNU – исключительно мощный инструмент, способный снабжать множеством сведений о внутреннем состоянии выполняющихся программ. В системах, поддерживающих средство аппаратно устанавливаемых контрольных точек, можно применять gdb для наблюдения за изменениями переменных в режиме реального времени. Аппаратно устанавливаемые контрольные точки – это функция некоторых ЦПУ; такие процессоры способны автоматически останавливаться при возникновении определенных условий, обычно доступе к памяти в заданной области. Кроме того, gdb может следить (watch) за выражениями. Это означает, что с потерей производительности gdb может остановить программу, когда выражение принимает конкретное значение, независимо от того, в каком месте программы выполнялось вычисление.
Точки останова можно устанавливать со счетчиками и условиями, так что они включаются только после фиксированного числа проходов или при выполнении условия.
Отладчик gdb также способен подключаться к уже выполняющимся программам. Это очень полезно при отладке клиент-серверных систем, поскольку вы сможете отлаживать некорректно ведущий себя серверный процесс во время выполнения без необходимости останавливать и перезапускать его. Можно компилировать программы, например, с помощью строки gcc -O -g
, чтобы получить преимущества от применения оптимизации и отладочной информации. Недостаток заключается в том, что оптимизация может слегка переупорядочить текст программы, поэтому, когда вы будете выполнять программу в пошаговом режиме, может оказаться, что вы «скачете вперед и назад» по строкам, чтобы добиться того эффекта, что и в первоначальном тексте программы.
Отладчик gdb можно также применять для отладки аварийно завершившихся программ. Системы Linux и UNIX при аварийном завершении программы часто создают дамп ядра в файле с именем core. Это отображение карты памяти программы, которое содержит значения глобальных переменных в момент возникновения сбоя. Вы сможете использовать gdb для того, чтобы определить место в программе, вызвавшее аварийное завершение. Дополнительную информацию см. в интерактивном справочном руководстве к gdb.
Отладчик gdb доступен в соответствии с требованиями Общедоступной лицензии проекта GNU и его поддерживает большинство систем UNIX. Мы настоятельно рекомендуем вам, как следует изучить его.
Дополнительные средства отладки
Помимо полнофункциональных отладчиков, таких как gdb, Linux-системы обычно предоставляют и другие средства, которые можно применять для поддержки процесса отладки. Некоторые из них снабжают статической информацией о программе, другие обеспечивают динамический анализ.
Статический анализ предоставляет сведения только об исходном тексте программы. Программы ctags, cxref и cflow работают с исходными файлами и предлагают полезные данные о вызовах функций и их месте в программе.
Динамический анализ предоставляет информацию о том, как программа ведёт себя во время выполнения. Программы prof и gprof предлагают сведения о том, какие функции были выполнены, и сколько времени заняло их выполнение,
Давайте рассмотрим некоторые из этих средств и их вывод. Не все они будут доступны во всех системах, хотя у многих из этих средств есть свободно распространяемые версии.
Lint удаление ошибок из ваших программПервые системы UNIX предоставляли утилиту lint
. Эта программа по существу – препроцессор компилятора С со вставленными тестами, обеспечивающими некоторые проверки с точки зрения здравого смысла и вывод предупреждений. Среди прочего она обнаруживает случаи применения переменных до того, как им было присвоено значение, или случаи неиспользования аргументов функций.
Более современные компиляторы C могут ценой производительности времени компиляции формировать аналогичные предупреждения. Утилиту lint
, как таковую, обогнала стандартизация языка С. Поскольку средство основывалось на раннем компиляторе С, оно совсем не справляется с синтаксисом ANSI. Есть несколько коммерческих версий lint
для UNIX и одна версия в Интернете для Linux, названная splint
. Она известна под именем LClint, как часть проекта MIT (Massachusetts Institute of Technology, Массачусетский технологический институт), занимающегося разработкой средств формального описания. splint
, средство подобное lint
, может предоставлять полезные обзорные комментарии к программному коду. Найти splint
можно по адресу http://www.splint.org.
Далее приведена первоначальная версия (debug0.c) программы-примера, которую вы уже отладили.
/* 1 */ typedef struct {
/* 2 */ char *data;
/* 3 */ int key;
/* 4 */ } item;
/* 5 */
/* 6 */ item array[j] = {
/* 7 */ {"bill", 3},
/* 8 */ {"neil", 4},
/* 9 */ {"john", 2},
/* 10 */ {"rick", 5},
/* 11 */ {"alex", 1},
/* 12 */ };
/* 13 */
/* 14 */ sort(a, n)
/* 15 */ item *a;
/* 16 */ {
/* 17 */ int i = 0, j = 0;
/* 18 */ int s;
/* 19 */
/* 20 */ for(; i < n & s != 0; i++) {
/* 21 */ s = 0;
/* 22 */ for(j = 0; j < n; j++) {
/* 23 */ if(a[j].key > a[j+1].key) {
/* 24 */ item t = a[j];
/* 25 */ a[j] = a[j+1];
/* 26 */ a[j+1] = t;
/* 27 */ s++;
/* 28 */ }
/* 29 */ }
/* 30 */ n–;
/* 31 */ }
/* 32 */ }
/* 33 */
/* 34 */ main()
/* 35 */ {
/* 36 */ sort(array,5);
/* 37 */ }
В этой версии есть проблема в строке 20, где вместо предполагаемого оператора &&
применяется оператор &
. Далее приведен отредактированный пример вывода splint
, выполненной с этой версией программы. Обратите внимание на то, как она обнаруживает проблемы в строке 20 – тот факт, что вы не инициализировали переменную s
и что возможны проблемы с условием из-за некорректного оператора.
neil@susel03:~/BLP4e/chapter10> splint -strict debug0.c
Splint 3.1.1 – 19 Mar 2005
debug0.c:7:18: Read-only string literal storage used as initial value for
unqualified storage: array[0].data = «bill»
A read-only string literal is assigned to a non-observer reference. (Use -readonlytrans to inhibit warning)
debug0.c:8:18: Read-only string literal storage used as initial value for
unqualified storage: array[1].data = «neil»
debug0.c:9:18: Read-only string literal storage used as initial value for
unqualified storage: array[2].data = «john»
debug0.с:10:18: Read-only string literal storage used as initial value for
unqualified storage: array[3].data = «rick»
debug0.c:11:18: Read-only string literal storage used as initial value for
unqualified storage: array[4].data = «alex»
debug0.с:14:22: Old style function declaration
Function definition is in old style syntax. Standard prototype syntax is
preferred. (Use -oldstyle to inhibit warning)
debug0.с: (in function sort)
debug0.c:20:31: Variable s used before definition
An rvalue is used that may not be initialized to a value on some execution
path. (Use -usedef to inhibit warning)
debug0.с:20:23: Left operand of & is not unsigned value (boolean):
i < n & s != 0
An operand to a bitwise operator is not an unsigned values. This may have
unexpected results depending on the signed representations. (Use
-bitwisesigned to inhibit warning).
debug0.c:20:23: Test expression for for not boolean, type unsigned int:
i < n & s != 0
Test expression type is not boolean or int. (Use -predboolint to inhibit
warning);
debug0.с:25:41: Undocumented modification of a[]: a[j] = a[j + 1]
An externally-visible object is modified by a function with no /*@modifies@*/
comment. The /*@modifies ... @*/ control comment can be used to give a
modifies list for an unspecified function. (Use -modnomods to inhibit
warning)
debug0.c:26:41: Undocumented modification of a[]: a[j + 1] = t
debug0.c:20:23: Operands of & are non-integer (boolean) (in post loop test):
i < n & s != 0
A primitive operation does not type check strictly. (Use -strictops to
inhibit warning)
debug0.с:32:14: Path with no return in function declared to return int
There is a path through a function declared to return a value on which there
is no return statement. This means the execution may fall through without
returning a meaningful result to the caller. (Use -noret to inhibit
warning)
debug0.с:34:13: Function main declared without parameter list
A function declaration does not have a parameter list. (Use -noparams
to inhibit warning)
debug0.с: (in function main)
debug0.с:36:22: Undocumented use of global array
A checked global variable is used in the function, but not listed in its
globals clause. By default, only globals specified in .lcl files are
checked.
To check all globals, use +allglobals. To check globals selectively use
/*@checked@*/ in the global declaration. (Use -globs to inhibit warning)
debug0.с:36:17: Undetected modification possible from call to unconstrained
function sort: sort
An unconstrained function is called in a function body where
modifications are checked. Since the unconstrained function may modify
anything, there may be undetected modifications in the checked function.
(Use -modunconnomods to inhibit warning)
debug0.c:36:17: Return value (type int) ignored: sort(array, 5)
Result returned by function call is not used. If this is intended, can
cast result to (void) to eliminate message. (Use -retvalint to inhibit
warning)
debug0.c:37:14: Path with no return in function declared to return int
debug0.c:6:18: Variable exported but not used outside debug0: array
A declaration is exported, but not used outside this module. Declaration
can use static qualifier. (Use -exportlocal to inhibit warning)
debug0.c:14:13: Function exported but not used outside debug0: sort
debug0.c:15:17: Definition of sort
debug0.c:6:18: Variable array exported but not declared in header file
A variable declaration is exported, but does not appear in a header
file. (Used with exportheader.) (Use -exportheadervar to inhibit warning)
debug0.c:14:13: Function sort exported but not declared in header file
A declaration is exported, but does not appear in a header file. (Use
-exportheader to inhibit warning)
debug0.c:15:17: Definition of sort
Finished checking – 22 code warnings
$
Утилита выражает неудовольствие по поводу объявления функций в старом стиле (не ANSI) и несоответствия типов значений, возвращаемых функциями, и самими величинами, которые они возвращают (или нет) в действительности. Эти предупреждения не влияют на работу программы, но должны быть выведены.
Она также обнаружила две реальные ошибки в следующем фрагменте кода:
/* 18 */ int s;
/* 19 */
/* 20 */ for(; i < n & s != 0; i++) {
/* 21 */ s = 0;
Средство splint определило (выделенные цветом строки предыдущего вывода), что переменная s
используется в строке 20, но не была при этом инициализирована, и что оператор &
стоит на месте более обычного оператора &&.
В данном случае старшинство оператора изменяет значение условия и создает проблему в программе.
Обе эти ошибки были исправлены при чтении исходного текста программы до запуска процесса отладки. Несмотря на то, что пример мало изобретателен и служит только для демонстрации, подобные ошибки регулярно возникают в реальных программах."
Средства, отслеживающие вызовы функцийТри утилиты – ctags
, cxref
и cflow
– формируют часть стандарта X/Open и, следовательно, должны включаться в системы, представляемые как системы UNIX с программными средствами разработки.
Примечание
Эти утилиты и другие, упоминаемые в этой главе, могут не входить в состав вашего дистрибутива Linux. Если они пропущены, можно поискать их реализации в Интернете. Хорошая отправная точка (для дистрибутивов Linux, поддерживающих формат RPM-пакетов) – Web-сайты http://rpmfind.net и http://rpm.pbone.net. Можно попытаться поискать в нескольких репозитариях для конкретных дистрибутивов, включая http://ftp.gwdg.de/pub/opensuse/ для openSUSE, http://rpm.livna.org для Fedora и http://packages.slackware.it/ для Slackware.
ctags
Программа ctags
создает алфавитный указатель функций. Для каждой функции вы получаете перечень мест в программе, где она применяется, как алфавитный указатель к книге.
ctags [-a] [-f filename] sourcefile sourcefile ...
ctags -x sourcefile sourcefile ...
По умолчанию ctags
создает в текущем каталоге файл с именем tags, содержащий для каждой функции, объявленной в любом из входных файлов исходного кода, строки следующего вида:
announce app_ui.c /^static void announce(void) /
Каждая строка файла содержит имя функции, файл, в котором она объявлена, и регулярное выражение, которое можно использовать для поиска описания функции в файле. Некоторые редакторы, например Emacs, могут применять файлы этого вида для навигации в исходном тексте программы.
Кроме того, с помощью опции -х
в программе ctags
(если она доступна в вашей версии программы) вы можете формировать строки аналогичного вида в стандартном файле вывода.
find_cat 403 appui.с static cdc_entry find_cat(
Можно перенаправить вывод в другой файл с помощью опции -f filename
и добавить его в конец существующего файла, указав опцию -а
.
cxref
Программа cxref
анализирует исходный текст на языке С и формирует перекрестные ссылки. Она показывает, где в программе упоминается каждое символическое имя (переменная, директива #define
и функция). Программа создает отсортированный список с указанием места определения каждого идентификатора, которое помечается звездочкой, как показано далее:
SYMBOL FILE FUNCTION LINE
BASENID prog.с – *12 *96 124 126 146 156 166
BINSIZE prog.с – *30 197 198 199. 206
BUFMAX prog.с – *44 45 90
BUFSIZ /usr/include/stdio.h – *4
EOF /usr/include/stdio.h – *27
argc prog.с – 36
prog.с main *37 61 81
argv prog.с – 36
prog.с main *38 61
calldata prog.с – *5
prog.с main 64 188
calls prog.с – *19
prog.с main 54
На машине одного из авторов этой книги предыдущий вывод был сгенерирован в каталоге с исходными файлами приложения с помощью команды
$ cxref *.с *.h
но точный синтаксис зависит от версии. См. документацию к вашей системе и интерактивное справочное руководство для получения дополнительной информации о том, включена ли программа cxref
и как ее применять.
cflow
Программа cflow
выводит дерево вызовов функций – схему, показывающую, какие функции вызывают другие функции, какие функции вызываются этими другими и т.д. Эта схема полезна для выяснения структуры программы, понимания ее принципов действия и наблюдения за влиянием изменений, внесенных в функцию. Некоторые версии программы cflow
могут работать с объектными файлами так же, как с исходными. Подробности см. в интерактивном справочном руководстве.
Далее приведен пример вывода, полученный версией cflow
(cflow-2.0), которая есть в Интернете и поддерживается Марти Лейснером (Marty Leisner).
0 file_ungetc {prcc.c 997}
1 main {prcc.c 70}
2 getopt {}
3 show_all_lists {prcc.c 1070}
4 display_list {prcc.c 1056}
5 printf {}
6 exit {}
7 exit {}
9 usage {prcc.c 59}
10 fprintf {}
11 exit {}
Пример информирует о том, что функция main
вызывает (среди прочих) функцию show_all_lists
и что show_all_lists
в свою очередь вызывает функцию display_list
, которая вызывает функцию printf
.
У этой версии cflow
есть опция -i
, которая формирует инвертированный потоковый граф. Утилита cflow
перечисляет для каждой функции другие функции, вызывающие данную. Звучит не очень понятно, но на самом деле все просто. Далее приведен пример:
19 display_list {prcc.c 1056}
20 show_all_lists {prcc.c 1070}
21 exit {}
22 main {prcc.c 70}
23 show_all_lists {prcc.c 1070}
24 usage {prcc.c 59}
25 ...
74 printf {}
75 display_list {prcc.c 1056}
76 maketag {prcc.c 4 87}
77 show_all_lists {prcc.c 1070}
78 main {prcc.c 70}
79 ...
99 usage {prcc.c 59}
100 main {prcc.c 70}
В примере показано, что функцию exit
, например, вызывают функции main
, show_all_lists
и usage
.
Методика, зачастую полезная при попытках выяснить проблемы снижения производительности программы, называется профилированием выполнения (execution profiling). Профиль программы, обычно поддерживаемый специальными опциями компилятора и вспомогательными программами, показывает, где программа тратит время.
Программа prof
(и ее эквивалент в проекте GNU, gprof
) выводит отчёт из файла трассировки выполнения, который формируется во время выполнения профилируемой программы. Профилируемый исполняемый файл создается с помощью флага компилятора -p
(для prof
) или флага -pg
(для gprof
).
$ cc -pg -о program program.с
Программа компонуется со специальной библиотекой С, и в нее включается контрольный код. В конкретных системах он может отличаться, но общая цель – такая организация программы, которая позволяет часто прерывать выполнение и записывать этап выполнения. Контрольные данные записываются в файл mon.out (gmon.out для gprof
) в текущем каталоге.
$ ./program
$ ls -ls
2 -rw-r–r– 1 neil users 1294 Feb 4 11:48 gmon.out
Программа prof/gprof читает эти контрольные данные и выводит отчет. См. подробности, касающиеся опций программы, в интерактивном справочном руководстве. Далее в качестве примера приведен вывод (сокращенный) программы gprof.
cumulative self self total
time seconds seconds calls ms/call ms/call name
18.5 0.10 0.10 8664 0.01 0.03 doscan [4]
18.5 0.20 0.10 mcount (60)
14.8 0.28 0.08 43320 0.00 0.00 _number [5]
9.3 0.33 0.05 8664 0.01 0.01 _format_arg [6]
7.4 0.37 0.04 112632 0.00 0.00 _ungetc [8]
7.4 0.41 0.04 8757 0.00 0.00 _memccpy [9]
7.4 0.45 0.04 1 40.00 390.02 _main [2]
3.7 0.47 0.02 53 0.38 0.38 _read [12]
3.7 0.49 0.02 w4str [10]
1.9 0.50 0.01 26034 0.00 0.00 _strlen [16]
1.9 0.51 0.01 8664 0.00 0.00 strncmp [17]
Проверки соблюдения условий
Несмотря на то, что вставка на этапе разработки программы с помощью условной компиляции отладочного кода, такого как вызовы printf
, распространена, иногда оставлять такие сообщения в поставляемой программе непрактично. Но часто проблемы возникают во время работы программы из-за некорректных допущений или исходных данных, а не из-за ошибок кодирования. Это события, которых «не может быть никогда». Например, функция может быть написана в расчете на то, что ее входные параметры будут в определенном диапазоне. Если передать ей некорректные данные, она может сделать некорректной работу всей системы.
В тех случаях, когда внутренняя логика системы нуждается в подкреплении, X/Open предоставляет макрос assert
, применяемый для проверки правильности исходных данных и остановки выполнения программы в противном случае.
#include
void assert(int expression)
Макрос assert
вычисляет выражение и, если оно не равно нулю, выводит некоторую диагностическую информацию о стандартной ошибке и вызывает функцию abort
для завершения программы.
Заголовочный файл assert.h определяет макросы в зависимости от определения флага NDEBUG
. Если NDEBUG
определен во время обработки заголовочного файла, assert
определяется по существу как ничто. Это означает, что вы можете отключить проверки заданных выражений во время компиляции, компилируя с опцией -DNDEBUG
или вставив перед включением файла assert.h строку
#define NDEBUG
в каждый исходный файл.
Этот метод применения порождает проблему. Если вы используете assert
во время тестирования, но отключите макрос в рабочем коде, в вашем рабочем коде может оказаться менее строгая проверка, чем применявшаяся в процессе его тестирования. Обычно макросы assert
не оставляют включенными в рабочем коде – вряд ли вам понравится рабочий код, предоставляющий пользователю недружелюбное сообщение assert failed
и останавливающий программу. Быть может, лучше написать свою отслеживающую ошибки подпрограмму, которая проверяет выражение, использовавшееся в макросе, но не нуждается в полном отключении в рабочем коде.
Вы также должны убедиться в том, что у выражения макроса assert
нет побочных эффектов. Например, если вы применяете вызов функции с побочным эффектом, этот побочный эффект не проявится в рабочем коде с отключенными макросами assert
.
Выполните упражнение 10.2.
Упражнение 10.2. Программа assert.c.
Далее приведена программа assert.c, определяющая функцию, которая должна принимать положительное значение. Она защищает от ввода некорректного аргумента благодаря применению макроса assert
.
После включения заголовочного файла assert.h и функции "квадратный корень", проверяющей положительное значение параметра, вы можете писать функцию main
.
#include
#include
#include
#include
double my_sqrt(double x) {
assert(x >= 0.0);
return sqrt(x);
}
int main() {
printf(«sqrt +2 = %gn», my_sqrt(2.0));
printf(«sqrt -2 = %gn», my_sqrt(-2.0));
exit(0);
}
Теперь при выполнении программы вы увидите нарушение в макросе assert
при передаче некорректного значения. Точный формат сообщения о нарушении условия макроса assert в разных системах разный.
$ сс -о assert assert.с -lm
$ ./assert
sqrt +2 = 1.41421
assert: assert.c:7: my_sqrt: Assertion 'x >= 0.0' failed.
Aborted
$
Как это работает
Когда вы попытаетесь вызвать функцию my_sqrt
с отрицательным числом, макрос assert
даст сбой. Он предоставляет файл и номер строки, в которой нарушено условие и само нарушенное условие. Программа завершается прерыванием abort
. Это результат вызова abort
макросом assert
.
Если вы перекомпилируете программу с опцией -DNDEBUG
, макрос assert
не компилируется, и вы получаете NaN
(Not a Number, не число) – значение, указывающее на неверный результат при вызове функции sqrt
из функции my_sqrt
.
$ cc -о assert -DNDEBUG assert.с -lm
$ ./assert
sqrt +2 = 1.41421
sqrt -2 = nan
$
Некоторые более старые версии математической библиотеки генерируют исключение для математической ошибки, и ваша программа будет остановлена с сообщением "Floating point exception" ("Исключение для числа с плавающей точкой") вместо возврата NaN.