Журнал LinuxFormat - перейти на главную

LXF120:debug

Материал из Linuxformat
Перейти к: навигация, поиск
Отладчик GNU Идейно выдержанное средство поиска ошибок в ваших приложениях

Содержание

GDB: Избавимся от ошибок

Один из основных принципов открытого ПО – «больше глаз – меньше ошибок»; так чего же вы ждете? Андрей Боровский познакомит вас с необходимым инструментарием.

При первом знакомстве отладчик GNU Debugger (GDB) может напугать программистов, привыкших к встроенным отладчикам графических интегрированных сред разработки. На самом деле все не так страшно. Выучив несколько простых команд, вы сможете сделать с помощью отладчика GNU все, что вы могли бы сделать в плане отладки в средах Microsoft или Borland. Выучив еще несколько команд, вы сможете делать такое, что пользователям графических IDE и не снилось. И хотя в наше время безалкогольного шампанского и бескофеинового кофе в Linux появились свои графические IDE со встроенными функциями отладки (Eclipse, Qt Creator), изучение возможностей GDB все равно будет вам полезно, поскольку «за кулисами» указанные среды вызывают именно его.

Выдумывать специальные примеры для отладки – дело неблагодарное, поэтому мы рассмотрим этот процесс на примере программы Cuneiform, «доводкой» которой я занимаюсь уже некоторое время (LXF118). Не пугайтесь, вам не придется вникать в логику работы Cuneiform. Все выбранные нами примеры самоочевидны.

Приступаем к отладке

Чтобы извлечь из GDB максимум, программу, предназначенную для отладки, следует скомпилировать с дополнительной отладочной информацией. Ее добавлением управляет ключ -g команд gcc и g++. Файл программы или библиотеки, скомпилированный с добавлением отладочной информации, занимает больше места, чем обычный, однако вам вовсе не обязательно перекомпилировать модуль по окончании отладки. Удалить отладочную информацию из исполняемого файла можно в любой момент с помощью утилиты strip.

Подготовив двоичные модули Cuneiform, мы запускаем отладчик с помощью команды gdb. Перед нами появляется приглашение командной строки: (gdb). Далее в примерах команд мы будем указывать его, чтобы отличать команды интерактивного режима отладчика от других. Интерактивный режим GDB похож на таковой в командной оболочке Bash. Более того, в GDB задействованы многие полезные функции командной оболочки: например, автоматическое завершение команды с помощью клавиши табуляции, преобразование символа ~ и история команд. Есть даже свой аналог сценариев оболочки. Для выхода из GDB служит команда quit.

Мы должны указать отладчику имя программы, которую собираемся отлаживать. Это можно сделать во время запуска GDB:

$ gdb cuneiform

А можно и после запуска Cuneiform с помощью команды exec-file:

(gdb) exec-file cuneiform

В любом случае, GDB лишь подготовит среду для отладки программы, но не запустит ее на выполнение. Вообще, следует помнить, что отладчик всегда находится в одном из трех режимов: отлаживаемая программа работает, отлаживаемая программа приостановлена и программа не выполняется. Большую часть команд отладчика можно вводить в последних двух режимах. Для запуска программы служит команда run:

(gdb) run rf2.bmp -l rus_fra -o out.txt

Все аргументы, которые мы указываем в команде run, передаются непосредственно программе, запускаемой на отладку. После ввода приведенной выше команды мы увидим:

Starting program: /usr/local/bin/cuneiform rf2.bmp -l rus_fra -o out.txt
Cuneiform for Linux 0.7.0 (multilang)
Program exited normally.

Текст, появляющийся между строками «Starting program:...» и «Program exited normally.», представляет собой консольный вывод отлаживаемой программы. Пока что отладчик не сообщил нам каких-либо полезных сведений о ней, но даже от запуска приложения в этом режиме может быть толк. Если во время выполнения произойдет ошибка сегментации, отладчик, помимо прочего, покажет нам состояние стека процедур на момент ее возникновения. Также, если программа зависнет и мы завершим ее выполнение с помощью Ctrl+C, нам будут выданы сведения о том, в каком месте программы была прервана ее работа, например:

Program received signal SIGINT, Interrupt.
0xb7d8c8b4 in IntervalsBuild (y=979) at /home/andrei/bazaar/cuneiform-multilang/cuneiform_src/Kern/rblock/sources/c/ltexcomp.c:144
144 while (x < nWidth && (pLine [x] & BlackMask) == 0)

Здесь мы видим имя функции, во время выполнения которой была прервана работа программы (то есть IntervalsBuild), значение параметров, с которыми она была вызвана (в нашем случае – один параметр, y, со значением 979), имя файла и номер строки исходного текста, соответствующего прерванному фрагменту, и саму строку кода.

Очень часто даже этой информации достаточно для того, чтобы вызвать озарение у программиста, ищущего ошибку. Но GDB способен показать нам гораздо больше. Мы можем просмотреть значения переменных, доступных нам в данном контексте (как минимум, это x, nWidth, pLine, и BlackMask). Для этого воспользуемся командой print:

(gdb) print x
Результат:
$1 = 681

Команда:

(gdb) print pLine[x]
Результат:
$2 = 0 '\0'

Таким образом мы узнаем, что на момент прерывания переменная x содержала значение 681, а элемент массива pLine[681] – символ '\0' . Если мы забыли или не знали, какой тип имеет переменная pLine, мы можем узнать это, не заглядывая в исходники:

(gdb) ptype pLine
type = unsigned char *

Но и это еще не все. Можно изменить значение переменной:

(gdb) set x=0

а затем возобновить выполнение программы с помощью команды continue.

Точки останова

Поскольку современные микропроцессоры работают очень быстро, комбинация клавиш Ctrl+C далеко не всегда останавливает программу именно там, где нам нужно. Как правило, мы хотим, чтобы ее выполнение было прервано в заранее определенном месте. Для этого и служат точки останова (breakpoint). Наберите

(gdb)break copy_text

Эта команда создает точку останова в начале функции copy_text (ленивые могут ввести просто b copy_text). В ответ на ввод команды GDB скажет:

Breakpoint 1 at 0xb7f57f35: file /home/andrei/bazaar/cuneiform-multilang/cuneiform_src/Kern/puma/c/spcheck.cpp, line 85.

Это значит, что отладчик нашел интересующую нас функцию и строку кода, соответствующую точке останова. Теперь мы снова запускаем программу Cuneiform на выполнение с помощью команды run. Когда процессор достигнет точки останова (если это случится), выполнение программы будет прервано, и на экране появится следующее сообщение:

Breakpoint 1, copy_text (word=0x9452338) at /home/andrei/bazaar/cuneiform-multilang/cuneiform_src/Kern/puma/c/spcheck.cpp:85
85 word->text = (Word8 *) malloc((word->wlen + 1)*sizeof(Word8));
Current language: auto; currently c++

Мы снова видим имя функции, выполнение которой было приостановлено, имя и значение переданного ей параметра и строку исходного текста, которая будет выполнена далее. Команда

(gdb) ptype word

сообщает нам нам следующее:

type = struct _SPWord {
Bool32 is_latin;
CSTR_rast begin;
CSTR_rast end;
Word8 *text;
int wlen;
} *

Мы видим, что переменная word представляет собой указатель на экземпляр структуры _SPWord. Чтобы узнать значения полей структуры, наберите

(gdb) output * word

Команда output делает то же, что и команда print, но при этом не утруждает себя излишним форматированием. Результат выполнения команды выглядит так:

{is_latin = 1, begin = 0x93e7008, end = 0x93c2a20, text = 0x1cc08f3f <Address 0x1cc08f3f out of bounds>, wlen = 6}

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

(gdb) step

мы переходим к следующей строке:

86 CSTR_rast rast = word->begin;

Разница между командами step и next заключается в том, что step «заходит» в каждую функцию, вызов которой встречается у нее на пути, и проходит ее построчно, тогда как next «перепрыгивает» вызов функции, выполняя его за один шаг. Команда step по смыслу соответствует командам Trace into/Step into интегрированных отладчиков Borland и Microsoft, тогда как команда next соответствует их командам Step over. Запуск приостановленной программы осуществляется командой continue. Вместо полных имен команд step, next и continue можно использовать их однобуквенные псевдонимы – s, n и c соответственно. Если вы хотите, чтобы программа сделала сразу 5 шагов, можете просто скомандовать

(gdb) s 5

Если же построчной отладки вам недостаточно, перейдите на уровень отслеживания выполнения отдельных инструкций процессора с помощью команд stepi и nexti.

Точки останова не обязательно должны располагаться в начале функций. Например, если мы скомандуем

(gdb) break 97

следующая точка останова будет создана в строке 97 файла /home/andrei/bazaar/cuneiform-multilang/cuneiform_src/Kern/puma/c/spcheck.cpp (поскольку он является текущим). Если вы хотите создать точку останова на заданной строке в другом исходном файле, команда должна выглядеть так:

(gdb) break имя_файла:номер_строки

Можно также указывать для точки останова определенный адрес.

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

151 if (!strchr(“,.():;!?\» «» %”, nxt->vers->Alt[0].Code[0])) {
152 rast->vers->Alt[0].Code[0] = ' ';
153 rast->vers->Alt[0].Code[1] = 0;
154 rast = CSTR_GetPrev(rast);
155 continue;
156 } else{
157 i++ ... }

отладчик разместит ее в строке 157. Дело в том, что строке 155 (как и строке 156) нельзя сопоставить машинный код. Первая строка после строки 155, для которой это можно сделать – строка 157, но она находится в другом логическом блоке. Очевидно, что это совсем не то, чего мы хотели. Самое неприятное заключается в том, что отладчик не покажет нам номер строки, на которой на самом деле была создана точка останова, до тех пор, пока эта точка не будет вызвана. Правильным решением может быть создание новой точки останова в строке 154, но в этом случае программа будет приостановлена до вызова функции CSTR_GetPrev(). Если нам нужно отследить результат выполнения CSTR_GetPrev(), точку останова следует создавать внутри этой функции (можно, конечно, остановиться до вызова CSTR_GetPrev(), а затем пройтись по функции серией команд step).


Помимо локаций, команда break позволяет задать для точки останова условие. Им может быть любое выражение, возвращающее значение, приводимое к типу int и действительное в контексте данного кадра отладки. Вы можете создать несколько точек останова в одной строке (это имеет смысл для условных точек останова).

Если вы не хотите больше прерывать выполнение программы на текущей точке останова, очистите ее с помощью команды clear. Любую точку останова можно удалить с помощью команды delete, указав в качестве аргумента номер точки останова (он присваивается автоматически при выполнении команды break). Если вы заранее уверены, что точка останова понадобится вам только один раз, создайте однократную точку останова с помощью команды tbreak (ее синтаксис такой же, как и у break).

Предъявите ваши данные

С некоторыми командами, позволяющими заглянуть в содержимое переменных отлаживаемой программы, мы уже знакомы. Когда выполнение программы приостановлено, мы можем просмотреть содержимое любой переменной, доступной в данном контексте. Очень часто в процессе отладки бывает необходимо отследить, как меняется значение некоторой переменной по ходу выполнения программы. Чтобы не вызывать каждый раз команду print, мы можем создать контрольную точку доступа к данным (watch).

Рассмотрим практический пример. В программе Cuneiform есть функция make_tokens, которая в ходе своей работы перебирает элементы некоего связного списка. Указатель на текущий элемент списка хранится в локальной переменной rast. В ходе отладки программы мне требовалось посмотреть, как меняются значения rast, для чего я решил создать контрольную точку доступа к данным. Такие точки осуществляют мониторинг обращения к определенному адресу памяти на аппаратном уровне. Это означает, между прочим, что контрольную точку доступа нельзя определить до тех пор, пока наблюдаемая переменная не будет создана, то есть, в нашем примере, до тех пор, пока не будет вызвана функция make_tokens. Таким образом, прежде чем устанавливать контрольную точку доступа, мне необходимо остановить программу в начале вызова make_tokens:

(gdb) break make_tokens
(gdb) c
Breakpoint 16, make_tokens (line=0x957a108, words=0xbf867b98) at /home/andrei/bazaar/cuneiform-multilang/cuneiform_src/Kern/puma/c/spcheck.cpp:140
140 CSTR_rast rast=CSTR_GetFirstRaster(line);


Теперь можно установить контрольную точку доступа к переменной rast:

(gdb) watch rast
Hardware watchpoint 17: rast

Обратите внимание, что значение rast изменяется в той самой строке (140), на которой было приостановлено выполнение программы. Это значит, что уже следующий вызов next приведет к изменению содержимого переменной:

(gdb) n
Hardware watchpoint 17: rast
Old value = (CSTR_rast) 0x0
New value = (CSTR_rast) 0x957a19c

Отладчик сообщает нам, что значение rast изменилось с 0x0 на 0x957a19c. Ниже показан дальнейший результат пошагового выполнения функции:

142 bool wb = true;
(gdb) n
143 int wc = 0;
(gdb) n
144 for(rast = CSTR_GetNext(rast);rast;rast=CSTR_GetNext(rast))
(gdb) n
Hardware watchpoint 17: rast
Old value = (CSTR_rast) 0x957a19c
New value = (CSTR_rast) 0x95776d0

Мы видим, что при входе в цикл for значение rast (которая является его управляющей переменной) тоже изменилось. Если бы значение rast было случайно перезаписано где-то между двумя явными обращениями к переменной, отладчик сообщил бы нам об этом.

Установленная нами контрольная точка существует столько же, сколько и переменная rast, то есть до выхода из функции make_tokens: при следующем вызове make_tokens ее придется создавать снова. Для удаления контрольных точек доступа служит уже известная нам команда delete.

Отслеживать изменение содержимого переменной с помощью комбинации команд watch и step/next не очень-то удобно. У отладчика GNU есть другой, лучший способ. Команда awatch будет останавливать выполнение программы всякий раз, когда значение заданной переменной считывается или записывается. Так же, как и watch, команда awatch стремится использовать аппаратные ловушки для перехвата обращений к конкретному адресу и может выявлять неявные обращения к нему.

Как я сюда попала?

Иногда программисты тоже пытаются найти ответ на этот сакраментальный вопрос. Если в программе во время отладки происходит нечто, требующее вашего внимания, вам может понадобится не только информация о функции, при вызове которой случилось страшное, но и сведения о полном состоянии стека вызовов. Команда backtrace позволяет узнать, какими путями программа попала в данную точку. Посмотрим результат ее выполнения для многострадальной функции copy_text (предполагается, что выполнение программы Cuneiform приостановлено в ее начале):

(gdb) backtrace
#0 copy_text (word=0x983d348) at /home/andrei/bazaar/cuneiform-multilang/cuneiform_src/Kern/puma/c/spcheck.cpp:85
#1 0xb7f213ff in make_tokens (line=0x9789278, words=0xbff78a88) at /home/andrei/bazaar/cuneiform-multilang/cuneiform_src/Kern/puma/c/spcheck.cpp:172
#2 0xb7f2158e in mix_lines (ruseng=0x9789278, local=0x97af118, rus=0x9794578) at /home/andrei/bazaar/cuneiform-multilang/cuneiform_src/Kern/puma/c/spcheck.cpp:369
#3 0xb7f1eeaf in MultilangRecognizeStringsPass1 () at /home/andrei/bazaar/cuneiform-multilang/cuneiform_src/Kern/puma/c/partrecog.cpp:425
#4 0xb7f1fb55 in Recognize () at /home/andrei/bazaar/cuneiform-multilang/cuneiform_src/Kern/puma/c/partrecog.cpp:798
#5 0xb7f226f7 in PUMA_XFinalRecognition () at /home/andrei/bazaar/cuneiform-multilang/cuneiform_src/Kern/puma/main/puma.cpp:600
#6 0x0804ad18 in?? ()
#7 0xb7463685 in __libc_start_main () from /lib/tls/i686/cmov/libc.so.6
#8 0x08049fb1 in?? ()

В переводе на русский язык сказанное отладчиком звучит так: «Функция PUMA_XFinalRecognition (), вызванная из функции верхнего уровня, вызвала функцию Recognize (), которая вызвала функцию MultilangRecognizeStringsPass1 (), которая вызвала функцию mix_lines (), которая вызвала функцию make_tokens (), которая и вызвала функцию copy_text()». Но это не все. Благодаря команде backtrace мы можем узнать не только адреса функций и значения их параметров (они приведены в скобках), но и локации вызовов в исходных текстах программы (copy_text вызывается в нескольких разных местах функции make_tokens(), и нам, разумеется, полезно знать, из какого именно места она была вызвана на этот раз). Не могу не отметить, что команда backtrace помогает не только при отладке кода, но и при изучении работы программы, написанной другими людьми. Фактически у нас перед глазами «живая» цепочка вызовов функций в сложной программе, снабженная значениями их аргументов и другими полезными сведениями.

Графический отладчик DDD

Из всех графических отладчиков, использующих GDB, мы рассмотрим только DDD. Такая честь выпала ему потому, что этот отладчик наиболее прозрачно интегрирован с GDB. Основные команды графической оболочки соответствуют командам GDB, а в нижней части главного окна DDD мы видим фрагмент консоли GDB, в которой отображаются вывод отладчика GNU и его командная строка. Таким образом, если возможностей DDD вам не хватает, вы всегда можете обратиться к отладчику GDB напрямую. Но главное преимущество DDD по сравнению с «чистым» GDB – это возможность легко ориентироваться в исходных текстах программы. LXF

Персональные инструменты
купить
подписаться
Яндекс.Метрика