355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Арнольд Роббинс » Linux программирование в примерах » Текст книги (страница 47)
Linux программирование в примерах
  • Текст добавлен: 6 мая 2017, 11:00

Текст книги "Linux программирование в примерах"


Автор книги: Арнольд Роббинс



сообщить о нарушении

Текущая страница: 47 (всего у книги 55 страниц)

15.3. Основы GDB

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

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

GDB, отладчик GNU, является отладчиком исходного кода. У него значительно больше возможностей, он значительно более переносим и более практичен, чем любой из sdb или dbx[162]162
  Мы говорим об оригинальном BSD dbx. В течение десяти лет мы использовали исключительно GDB – Примеч. автора.


[Закрыть]
.

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

Имеются графические отладчики; они предоставляют больший обзор исходного кода и обычно предоставляют возможность манипулировать программой как из окна командной строки, так и через компоненты GUI, такие, как кнопки и меню. Отладчик ddd[163]163
  ddd поставляется со многими системами GNU/Linux. Исходный код доступен на FTP-сайте проекта GNU ddd (ftp://ftp.gnu.org/gnu/ddd/) – Примеч. автора.


[Закрыть]
является одним из таких; он построен поверх GDB, так что если вы изучите GDB, вы сразу же сможете начать использовать ddd. (У ddd есть собственное руководство, которое следует прочесть, если вы собираетесь интенсивно его использовать.) Другим графическим отладчиком является Insight[164]164
  http.//sources.redhat.com/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[165]165
  ftp://ftp.gnu.org/gnu/gdb/Примеч. автора.


[Закрыть]
и самостоятельно его построить.

GDB поставляется с собственным руководством, которое занимает 300 страниц. В каталоге исходного кода GDB можно сгенерировать печатную версию руководства и самостоятельно его распечатать. Можно также купить в Free Software Foundation (FSF) готовые печатные экземпляры; ваша покупка поможет FSF и непосредственно внесет вклад в производство большего количества свободного программного обеспечения. (Информацию для заказа см. на веб-сайте FSF)[166]166
  http://www.gnu.orgПримеч. автора.


[Закрыть]
. Данный раздел описывает лишь основы GDB; мы рекомендуем прочесть руководство, чтобы научиться использовать все преимущества возможностей GDB.

15.3.1. Запуск GDB

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

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

Здесь исполняемый файл является отлаживаемой программой. Имя файла дампа, если оно имеется, является именем файла core, созданном при завершении программы операционной системой с созданием снимка процесса. Под GNU/Linux такие файлы (по умолчанию) называются core.pid[167]167
  Если вы хотите изменить такое поведение, см. sysctl(8) – Примеч. автора.


[Закрыть]
, где 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 "pRtb",

 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 дюймов[168]168
  Примерно 213×275 мм – Примеч. перев.


[Закрыть]
(размер «letter») в горизонтальном (landscape) формате. В ней на шести колонках предоставлена сводка наиболее полезных команд GDB. Мы рекомендуем распечатать ее и поместить под своей клавиатурой при работе с GDB.

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

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

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

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

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

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

#ifdef DEBUG

fprintf(stderr, "myvar = %dn", 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 = %dn", myvar);

...

DPRINT2("v1 = %d, v2 = %fn", v1, v2);

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

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

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

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

#ifdef MYAPPDEBUG

#define DPRINT(stuff) fprintf stuff

#else

#define DPRINT(stuff)

#endif

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

DPRINT((stderr, "myvar = %dn", myvar));

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

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

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

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

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

#ifdef MYAPPDEBUG

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

#else

#define DPRINT(mesg, ...)

#endif

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

DPRINT("myvar = %dn", myvar);

DPRINT("v1 = %d, v2 = %fn", v1, v2);

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

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

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


    Ваша оценка произведения:

Популярные книги за неделю