Date Редакция Категория comp Теги C / gdb

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

В качестве примера рассмотрим отладку программы вычисления факториала

#include <stdio.h>
#include <stdlib.h>

int fact(int num)
{
    if(num <= 1)
    {
        return 1;
    }

    return num * fact(num - 1);
}

int main(int argc, char **argv)
{
    int a = atoi(argv[1]);
    printf("%d! = %d\n", a, fact(a));

    return 0;
}

Скомпилируем ее с включением отладочной информации:

g++ -g fact.c -o fact

Опция -g сообщает компилятору о том, что в программу нужно включить отладочную информацию, которой и воспользуется отладчик GDB. Опция -o сообщает компилятору g++ имя исполняемого файла (fact). Если ее не указать, то, по умолчанию, компилятор создаст исполняемый файл и назовет его a.out.

Запустим нашу программу в режиме отладки:

gdb -q fact

Опция -q используется для того, чтобы не выводилась информация о версии отладчика, лицензионное соглашение и т. п.

Итак, отладчик запущен. Теперь можно выполнять его команды.

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

(gdb) list 4
1   #include <stdio.h>
2   #include <stdlib.h>
3   
4   int fact(int num)
5   {
6       if(num <= 1)
7       {
8           return 1;
9       }
10  

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

Запустим теперь выполнение нашей программы отладчиком. Это можно сделать одной из двух команд: run или start. Отличие между ними в том, что после вызова start отладчик останавливается на входе в функцию main(), а команда run выполняется до тех пор, пока не встретит точку останова (о них мы поговорим позднее).

Обе эти команды принимают в качестве параметров аргументы программы, так же, как если бы программа запускалась из командной строки.

Запустим программу на выполнение с помощью start:

(gdb) start 4
Temporary breakpoint 1 at 0x80484fc: file fact.c, line 16.
Starting program: /home/dima/work/fact 4
Temporary breakpoint 1, main (argc=2, argv=0xbffff1e4)
    at /home/dima/work/fact.c:16
16      int a = atoi(argv[1]);

Как видно, отладчик остановился на входе в функцию main(), и вывел содержимое и номер строки, перед которой остановился.

Используя команду print, можно вывести на экран значения переменных и выражений. Причем писать полное название команды необязательно, достаточно использовать ее краткий вариант — p: Посмотрим какие входные аргументы приняла наша программа:

(gdb) print argv[0]
$1 = 0xbffff38e "/home/dima/work/fact"
(gdb) p argv[1]
$2 = 0xbffff3ba "4"

В первом примере мы использовали полное название команды, во втором — сокращенное. Многие команды отладчика имеют подобные сокращенные варианты.

Как видно, в нулевом элементе массива argv содержится путь к исполняемому файлу программы, а в первом элементе — переданный программе аргумент (4).

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

Перейдем к следующей строке:

(gdb) next
17        printf("%d! = %d\n", a, fact(a));

Мы сделали один шаг и остановились перед 17-ой строкой. Здесь мы скомандовали next, а не step, поскольку не хотели, чтобы отладчик заходил внутрь библиотечной функции atoi(). Краткие названия step и nexts и n соответственно. Мы будем использовать в примерах полные названия, а затем, в конце, приведем сводку команд и их сокращений.

Пойдем дальше:

(gdb) step
fact (num=4) at fact.c:6
6           if(num <= 1)

Мы вошли в функцию fact с входным параметром 4 и оказались перед 6-ой строкой.

Посмотрим теперь как будет изменяться значение переменной num при каждом рекурсивном входе в функцию fact(). Добавим в процесс отладки слежение за переменной num. Для этого существует команда display, которой необходимо передать название переменной (num). Выполним команду display, и перейдем на один шаг вперед:

(gdb) display num
1: num = 4
(gdb) step
11          return num * fact(num - 1);
1: num = 4

Теперь при каждой остановке программы GDB будет сообщать нам значение переменной num.

Сейчас мы находимся перед рекурсивным входом в функцию fact(). Зайдем в нее и сделаем еще один шаг вперед:

(gdb) step
fact (num=3) at fact.c:6
6           if(num <= 1)
1: num = 3
(gdb) step
11          return num * fact(num - 1);
1: num = 3

Отладчик сообщает нам значение переменной num, которое теперь равно 3.

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

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

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

Давайте поставим точку останова в строке 11, и выполним команду continue:

(gdb) break 11
Breakpoint 1 at 0x80484df: file fact.c, line 11.
(gdb) continue
Continuing.

Breakpoint 1, fact (num=2) at fact.c:11
11          return num * fact(num - 1);
1: num = 2

Мы прошли все шаги, которые до этого проделывали командой step. Теперь, каждый раз выполняя команду continue мы будем оказываться на 11-ой строке и, благодаря выполненной ранее команде display, сможем наблюдать как изменяется значение переменной num на каждом шаге.

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

Удалим точку останова:

(gdb) clear 11
Deleted breakpoint 1

и продолжим выполнение программы:

(gdb) continue
Continuing.
4! = 24

Program exited normally.

Отладчик показал вывод нашей программы, а именно подсчитанный факториал от 4. Последняя строка в выводе отладчика сообщает, что программа завершилась нормально. Если бы это было не так, то отладчик вывел бы код возврата программы в восьмеричной форме.

Очень полезной может оказаться команда set variable, которая позволяет изменять значение переменной по ходу выполнения программы. Запустим нашу программу сначала:

(gdb) start 4
Temporary breakpoint 1 at 0x8048556: file fact.c, line 16.
Starting program: /home/dima/work/fact 4

Temporary breakpoint 1, main (argc=2, argv=0xbfffef84) at fact.c:16
16        int a = atoi(argv[1]);
(gdb) step
17        printf("%d! = %d\n", a, fact(a));
(gdb) set variable a=10
(gdb) c
Continuing.
10! = 3628800

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

Команду set variable удобно применять, когда вы обнаружили, что программа ведет себя неверно, но путем изменения значения переменной можно исправить ситуацию и посмотреть как программа будет вести себя дальше.

Есть и другой способ следить за изменением переменной в ходе работы программы — с помощью команды watch. В отличие от display, команда watch будет останавливать выполнение программы при каждом изменении переменной, переданной в качестве параметра. При этом будут выводиться старое и новое значение переменной.

Обе команды display и watch могут в качестве параметров принимать не только переменные, но и выражения.

Рассмотрим еще один пример

```C linenum

include

include

void bar(int p) { p = 128; }

void foo(int *p) { bar(p); }

int main() { int p = (int ) calloc(sizeof(int *), 1);

printf("main start: p is %d\n", *p);
foo(p);
printf("main end: p is %d\n", *p);

return 0;

}

В нем резервируется память, адрес которой хранится в указателе `*p`, и нам необходимо отследить, в каком месте программы изменяется содержимое этой области памяти.

Точки останова для этого использовать неудобно  их придется ставить во всех "подозрительных" местах, где встречается `*p`, а таких в реальной программе может оказаться много. Поэтому с помощью команды `wacth` мы установим точку наблюдения за `*p`.

Запустим программу в отладчике, сделаем несколько шагов, и установим точку наблюдения за `*p`:

```bash
(gdb) start
Temporary breakpoint 1 at 0x80484f6: file /home/dima/work/sample.c, line 16.
Starting program: /home/dima/work/sample 
Temporary breakpoint 1, main () at /home/dima/work/sample
/main.c:16
16      int *p = (int *) calloc(sizeof(int *), 1);
(gdb) n
18      printf("main start: p is %d\n", *p);
(gdb) n
main start: p is 0
19      foo(p);
(gdb) watch *p
Hardware watchpoint 6: *p
(gdb) c
Continuing.
Hardware watchpoint 6: *p

Old value = 0
New value = 128
bar (p=0x804b008) at /home/dima/work/gdb_test/main.c:7
7   }

Выполнение программы было остановлено перед 7-ой строкой, внутри функции bar(), где значение *p стало равным 128.

Если нам интересно более подробно узнать, в каком именно месте остановилась выполнение программы, это можно сделать с помощью команды backtrace:

(gdb) backtrace
#0  bar (p=0x804b008) at /home/dima/work/gdb_test/main.c:7
#1  0x080484eb in foo (p=0x804b008) at /home/dima/work/gdb_test/main.c:11
#2  0x08048530 in main () at /home/dima/work/gdb_test/main.c:19

Информация, которую выдаем нам отладчик означает, что мы находимся внутри выполняющейся функции bar() (перед строкой 7), вызванной из функции foo() (в строке 11), которая, в свою очередь, вызвана из функции main() (в строке 19). Таким образом, команда backtrace показывает весь стек вызываемых функций от начала программы до текущего места.

Сводка команд

Запуск и прекращение работы

Команда Описание
r (run) запуск программы
kill убиваем текущий процесс
Ctrl+c прерывание работы
q (quit) выход из GDB

Точки останова и наблюдения

Команда Описание
b (break) установить точку останова
b <функция> ...на заголовке заданной функции
b <строка> ...в заданной строке
b <файл>:<строка> ... в заданной строке файла
b <файл>:<функция> ... на заголовке заданной функции из файла
b <адрес функции> возможность установки точки останова на адрес функции очень полезна, когда надо попасть в одну из множества копий одной функции (например, статическая inline-функция, объявленная в заголовочном файле)
b <выражение> if <условие> поставить условную точку останова, аналогично установке cond для нее
tb (tbreak) поставить временную точку останова которая сработает один раз, а затем будет удалена
cond <N> <условие> установить условие срабатывания точки останова номер N
[ watch | rwatch | awatch ] <выражение> установить точку наблюдения, которая сработает, если значение по адресу выражение [ изменяется | читается | читается/изменяется ]. Например: (gdb) watch ((int)0x0B0B0B) означает следить за значением по адресу 0x0B0B0B
info brakepoints показать информацию обо всех точках останова и наблюдения
disable <N> выключить точку останова/наблюдения под номером N, но не удалять ее
d (delete) [ <список номеров> ] удалить точки останова/наблюдения с номерами из списка
clear [ <функция> | <файл:строка> ] удалить точки останова привязанные к функции/файлу

Управление выполнением

Команда Описание
n | next следующий шаг выполнения (следующая строчка кода)
s | step углубляемся в стек выполнения (заходим внутрь вызываемых функций)
c | continue продолжаем работу программы
u | until <место> продолжаем работу программы до заданного места. Указания места такие же, как для break

Трассировка

Команда Описание
bt | backtrace показать стек вызовов текущей точки
up подняться на одну функцию вверх в текущем стеке вызовов
down вглубь стека вызовов на одну функцию

Вывод: строки, переменные, выражения, функции

Команда Описание
l (list) показать исходник вокруг текущей строчки (по умолчанию - 10 строк, повторный вызов l показывает следующие 10 строк)
l 247 показать исходник вокруг строки 247
l <функция> показать исходник функции <функция>
p [ <переменная> | <выражение> ] показать значение переменной, памяти, выражения
set <переменная> = <значение> установить <значение> для <переменной>
info [ locals | args ] > показать значения [ локальных переменных | аргументов функции ]
call <функция> вызвать функцию <функция>

миниFAQ

Как в gdb запустить программу с аргументами arg1, arg2,...?

  1. Указать аргументы программы (myprogram) в командной строке при запуске gdb:
gdb --args myprogram arg1 arg2
  1. В запущенном gdb указать аргументы при старте программы:
gdb myprogram
(gdb) r arg1 arg2

Как начать отладку с первой строки программы?

(gdb) b main
(gdb) r

Как перейти к просмотру нужного метода класса?

(gdb) b filename:ClassName::methodName
(gdb) r # или c, если программа уже запущена

Как остановить выполнение программы в заданной строке при выполнении заданного условия?

Допустим, в следующей программе

#include <stdio.h>

int main()
{
    for (char i = 0; ++i; )
        printf("%i,", i);
    return 0;
}

нужно остановить выполнение в строке 6, когда i станет равно 127.

Устанавливаем условную точку останова

(gdb) b 6 if i == 127

и выполняем программу до этой точки.



Комментарии

comments powered by Disqus