Отладчиком (или дебаггером) называется программа, предназначенная для поиска ошибок в других программах в процессе их выполнения. Процесс поиска и устранения ошибок называется отладкой Существует множество различных отладчиков. Одним из наиболее распространенных отладчиков для системы Linux является GDB. На данной странице описывается, как нужно запускать этот отладчик и какие команды нужно выполнять для решения некоторых типовых задач отладки.
Для использование отладчика программу (каждый исходный файл, если Ваша программа разделена на несколько файлов) необходимо откомпилировать с включением отладочной информации. Это делается добавлением ключа компиляции -g, например:
gcc -g file.cЗапуск программы в отладчике осуществляется командой вида
gdb -q имя_выполняемого_файлас последующим выполнение команды отладчика run.
gdb -q file.c
$ 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 Введите последовательность чисел и нажмитеЗаметим, что команды gcc и gdb являются командами операционной системы (они вводятся после подсказки $), а команды run и quit суть команды отладчика. Отладчик GDB — это программа со своей собственной системой команд. Командная строка отладчика (место, куда нужно вводить команды, которые описываются дале на этой странице), начинается с "(gdb) ".0 - символ конца последовательности 0 Не удалось прочитать первый элемент [Inferior 1 (process 12043) exited with code 0377] (gdb) quit $
Если программа заканчивается ошибкой сегментировангия (segmentation fault) или ошибкой выполнения арифметической операции, то точное место возникновения ошибки можно выяснить с помощью отладчика gdb. Это настолько частая ситуация, что она описана на отдельной странице. Для полноты описания продублируем осовные команды здесь.
Типичная последовательность команд такова:
Если программа заканчивает работать, но выдает некорректный результат, то использование команд из предыдущего раздела не даст никакой дополнительной информации. Программа просто доработает до конца после выполнения команды run. Для того, чтобы понять, что именно не работает в программе, нужно иметь возможность получать значения переменных в процессе работы программы. Это можно сделать либо включив в текст программы отладочную печать, либо использовать возможности отладчика, о которых рассказывается ниже.
Чтобы прервать работу программы в определенном месте, перед ее запуском в отладчике командой run следует создать точку останова (break point). В отладчике GDB можно создавать точки останова с различными условиями. Самые распространенные виды точек останова включают:
Далее приводятся примеры команд создания точки останова (в порядке убывания их частотности). Можно определять более одной точки останова для одной программы.
Рассмотрим в качестве примера следующую программу, вычисляющую функцию Аккермана 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=Эти данные нужно читать снизу-вверх. В последней строчке (#6) написано, что выполнение началось с вызова функции, 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)
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.