Использование отладчика

Сайт: Дистанционные курсы кафедры ИМО
Курс: Основы информатики и программирования для студентов ПМиИ, ИСиТ и ПИ
Книга: Использование отладчика

1. Введение в отладку

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

  • результаты выполнения программы на контрольных примерах (тестовых данных) отличаются от эталонных;
  • программа демонстрирует непредусмотренное поведение (например, непредвиденное падение).

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

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

Справедливости ради стоит отметить, что локализовать ошибку в ходе анализа не всегда оказывается возможным или ее устранение затрагивает значительный объем исходного кода. В этом случае, вероятно, ошибка закралась на ранних стадиях – при выборе архитектурного решения, проектировании элементов программной системы, выборе алгоритмов и структур данных. Далее такие серьезные ошибки не рассматриваются.

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

Простейший, но не всегда действенный способ подобного анализа – внедрение в текст программы вспомогательного отладочного кода, например, для вывода текущих значений наблюдаемых переменных в нескольких точках. Гораздо более эффективным является применение специальных программных инструментов – отладчиков, обеспечивающих возможность установки контрольных точек, пошагового выполнения инструкций программы, наблюдения за значениями объектов данных или вычисляемыми на основе этих значений выражениями, и даже подмены значений переменных в процессе выполнения программы для моделирования потенциально ошибочных ситуаций.

В данном руководстве рассматривается отладчик GDB (GNU Debugger) – переносимый отладчик, с 1988 г. развиваемый в рамках проекта GNU по созданию свободной Unix-подобной системы. Среди доступных аналогов отладчик GDB поддерживает наиболее широкий спектр форматов исполняемых файлов и отладочной информации, а также несколько десятков аппаратных платформ. Одной из мощных возможностей GDB является удаленная отладка – в этом случае отладчик работает на одном компьютере, а отлаживаемая программа и небольшой модуль отладчика (gdbserver) запускаются на другом, например, на встраиваемой системе или мобильном устройстве.

В то же время, в соответствии с философией GNU, подразумевающей, что каждая программа должна уметь выполнять свою основную задачу, но делать это хорошо, GDB позиционируется именно как инструмент отладки, а не среда для комфортной работы программиста. Вместо этого, GDB обеспечивает возможность интеграции с средами разработки. С GDB могут взаимодействовать такие IDE, как Qt Creator, Visual Studio,CLion, Eclipse, NetBeans и многие другие. Собственный интерфейс отладчика GDB – весьма скромный, что весьма кстати для целей обучения, так как позволяет сконцентрироваться на самом процессе отладки, а не на визуальных элементах интерфейса отладчика в IDE. К тому же, простой текстовый интерфейс отладчика позволяет работать удаленно с использованием эмулятора терминала.

2. Отладка вручную в GDB

В этом разделе мы рассмотрим основные приемы использования отладчика GDB, вручную управляя процессом отладки. Рассмотрим простой пример программы, получающей на вход значение температуры в градусах Цельсия и рассчитывающей температуру по шкале Фаренгейта. Ниже приведено содержимое файла исходного кода celsius.c:

/**
 * celsius.c -- converts temperature from degree centigrade to Fahrenheit 
 *
 * Copyright (c) 2014 Petrozavodsk State University
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 */

#include<stdio.h>

int main()
{
    double celsius, fahrenheit;

    printf("\nEnter temperature in Celsius: ");
    scanf("%lf", &celsius);

    fahrenheit = (1.8 * celsius) + 32;
    printf("Temperature in Fahrenheit: %f\n", fahrenheit);

    return 0;
}

и файла сборки Makefile:

celsius: celsius.o
	gcc -g -O0 -o celsius celsius.o

celsius.o: celsius.c
	gcc -g -O0 -c celsius.c

Будем считать, что оба файла находятся в некотором каталоге debug, как показано ниже.

Убедимся, что содержимое файла исходного кода соответствует приведенному выше листингу:

Обратите внимание, что при компиляции и сборке (в действиях Makefile) добавлены ключи -g -O0.

При выполнении этапов сборки с опцией -g в объектный файл (перемещаемый и исполняемый) помимо сгенерированного машинного кода добавляются сведения, позволяющие выполнять отладку. В частности, помещается исходный текст и информация о соответствии строк исходного текста машинным инструкциям. Это позволяет программисту, исполняя программу по шагам, видеть, в какой строке исходной программы выполнение происходит в данный момент.

Использование опции -O0 отключает оптимизацию, в результате которой соответствие между исходным и машинным кодом может быть нарушено.

Собираем программу и запускаем в отладчике gdb. Обратите внимание: в отладчике мы открываем исполняемый файл, не исходный!

Отладчик gdb запущен и ожидает наших инструкций. Это видно по приглашению командной строки отладчика (gdb) с курсором.

Командой list можно вывести на экран часть исходного кода программы. Заметим, что писать команду полностью не обязательно, можно обойтись буквой l.

Повторная команда list напечатает следующий фрагмент.


При этом, можно задать диапазон строк для вывода. Например, напечатать весь текст исходного кода для нашего примера можно командой l 1,14.


Для запуска программы воспользуемся командой run (r). Программа работает, при необходимости запрашивая данные:

И в итоге завершается. Отладчик готов к дальнейшей работе.

Разумеется, просто выполнить программу можно было бы и без всякого отладчика. Гораздо интереснее, начиная с определенной команды отследить работу программы по шагам. Для этого выберем строку, с которой начнем пошаговое выполнение. Пусть это будет строка 7.Установим контрольную точку на седьмую строку командой break (br).

Снова запустим программу командой run (r). Видно, что выполнение приостановлено перед седьмой строкой.

Далее будем выполнять программу по шагам, используя команду next (n). Помимо next имеется команда step (s), поведение которой аналогично, за исключением обработки вызовов функций. Если в выражении присутствует вызов функции, step переходит к пошаговому выполнению ее тела, а next - нет, выполняя выражение за один шаг.

Проанализировав поведение программы в нужном участке по шагам, продолжаем выполнение до следующей контрольной точки или завершения программы командой continue (c).

Для запущенной программы имеется возможность отслеживания текущих значений необходимых переменных. Поставим на контроль переменную celsius командой display (disp). Заметим, что для отслеживания программа должна быть запущена.

Запустим программу и включим отслеживание в момент останова в контрольной точке. Теперь переменная отслеживается. Обратите внимание, что переменная celsius имеет ненулевое значение. В тексте программы она не инициализируется начальным значением, поэтому там может оказаться все, что угодно (то, что было в выделенной для хранения переменной ячейке памяти ранее).

В список отслеживаемых переменных можно добавить несколько объектов. Добавим переменную fahrenheit. Эта переменная тоже не инициализирована и могла иметь произвольное значение, в нашем случае там оказалось нулевое значение. Все локальные переменные можно отобразить одной командой info locals.


Выполняя программу по шагам, убедимся, что значения переменных изменяются. Зная, каким образом должны пересчитываться объекты данных и увидев, как это происходит на самом деле при выполнении программы, можно понять источник ошибки.

Выполним оставшуюся часть программы.

Иногда для лучшего понимания поведения программы необходимо подменить значение тех или иных объектов прямо в ходе выполнения. Отладчик позволяет это сделать. Подменим значение переменной celsius, поменяв его на 81. Убедимся, что значение изменилось, выполнив еще один шаг программы.

В завершение покажем, что даже такая небольшая и очевидная программа как эта не свободна от недостатков.  Снимем контрольную точку (сами найдите подходящую команду, используя встроенную справочную систему или ресурсы сети Интернет) и запустим программу. Введем температуру "eleven degrees". Видно, что напечатанный в итоге результат неверен. Каким образом с помощью отладчика можно было бы показать, где возникает ошибка?


3. Интерфейс TUI отладчика GDB

В предыдущем разделе было показано, как отладчик GDB можно использовать в командном режиме. Это удобно для машины, но не так удобно для человека, поэтому GDB в настоящее время рассматривается в большей степени как debugger backend – программа, обеспечивающая пошаговое исполнение исследуемых программ и предоставляющая интегрированным средам разработки интерфейс взаимодействия для отправки отладочных инструкций и извлечения наблюдаемых значений переменных, регистров процессора, блоков памяти и т.п. Однако и для удобной отладки в терминале GDB поддерживает специальный режим, реализующий простой текстовый интерфейс – TUI (Text User Interface).

Включить TUI можно, находясь в отладчике, горячей комбинацией C-x a, или при запуске отладчика, указав опцию -tui.

Обратимся к примеру из предыдущего раздела. Откроем его в отладчике и активируем TUI. Откроется вспомогательное окно с листингом программы (помимо консоли GDB, которая по-прежнему доступна в нижней части экрана). Следуя примеру, поставим контрольную точку в седьмой строке и запустим программу. Текущая позиция отображается угловой скобкой и выделением. Контрольная точка – символами "B" (посещенная) или "b" (непосещенная) и "+" (активная) или "-" (неактивная). Строка над консолью GDB содержит, в частности, имя текущей функции (In: main) и номер строки (L7).


В режиме TUI доступно 4 окна: command (консоль GDB) и source (исходный код) мы сейчас наблюдаем. Окна assembly (ассемблерное представление машинного кода) и register (регистры процессора). В верхней части экрана может быть одно или два окна, переключение – C-x 1 и C-x 2. Выбор окон зависит от представления, которые переключаются командой layout: layout src – одно окно, исходный код; layout asm – одно окно, ассемблерный код; layout split – два окна, и исходный, и ассемблерный коды; layout reg - два окна, исходный или ассемблерный код (в зависимости от предыдущего представления) и окно регистров процессора (конкретный набор регистров переключается командой tui reg <набор> (например, tui reg general или tui reg float).


Одно из видимых окон является активным. Сменить активное окно можно комбинацией C-x o. В активном окне клавиши курсора, PgUp, PgDn и т.п. могут использоваться для прокрутки содержимого.

Наличие окна визуальное помогает ориентироваться в коде и упрощает процесс отладки. Еще более комфортной отладку делает специальный "режим одной кнопки" (Single Key Mode), в котором отладочная инструкция активируется одной кнопкой без необходимости подтверждения клавишей Enter. Режим включается и выключается комбинацией C-x s. Активные клавиши: c (continue), d (down), f (finish), n (next), q (quit), r (run), s (step), u (up), v (info locals), w (where).

4. Интеграция с редактором emacs

Как уже отмечалось ранее, gdb - отладчик, предназначенный для использования прежде всего как бэкенд отладки, это мощная программа с огромным набором возможностей, но предоставляющая достаточно примитивный интерфейс пользователя. Гораздо более удобный интерфейс обеспечивается фронтендами - программами-оболочками к отладчику gdb.

Как интерфейс к gdb можно использовать и редактор emacs!

Откроем пример из предыдущего раздела инструкции в редакторе emacs.

Перейдем к отладчику, используя команду M-x gdb (напомним, что клавиша M - Meta - на большинстве современных клавиатур соответствует Alt). Набираемая команда отображается в нижней части экрана.

Редактор предложит запустить gdb с указанными ключами. Согласимся.

Теперь мы имеем возможность выполнять команды gdb в соответствии с предыдущей инструкцией. При этом команды можно не набирать, а использовать кнопки на панели инструментов.

Однако, emacs предоставляет и еще более комфортный интерфейс. В меню Gud выберем раздел Gud-MI, а в нем пункт Display Other Windows. Теперь наряду с консолью gdb мы видим исходный код, локальный переменные, консоль ввода-вывода и другие окна.

Установим контрольную точку. Это можно сделать, кликнув левой кнопкой мыши слева от нужной строки исходного кода.

Запускаем программу, нажав на кнопку Run с зеленой иконкой Go. Выполнение программы будет остановлено перед строкой с установленной контрольной точкой. В окне locals отображаются типы, имена и текущие значения локальных переменных.

Кнопкой Next Line выполним строку программы. Обратите внимание, что ввод данных со стандартного потока ввода и вывод в стандартный поток вывода обеспечивается через окно input/output, а значения переменных изменяются соответственно выполняемым инструкциям. Напомним, что команда Step Line в большинстве случае эквивалентна, но если, в выражении присутствует вызов функции, то Step Line переходит к ее инструкциям (вход в функцию), а Next Line выполняет строку за один шаг в любом случае.

Выполним еще один шаг.

И еще один. Обратите внимание на изменения.

Выполним оставшуюся часть программы, нажав на Continue.

Покажем пример подмены значения переменной. Пусть в ходе работы программы переменная some_variable получила значение 123.

Щелкнув средней кнопкой мыши по значению в окне locals, мы можем подменить его, напечатав новое значение

Набираем новое значение и подтверждаем изменение клавишей Enter.

Выполняя программу далее, убедимся, что в вычислениях используется новое значение.