Отладчик, поставляемый с FreeBSD, называется gdb (GNU debugger). Он запускается по команде
% gdb progname
хотя большинство предпочитает запускать его из Emacs. Вы можете сделать это так:
M-x gdb RET progname RET
Использование отладчика позволяет вам запустить программу в более контролируемом окружении. Как правило, вы сможете выполнить программу в пошаговом режиме построчно, изучать значения переменных, изменять их, указывать отладчику выполнять программу до определенного места и затем остановиться, и так далее. Вы даже можете подключиться к уже работающей программе или загрузить файл дампа для изучения причины ошибки в программе. Возмлжно даже отладить ядро, хотя это делается немножко хитрее, чем отладка пользовательских приложений, которым посвящен этот раздел.
В gdb имеется достаточно хорошая встроенная система помощи, а также набор info-страниц, так что в этом разделе упор будет делаться на несколько основных команд.
Наконец, если вы находите, что его выдача команд в стиле командной строки в текстовом режиме неудобна, то в Коллекции Портов для него имеется графический инструмент xxgdb.
Этот раздел предназначен быть введением в использование gdb и не покрывает специальные вопросы, такие, как отладка ядра.
Чтобы получить максимумальный результат от использования gdb, вам нужно откомпилировать программу с параметром -g. Отладчик будет работать и без этой опции, но вы сможете увидеть только название текущей функции, но не ее исходный код. Если вы увидите такое сообщение:
... (no debugging symbols found) ...
при запуске gdb, то определите, что программа не была откомпилирована с опцией -g.
В приглашении gdb наберите команду break main. Это укажет отладчику пропустить предварительный подготовительный код программы и начать сразу с вашего кода. Теперь выдайте команду run для запуска программы--она начнет выполняться с подготовительного кода и затем будет остановлена отладчиком при вызове main(). (Если вы когла-либо удивлялись, откуда вызывается main(), то теперь вы должны знать!).
Теперь вы можете выполнить программу построчно по шагам, нажимая n. Если вы попали на вызов функции, то можете перейти в нее, нажав s. Оказавшись в вызове функции, вы можете вернуться из пошагового выполнения функции нажатием f. Вы можете также использовать команды up и down для просмотра вызывающей подпрограммы.
Вот простой пример того, как выявить ошибку в программе при помощи gdb. Это наша программа (с намеренно допущенной ошибкой):
#include <stdio.h> int bazz(int anint); main() { int i; printf("This is my program\n"); bazz(i); return 0; } int bazz(int anint) { printf("You gave me %d\n", anint); return anint; }
Эта программа устанавливает значение переменной i равным 5 и передает ее в функцию bazz(), которая выводит переданное нами число.
При компиляции и запуске программы мы получили
% cc -g -o temp temp.c % ./temp This is my program anint = 4231
Это не то, что мы ожидали! Самое время посмотреть, что же происходит!
% gdb temp GDB is free software and you are welcome to 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. GDB 4.13 (i386-unknown-freebsd), Copyright 1994 Free Software Foundation, Inc. (gdb) break main Пропуск начального кода Breakpoint 1 at 0x160f: file temp.c, line 9. gdb устанавливает точку останова в main() (gdb) run Запуск до вызова main() Starting program: /home/james/tmp/temp Программа начинает работать Breakpoint 1, main () at temp.c:9 gdb останавливается в main() (gdb) n Переход к следующей строке This is my program Программа выводит (gdb) s Переход в bazz() bazz (anint=4231) at temp.c:17 gdb выводит стек вызовов (gdb)
Минуточку! Как параметр anint оказался равным 4231? Разве мы не присвоили ему значение 5 в функции main()? Давайте перейдем к функции main() и взглянем туда.
(gdb) up Переход вверх по стеку вызовов
#1 0x1625 in main () at temp.c:11 gdb выводит стек вызовов
(gdb) p i Вывод значения переменной i
$1 = 4231 gdb выводит 4231
О боже! Судя по коду, мы забыли инициализировать переменную i. Вы хотели сделать вот что
... main() { int i; i = 5; printf("This is my program\n"); ...
но забыли про строку i=5;. Так как мы не присвоили начальное значение для i, то переменная принимает случайное значение, оказывающее в соответствующей области памяти при работе программы, и в нашем случае это оказалось число 4231.
Note: gdb выводит стек вызовов всякий раз, когда мы вызываем или возвращаемся из функции, даже если мы используем команды up и down для продвижения по стеку. При этом выводится имя функции и значения ее оргументов, что помогает нам отслеживать, где мы находимся и что происходит. (Стек является областью, где программа сохраняет информацию о передаваемых функциям аргументах и о том, куда нужно перейти после возврата из функции).
Файл дампа, вообще говоря, является файлом, содержащим полный образ процесса в момент его сбоя. В "добрые старые времена" программисты выводили шестнадцатиричные распечатки файлов дампа и корпели над справочниками по машинным кодам, но сейчас жизнь несколько облегчилась. В частности, во FreeBSD и других системах на основе 4.4BSD файлы дампа называются progname.core, а не просто core, для того, чтобы было понятнее, к какой программе относится соответствующий файл дампа.
Для исследования файла дампа запустите gdb обычным образом. Вместо того, чтобы выдавать команду break или run, наберите
(gdb) core progname.core
Если вы не в том же каталоге, что и файл дампа, то вам нужно сначала выполнить команду dir /path/to/core/file.
Вы должны увидеть нечто вроде следующего:
% gdb a.out GDB is free software and you are welcome to 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. GDB 4.13 (i386-unknown-freebsd), Copyright 1994 Free Software Foundation, Inc. (gdb) core a.out.core Core was generated by `a.out'. Program terminated with signal 11, Segmentation fault. Cannot access memory at address 0x7020796d. #0 0x164a in bazz (anint=0x5) at temp.c:17 (gdb)
В этом случае программа называлась a.out, так что файл дампа называется a.out.core. Мы можем видеть, что программа завершилась аварийно из-за попытки доступа к области памяти, ей недоступной, в функции bazz.
Иногда бывает полезно иметь возможность просмотреть, как функция была вызвана, потому что в сложной программе проблема могла появиться в любом месте большого стека вызовов. Команда bt заставляет gdb выдать обратную трассировку стека вызовов:
(gdb) bt #0 0x164a in bazz (anint=0x5) at temp.c:17 #1 0xefbfd888 in end () #2 0x162c in main () at temp.c:11 (gdb)
При сбое программы была вызвана функция end(); в этом случае функция bazz() была вызвана из main().
Одной из выдающихся особенностей gdb является то, что он может подключаться к программе, которая уже выполняется. Конечно, при этом предполагается, что у вас достаточно полномочий, чтобы это осуществить. Имеется проблема в ситуации, когда вы хотите выполнить трассировку порождаемого процесса, но отладчик позволяет вам трассировать только родительский.
В этой ситуации нужно запуститть еще один отладчик gdb, воспользоваться командой ps для поиска идентификатора порожденного процесса и выполнить команду
(gdb) attach pid
в gdb, после чего отлаживать программу обычным образом.
"Это все хорошо", думаете, наверное, вы, "но к моменту, когда я все это сделаю, порожденный процесс уже завершит свою работу". Может быть, и нет, дорогой читатель, и вот как это делается (согласно info-страницам программы gdb):
... if ((pid = fork()) < 0) /* _Always_ check this */ error(); else if (pid == 0) { /* child */ int PauseMode = 1; while (PauseMode) sleep(10); /* Wait until someone attaches to us */ ... } else { /* parent */ ...
Теперь все, что вам нужно сделать, это подключиться к порожденному процессу, установить значение переменной PauseMode в 0 и дождаться возврата из вызова функции sleep()!