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

Все это из области фантастики. Каждый из нас допускает ошибки, поэтому нам нужно их как-то отлавливать.

Если ошибка синтаксическая, то в большинстве случаев на это укажет компилятор. Но, если ошибка логическая, то часто программист сам не в состоянии определить ее.

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

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

В этой статье рассказывается об отладке программ, написанных на языке C, в командной строке Linux с использованием утилиты GDB.

Многие программисты, которые с успехом разрабатывали свои приложения на языках C/C++ для Windows, после перехода к разработке программ на том же языке, но под ОС семейства Linux с удивлением обнаруживают, что отсутствует возможность той отладки, которой они пользовались в редакторах при написании программ для Windows.

Однако, такая возможность есть и это отладчик GDB.

Как следует из документации к этой утилите «цель отладчика GDB - это позволить вам увидеть, что происходит внутри программы пока она выполняется, или что программа делает в момент ее краха.».

На момент написания этой статьи отладчик GDB работает с программами, написанными на языках C, C++, Modula-2. Мы будем отлаживать приложение, написанное на языке C.

Давайте начнем с приведения кода программы, которая в качестве аргумента принимает число и высчитывает факториал этого числа при помощи рекурсии. Ее мы и будем отлаживать.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <unistd.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;
}

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

g++ -g fact.c -o fact

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

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

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

gdb -q fact

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

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

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

(gdb) list 10
5       int fact(int num)
6       {
7           if(num <= 1)
8           {
9             return 1;
10          }
11
12          return num * fact(num - 1);
13      }

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

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

Давайте запустим теперь выполнение программы под отладчиком. Это можно сделать одной из двух команд: run или start.

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

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

Итак, запустим программу на выполнение в режиме отладки:

(gdb) start 4
Temporary breakpoint 2 at 0x8048556: file fact.c, line 17.
Temporary breakpoint 2, main (argc=2, argv=0xbffffa24) at fact.c:17
17        int a = atoi(argv[1]);

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

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

(gdb) print argv[0]
$1 = 0xbffffb41 "/home/user/cpp/fact"
(gdb) print argv[1]
$2 = 0xbffffb5f "4"

В нулевом элементе массива argv содержится путь до выполняемой программы, а в первом элементе наш переданный аргумент — 4.

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

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

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

(gdb) step
18        printf("%d! = %d\n", a, fact(a));

Мы сделали один шаг и остановились перед 18-ой строкой.

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

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

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

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

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

Выполним команду display и перейдем на один шаг вперед:

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

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

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

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

Отладчик сообщает нам значение переменной num, которое уже равно 3. Сейчас мы снова находимся перед заходом в функцию fact.

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

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

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

Расставив точки останова, отладчик может «перепрыгивать» к строкам, на которых они поставлены, минуя весь остальной код. Для этого можно использовать команду continue, которая продолжает выполнение программы до тех пор, пока не встретится точка останова. Если их нет, то программа выполнится до конца.

Итак, давайте поставим точку останова в строке 12 и выполним команду continue, чтобы продолжить программу до тех пор, пока не наткнемся на строку 12:

(gdb) break 12
Breakpoint 2 at 0x8048529: file fact.c, line 12.
(gdb) continue
Continuing.
 
Breakpoint 2, fact (num=2) at fact.c:12
12          return num * fact(num - 1);
1: num = 2

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

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

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

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

(gdb) continue
Continuing.
4! = 24
 
Program exited normally.

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

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

user@linux-hn2z:~/cpp/> gdb -q fact
(gdb) start 4
Temporary breakpoint 1 at 0x8048556: file fact.c, line 17.
Starting program: /home/user/cpp/fact 4
 
Temporary breakpoint 1, main (argc=2, argv=0xbfffef84) at fact.c:17
17        int a = atoi(argv[1]);
(gdb) step
18        printf("%d! = %d\n", a, fact(a));
(gdb) set variable a=10
(gdb) c
Continuing.
10! = 3628800

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

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

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

На этом все. Удачной отладки.