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

Надеюсь, что данная страница покажет, что в использовании отладчика нет ничего сложного, но отладочная печать может показаться Вам гораздо более простым и понятным методом поиска ошибок.

Компиляция и запуск программы в отладчике

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

gcc -g file.c
Запуск программы в отладчике осуществляется командой вида
gdb -q имя_выполняемого_файла
с последующим выполнение команды отладчика run.
Внимание! В отладчике запускается исполняемый файл, а не исходный файл программы. Не следует выполнять команду
gdb -q file.c
Для завершения работы отладчика нужно выполнить команду quit. Ниже приведен пример команд компиляции программы (gcc), запуска отладчика для работы с этой программой (gdb), запуска программы (run) и выхода из отладчика (quit). Между командами run и quit мы обычным образом вводим данные, необходимые нашей программе (в данном примере мы сразу ввели 0).
$ gcc -g first-prog.c
$ gdb -q ./a.out
Reading symbols from ./a.out...done.
(gdb) run
Starting program: /home/serg/tmp/gdb/a.out
Введите последовательность чисел и нажмите 
0 - символ конца последовательности
0
Не удалось прочитать первый элемент
[Inferior 1 (process 12043) exited with code 0377]
(gdb) quit
$
Заметим, что команды gcc и gdb являются командами операционной системы (они вводятся после подсказки $), а команды run и quit суть команды отладчика. Отладчик GDB — это программа со своей собственной системой команд. Командная строка отладчика (место, куда нужно вводить команды, которые описываются дале на этой странице), начинается с "(gdb) ".

Выявление причин ошибок сегментирования

Если программа заканчивается ошибкой сегментировангия (segmentation fault) или ошибкой выполнения арифметической операции, то точное место возникновения ошибки можно выяснить с помощью отладчика gdb. Это настолько частая ситуация, что она описана на отдельной странице. Для полноты описания продублируем осовные команды здесь.

Типичная последовательность команд такова:

  1. gdb -q имя выполняемого файла
  2. run
  3. where
  4. print переменная или выражение
  5. quit
После выполнения команды where будет распечатана последовательность вызовов функций, которая привела к возникновению ошибки. С помощью команды print можно распечатать значение переменной, обявленной в функции (обычно это помогает понять причину возникновения ошибки).

Точки останова, пошаговое выполнение и печать переменных

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

Чтобы прервать работу программы в определенном месте, перед ее запуском в отладчике командой run следует создать точку останова (break point). В отладчике GDB можно создавать точки останова с различными условиями. Самые распространенные виды точек останова включают:

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

Далее приводятся примеры команд создания точки останова (в порядке убывания их частотности). Можно определять более одной точки останова для одной программы.

Для просмотра списка всех определенных точек останова используется команда info break. Удалить ненужную точку останова можно командой delte breakpoints n, где n есть номер точки останова, который печатается командой info break.

Рассмотрим в качестве примера следующую программу, вычисляющую функцию Аккермана A(m, n).

#include <stdio.h>
unsigned int A(unsigned int m, unsigned int n) {
    if(m == 0)
        return n+1;
    else if(m > 0 && n == 0)
        return A(m-1, 1);
    return A(m-1, A(m, n-1));
}

int main() {
    unsigned int m=4, n=2;
    printf("A(%u, %u) == %u\n", m, n, A(m, n));
} 

Выполним команды:

В результате получим следующее.
$ gcc -g ackermann.c
$ gdb -q a.out
Reading symbols from a.out...done.
(gdb) break main
Breakpoint 1 at 0x400594: file ackermann.c, line 11.
(gdb) break A if n == 0
Breakpoint 2 at 0x40053b: file ackermann.c, line 3.
(gdb) break ackermann.c:7 if m == 3
Breakpoint 3 at 0x400569: file ackermann.c, line 7.
(gdb) info breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000400594 in main at ackermann.c:11
2       breakpoint     keep y   0x000000000040053b in A at ackermann.c:3
        stop only if n == 0
3       breakpoint     keep y   0x0000000000400569 in A at ackermann.c:7
        stop only if m == 3
(gdb)
  

Теперь запустим нашу программу командой отладчика run. Естественно, у нас сработает точка останова, определенная для функции main, так как main — это первая функция, которая вызывается в программе.

(gdb) run
Starting program: /home/serg/tmp/gdb/a.out

Breakpoint 1, main () at ackermann.c:11
11          unsigned int m=4, n=2;
(gdb)
    
Отображается строчка кода, которая будет выполнена следующей (еще не была выполнена).

Если программа остановилась при срабатывании точки останова, то у нас есть возможность вывести на экран значения переменных, выполнить один оператор, или продолжить выполнение до достижения следующей точки останова. Распечатаем значение переменной n (команда print, выполним одну строчку кода (next), снова распечатаем значение переменной (print) и продолжим нормальное выполнение программы (continue).

(gdb) print n
$3 = 0
(gdb) next
12          printf("A(%u, %u) == %u\n", m, n, A(m, n));
(gdb) print n
$4 = 2
(gdb) continue

После выполнения команды continue выполнение программы продолжается в обычном режиме, до срабатывания очередной точки останова.

(gdb) continue
Continuing.

Breakpoint 2, A (m=4, n=0) at ackermann.c:3
3           if(m == 0)
(gdb)

В данном случае сработала точка останова номер 2. Отладчик сообщает, что была вызвана функция A со значениями m=4 и n = 0. Для посмотра набольшого фрагмента кода около текущей позиции можно использовать команду list, которая выводит 5 предыдущих и 5 последующих строк кода.

(gdb) continue
Continuing.

Breakpoint 2, A (m=4, n=0) at ackermann.c:3
3           if(m == 0)
(gdb) list
1       #include <stdio.h>
2       unsigned int A(unsigned int m, unsigned int n) {
3           if(m == 0)
4               return n+1;
5           else if(m > 0 && n == 0)
6               return A(m-1, 1);
7           return A(m-1, A(m, n-1));
8       }
9
10      int main() {
(gdb)

Если сейчас выполнить команду where, которая печатает стек вызовов (последовательность вызовов функций, начиная от main), то мы увидем следующее:

(gdb) where
#0  A (m=4, n=0) at ackermann.c:3
#1  0x000000000040057b in A (m=4, n=1) at ackermann.c:7
#2  0x000000000040057b in A (m=4, n=2) at ackermann.c:7
#3  0x00000000004005b1 in main () at ackermann.c:12
(gdb)
  
Это означает, что в функции main (в 12-ой строчке файла ackermann.c) была вызвана функция A с аргументами (m=4, n=2), которая в строчке 7 вызвала функцию A (m=4, n=1), которая, в свою очередь, вызвала A (m=4, n=0). Если еще несколько раз выполнить команду continue, а потом опять where, то будет видно как увеличивается глубина рекурсии.

Команды отладчика, которые нужно запомнить.

Определения места, где программа "зациклилась"

Если Ваша программа выполняется непредвиденно долго, то, возможно, она содержит бесконечный цикл (цикл, условие которого всегда выполняется). Отладчик позволяет узнать, какой оператор программы выполняется в данный момент. Приведенная выше программа вычисления функции Аккермана не содержит бесконечного цикла, но работает "бесконечно" долго. Если мы запустим эту программу в отладчике и через некоторое время после старта нажмем CTRL+C (клавиша C при нажатой клавише Ctrl), то можем получить сообщение следующего вида.

$ gcc -g ackermann.c
$ gdb -q ./a.out
Reading symbols from ./a.out...done.
(gdb) run
Starting program: /home/serg/tmp/gdb/a.out
^C
Program received signal SIGINT, Interrupt.
0x000000000040057b in A (m=1, n=20413) at ackermann.c:7
7           return A(m-1, A(m, n-1));
(gdb)

В момент нажатия CTRL+C программа выполняла оператор в строке 7. При этом вычислялась функция A с аргументами m=1 и n=20413. Можно продолжить обычное выполнение программы командой continue и нажать CTRL+C снова через некоторое время. На этот раз сообщение может быть таким.

(gdb) continue
Continuing.
^C
Program received signal SIGINT, Interrupt.
0x000000000040057e in A (m=1, n=12825) at ackermann.c:7
7           return A(m-1, A(m, n-1));
(gdb)

Если Ваша программа ожидает ввода данных с клавиатуры, то внешне это может выглядеть как "зацикливание". Запустим некоторую прогамму (ее исходный код нас не интересует) в отладчике и нажмем Ctrl+C. Если выполнение программы было прервано в системной функции, а наша программа в действительности вызывает scanf, то мы увидим не совсем понятное сообщение.

$ gdb -q ./a.out
Reading symbols from ./a.out...done.
(gdb) run
Starting program: /home/serg/tmp/gdb/a.out
^C
Program received signal SIGINT, Interrupt.
0x00007ffff7b00810 in __read_nocancel ()
    at ../sysdeps/unix/syscall-template.S:81
81      ../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb)
Чтобы посмотреть историю вызовов функций, выполним команду where.
(gdb) where
#0  0x00007ffff7b00810 in __read_nocancel ()
    at ../sysdeps/unix/syscall-template.S:81
#1  0x00007ffff7a8f6a0 in _IO_new_file_underflow (
    fp=0x7ffff7dd4640 <_IO_2_1_stdin_>) at fileops.c:613
#2  0x00007ffff7a9062e in __GI__IO_default_uflow (
    fp=0x7ffff7dd4640 <_IO_2_1_stdin_>) at genops.c:435
#3  0x00007ffff7a6d892 in _IO_vfscanf_internal (s=,
    format=, argptr=argptr@entry=0x7fffffffe2e8,
    errp=errp@entry=0x0) at vfscanf.c:620
#4  0x00007ffff7a72ed9 in __isoc99_scanf (format=)
    at isoc99_scanf.c:37
#5  0x0000000000400582 in read_array (array=0x7fffffffe404, n=21)
    at gdb_scanf.c:5
#6  0x00000000004005ac in main () at gdb_scanf.c:10
(gdb)
Эти данные нужно читать снизу-вверх. В последней строчке (#6) написано, что выполнение началось с вызова функции main, которая в десятой строке файла gdb_scanf.c (это имя исходного файла программы, которую мы запускаем) вызвала функцию read_array. В пятой строке файла gdb_scanf.c была вызвана функция __isoc99_scanf. Все, что выше — это реализация системной функции scanf и нас не должно интересовать. Мы выяснили, что в момент прерывания программы нажатием Ctrl+C она выполняла scanf в пятой строке исходного файла, а там действительно был оператор scanf("%d", &k);.

Поиск непредвиденного изменения значения переменной

Рассмотрим следующую программу. В ней инициализируется две целочисленных переменных знаением 0, а массив целых чисел array заполняется значением 1 в функции init_by_value.

#include <stdio.h>
#define N 3
void init_by_value(int *a, int n, int value) {
    while(n >= 0) {
        a[n-1] = value;
        n--;
    }
}

int main() {
    int a = 0;
    int array[N]; /* Статический массив из N элементов */
    int b = 0;

    init_by_value(array, N, 1); /* Инициализируем массив */

    printf("a=%d, b=%d\n", a, b);
    return 0;
} 
Если мы выполним эту программу, то в результате можем увидеть что-то такого вида
a=0, b=1
Эти данные получены при использовании компилятора gcc версии 4.8.4 на Ubuntu 4.8.4-2ubuntu1~14.04. При использовании другого компилятора результат может быть иным. Заметим, что значение переменной b изменилось, хотя в программе нет оператора, который присваивает отличное он нуля значение переменной b. Каким же образом могло измениться значение этой переменной?

Отладчик GDB позволяет поставить точку останова не только при вызове какой-либо функции, но и при изменении значения указанного адреса памяти (значения переменной). То есть имеется возможность прервать выполнение программы, когда производится запись нового значения перемменной. Это делается следующими командами.

gdb -q a.out
break main
run
watch b
continue
Сначала мы делаем обычную точку останова при вызове функции main и запускаем программу. Когда вызывается main перемменная b становится определенной в текущем контексте и мы объявляем точку останова при изменении этого значения (watch b). Если бы команда watch b была бы выполнена сразу после запуска отладчика, то мы получили бы сообщение об ошибке: No symbol "b" in current context.

После объявления новой точки останова мы продолжаем выполнение программы, прерванное на функции main (команда continue). Выполнение программы будет прервано, как только значение переменной b изменится. Результат выполнения этих команд приведен ниже.

$ gdb -q a.out
Reading symbols from a.out...done.
(gdb) break main
Breakpoint 1 at 0x400568: file changes.c, line 11.
(gdb) run
Starting program: /home/serg/tmp/gdb/a.out

Breakpoint 1, main () at changes.c:11
11          int a = 0;
(gdb) watch b
Hardware watchpoint 2: b
(gdb) continue
Continuing.
Hardware watchpoint 2: b

Old value = 0
New value = 1
init (a=0x7fffffffe400, n=0) at changes.c:6
6               n--;
Последние строчки показывают, что произошла остановка программы по условию изменения переменной b (Hardware watchpoint 2: b), и что значение изменилось с 0 на 1. Программа останавливается после выполнения оператора, изменившего значение переменной. Если мы распечатаем значение переменной n
(gdb) print n
$2 = 0
(gdb)
то узнаем, что значение переменной b изменилось в при n=0. Если мы посмотрим на предыдущий оператор a[n-1] = value;, выполнение которого и привело к изменению значения b, то заметим, что он изменяет "элемент" массива с индексом -1, так как n-1 принимает значение -1. Очевидно, что такого элемента не существует. В действительности происходит изменение значения, которое не принадлежит области памяти, выделенной для массива, а используется для хранения другой переменной. В нашем случае, это оказалась переменная b.

Дополнительные возможности

Если приведенные выше примеры использования отладчика кажутся Вам слишком простыми и не соответствуют стоящим перед Вами задачам (в том числе, и по поиску ошибок), то Вы, вероятно, уже можете самостоятельно изучить документацию по GDB.