Наконец-то я нашел время изучить GDB. И это потрясающе! Вот что я узнал.

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

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

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

Для пользователей Mac

Это полная P.I.T.A. чтобы GDB работал на Mac. Вот уроки, которым я должен был следовать, чтобы заставить его работать:

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

[New Thread 0x1c03 of process 34571]

и терминал просто зависает, я должен нажать ctrl+z, убить процесс с помощью kill -9, а затем запустить

sudo DevToolsSecurity -enable && sudo DevToolsSecurity -disable

Пару раз. После этого он обычно работает.

Вставай и беги

Чтобы отладить код C/C++ с помощью GDB, скомпилируйте его с помощью инструкций по отладке:

gcc -g source.c -o executable

Начнем с самой простой программы:

Это приглашение GDB. Здесь отправляются команды для проверки вашего кода путем взаимодействия с GDB.

Самая основная команда — это команда run. Он просто запустит код. Как и при запуске с ./executable:

(gdb) run
Starting program: .../debuggable-executable
Hello World!

Вот! Теперь займемся чем-нибудь более полезным.

Установка точек останова

Используя эту другую простую программу в качестве примера:

Команда list печатает наш код с прикрепленными номерами строк:

Это полезно, потому что мы можем использовать номера строк для установки точек останова. Чтобы установить точку останова для строки, используется команда break:

(gdb) break 4
Breakpoint 1 at 0x100003f5f: file source.c, line 4.

Это установило точку останова в строке 4. Использование номеров строк не необходимо, поскольку есть другие способы установки точек останова. Наиболее полезным, пожалуй, будет установка точек останова при входе в функции, например. break main.

Теперь запустив код, он должен остановиться в точке останова в #4.

(gdb) run
Thread 2 hit Breakpoint 1, main () at source.c:4
4       int i = 0;

Оно делает! Хороший.

Теперь давайте напечатаем переменные:

(gdb) print i
$1 = 69669
(gdb) print j
$2 = 32766

Поскольку ни один из них еще не инициализирован, мы просто видим мусор. Продолжим выполнение программы, перейдя на следующую строку:

(gdb) next
5       int j = 1;

«5» в начале вывода сигнализирует, в какой строке кода находится отладчик. Если мы теперь напечатаем переменную i, то получим ожидаемое значение:

(gdb) print i
$3 = 0

Теперь, переходя к следующим нескольким строкам, объявляется переменная j и изменяется i:

(gdb) next
6       i = 13;
(gdb) print j
$4 = 1
(gdb) next
8       printf("Hello World!\n");
(gdb) print i
$5 = 13

Команду info можно использовать для печати значений всех переменных.

(gdb) info locals
i = 13
j = 1

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

(gdb) continue
Continuing.
Hello World!

Функции отладки

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

Возьмем, к примеру, следующий код:

(gdb) list
1   #include "stdio.h"
2
3   int add_and_increment(int a, int b) {
4       int sum = a + b;
5       int incremented = sum + 1;
6       return incremented;
7   }
8
9   int main() {
10      int n = add_and_increment(2, 3);

Чтобы перечислить весь код, если в коде ›10 строк, нужно несколько раз использовать команду list:

(gdb) list
11      n = add_and_increment(n, n);
12
13      printf("result = %i!\n", n);
14      return 0;
15  }

Установите точку останова для первого вызова функции

(gdb) break 10
Breakpoint 1 at 0x100003f4f: file source.c, line 10.
(gdb) run
Thread 2 hit Breakpoint 1, main () at source.c:10
10      int n = add_and_increment(2, 3);

Теперь вместо запуска команды next используйте команду step, чтобы перейти к выполнению функции:

(gdb) step
add_and_increment (a=2, b=3) at source.c:4
4       int sum = a + b;

Внутри функции другой вариант команды info печатает аргументы, переданные этой функции:

(gdb) info args
a = 2
b = 3

Еще одним вариантом info является stack, чтобы увидеть текущий стек вызовов:

(gdb) info stack
#0  add_and_increment (a=2, b=3) at source.c:4
#1  ... in main () at source.c:11

... был адресом функции, удаленным для удобочитаемости. Выглядит это так: 0x0000000100003f6c

Переходим к функции:

(gdb) next
5       int incremented = sum + 1;
(gdb) next
6       return incremented;
(gdb) info locals
sum = 5
incremented = 6

Чтобы выйти из функции, используйте команду finish:

(gdb) finish
Run till exit from #0  add_and_increment (a=2, b=3) at source.c:6
   ... in main () at source.c:10
   10      int n = add_and_increment(2, 3);
Value returned is $1 = 6

Обратите внимание, что команда Finish также печатает возвращаемое значение функции!

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

(gdb) frame
#0  ... in main () at source.c:10
10      int n = add_and_increment(2, 3);

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

(gdb) list 10
5       int incremented = sum + 1;
6       return incremented;
7   }
8
9   int main() {
10      int n = add_and_increment(2, 3);
11      n = add_and_increment(n, n);
12
13      printf("result = %i!\n", n);
14      return 0;

Если вам нужна дополнительная информация о какой-либо команде, вы можете использовать команду help:

(gdb) help list
list, l
List specified function or line.
...

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

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

Возьмем, к примеру, следующий код:

Сначала установим точку останова в функции:

(gdb) break max_number_in_array
Breakpoint 1 at 0x100003e6c: file source.c, line 4.
(gdb) run
Thread 2 hit Breakpoint 1, max_number_in_array (array=0x7ffeefbff330, array_size=6) at source.c:4
4       int max = array[0];

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

(gdb) watch max
Hardware watchpoint 2: max

Поскольку мы установили точку наблюдения перед инициализацией переменной, первая остановка будет после инициализации переменной.

(gdb) continue
Continuing.
Thread 2 hit Hardware watchpoint 2: max
Old value = 0
New value = 5
max_number_in_array (array=0x7ffeefbff330, array_size=6) at source.c:5
5       for (size_t i = 1; i < array_size; i++) {

Хороший. Затем он остановится, когда значение изменится с 5→12 на 12→235:

(gdb) continue
Continuing.
Thread 2 hit Hardware watchpoint 2: max
Old value = 5
New value = 12
max_number_in_array (array=0x7ffeefbff330, array_size=6) at source.c:10
10      }
(gdb) continue
Continuing.
Thread 2 hit Hardware watchpoint 2: max
Old value = 12
New value = 235
max_number_in_array (array=0x7ffeefbff330, array_size=6) at source.c:10
10      }

Когда мы снова нажмем «Продолжить», функция выйдет из функции, удалив точку наблюдения.

(gdb) continue
Continuing.
Watchpoint 2 deleted because the program has left the block in
which its expression is valid.
... in main () at source.c:16
16      int max = max_number_in_array(array, 6);

Продолжая снова, программа завершается.

(gdb) continue
Continuing.
result = 235!

Условная точка останова

Мы также можем установить условные разрывы: точки останова, которые активируются только в том случае, если условие истинно. Продолжая использовать предыдущий пример кода, мы рассмотрим условные точки останова.

Во-первых, давайте установим обычную точку останова в первой строке цикла for:

(gdb) break 6
Breakpoint 6 at 0x100003e8b: file source.c, line 6.
(gdb) run
Thread 2 hit Breakpoint 6, max_number_in_array (array=0x7ffeefbff330, array_size=6) at source.c:6
6           int current = array[i];

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

(gdb) break 6 if i == 5
Note: breakpoint 6 also set at pc 0x100003e8b.
Breakpoint 7 at 0x100003e8b: file source.c, line 6.

Обратите внимание, что теперь у нас есть две точки останова в строке 6. Давайте удалим первую (безусловную). С помощью info break мы можем видеть информацию о каждой установленной точке останова.

(gdb) info break
Num     Type           Disp Enb Address            What
6       breakpoint     keep y   0x0000000100003e8b
    in max_number_in_array at source.c:6
    breakpoint already hit 1 time
7       breakpoint     keep y   0x0000000100003e8b
    in max_number_in_array at source.c:6
    stop only if i == 5

Для удаления нам достаточно использовать команду del, передав идентификатор точки останова:

(gdb) del 6
(gdb)

Молчание = успех. Если бы мы хотели удалить несколько точек останова, мы могли бы сделать del start-end, например. del 1-5 удалит точки останова с 1 по 5.

Давайте снова проверим info break:

(gdb) info break
Num     Type           Disp Enb Address            What
7       breakpoint     keep y   0x0000000100003e8b
    in max_number_in_array at source.c:6
    stop only if i == 5

Со всем этим мы можем нажать «Продолжить», и он остановится, когда условие будет истинным (когда i == 5):

(gdb) continue
Continuing.
Thread 2 hit Breakpoint 7, max_number_in_array (array=0x7ffeefbff330, array_size=6) at source.c:6
6           int current = array[i];
(gdb) print i
$1 = 5

Идеальный. Если мы снова нажмем «Продолжить», программа завершится:

(gdb) continue
Continuing.
result = 235!

Отладка реального проекта

Вместо однофайловых игрушечных примеров в этом разделе в качестве примера будет использоваться этот многофайловый проект: Этюды на C — URL-адреса внешней сортировки

TLDR: в проекте реализован внешний алгоритм сортировки. Репозиторий — это личная игровая площадка для изучения идей в C. Основная исследуемая идея — это общие структуры данных: структуры данных, которые используют общие указатели, так что им все равно, какие данные они содержат. несущий. Не стесняйтесь осматривать репозиторий!

Прежде всего, давайте скомпилируем с инструкциями по отладке. Для этого нужно изменить make-файл, добавив флаг -g:

   gcc cases/external-sort-urls/main.c lib/*.c -o exec -g
   # ./exec # let's also comment this line for now

Теперь запуск make external-sort-urls скомпилирует проект с инструкциями по отладке. Выполнение git status дает следующий результат:

  • Makefile - это Makefile, который был отредактирован
  • exec — исполняемый скомпилированный двоичный файл
  • exec.dSYM это папка, содержащая «символы отладки», которую будет использовать GDB. При компиляции кода без флага -g эта папка не создается.

Чтобы установить точку останова в файле, мы можем использовать команду break с немного другим синтаксисом. Во-первых, все файлы могут быть перечислены с info:

(gdb) info sources
.../lib/merge_sort.c,
.../lib/heaps.c,
.../lib/generic_arrays.c,
.../lib/printers.h,
.../lib/external_sorting.c,
.../cases/external-sort-urls/main.c

... - это полный путь к папке с проектом

Установка точки останова в начале функции external_sorting в файле external_sorting.c:

(gdb) break external_sorting.c:153
Breakpoint 1 at 0x100002acc: file lib/external_sorting.c, line 154.
(gdb) run
Starting program: .../exec
Thread 2 hit Breakpoint 1, external_sorting 
...

Используя уже изученный info args, можно увидеть переданные функции параметры:

(gdb) info args
   input_filename = 0x100003ee4 "./cases/external-sort-urls/input.txt"
   output_filename = 0x100003f09 "./cases/external-sort-urls/output.txt"
   max_lines_per_tape = 10
   comparator = 0x100002150 <compare_entities>
   tape_filename_format = 0x100003f2f "./cases/external-sort-urls/tapes/tape-%zu.txt"

Хороший.

Установка точки останова в функции:

(gdb) break compare_entities
Breakpoint 2 at 0x100002160: file cases/external-sort-urls/main.c, line 28.
(gdb) continue
Continuing.
Thread 2 hit Breakpoint 2, compare_entities (a=0x100404080, b=0x100404180) at cases/external-sort-urls/main.c:28
28      entity entity_a = make_entity(a);

Также можно вызывать функции/оценивать выражения с помощью команды print:

(gdb) print 1 + 2
$4 = 3
(gdb) print make_entity(a)
$3 = {url = 0x100604080 "http://bbmqb.darz.com/yhiddqsc/rjmow/xsjy/dbef.html", amount = 108}
(gdb) print (char*) a
$6 = 0x100404080 "http://bbmqb.darz.com/yhiddqsc/rjmow/xsjy/dbef.html 108\n"

При вызове функций void можно использовать команду call, чтобы не загромождать вывод:

(gdb) print set_global_comparator(compare_entities)
$8 = void
(gdb) call set_global_comparator(compare_entities)
(gdb)

Примечание call не печатает $8 = void

Теперь предположим, что в функции compare_entities произошла странная ошибка, когда entity_a.amount < entity_b.amount

Чтобы отследить эту ошибку, можно установить условную точку останова для этого конкретного случая:

(gdb) break 30 if entity_a.amount < entity_b.amount
Breakpoint 3 at 0x100002180: file cases/external-sort-urls/main.c, line 31.

Для здравого смысла давайте удалим другие созданные точки останова:

(gdb) info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000100002acc 
  in external_sorting at lib/external_sorting.c:154
      breakpoint already hit 1 time
2       breakpoint     keep y   0x0000000100002160
  in compare_entities at cases/external-sort-urls/main.c:28
      breakpoint already hit 1 time
3       breakpoint     keep y   0x0000000100002180
  in compare_entities at cases/external-sort-urls/main.c:31
      stop only if entity_a.amount < entity_b.amount
(gdb) del 1-2

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

(gdb) continue
Continuing.
Thread 2 hit Breakpoint 3, compare_entities (a=0x100404080, b=0x100404180) at cases/external-sort-urls/main.c:31
31      if (entity_a.amount == entity_b.amount) {

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

(gdb) print entity_a
$9 = {url = 0x100304140 "http://bbmqb.darz.com/yhiddqsc/rjmow/xsjy/dbef.html", amount = 108}
(gdb) print entity_b
$10 = {url = 0x100304180 "http://nec.ggx.com/orelln/apqfwkhop/coqhnwn.html", amount = 121}
(gdb) finish
Run till exit from #0  compare_entities (a=0x100404080, b=0x100404180) at cases/external-sort-urls/main.c:31
0x0000000100003be6 in merge (array1=..., array2=..., comparator=0x100002150 <compare_entities>) at lib/merge_sort.c:40
40          if (comparator(array1_head, array2_head)) {
Value returned is $12 = false

Где сейчас находится отладчик:

(gdb) frame
#0  ... in merge (array1=..., array2=..., comparator=... <compare_entities>)
    at lib/merge_sort.c:40
40          if (comparator(array1_head, array2_head)) {

Текущий стек вызовов:

(gdb) info stack
#0  ... in merge (...)
      at lib/merge_sort.c:40
#1  ... in merge_sort (...)
      at lib/merge_sort.c:73
#2  ... in merge_sort (...)
      at lib/merge_sort.c:70
#3  ... in merge_sort (...)
      at lib/merge_sort.c:70
#4  ... in flush_lines_to_tape (...)
      at lib/external_sorting.c:29
#5  ... in split_input_file_into_sorted_tapes (...)
      at lib/external_sorting.c:53
#6  ... in external_sorting (...)
      at lib/external_sorting.c:154
#7  0x0000000100002205 in main () at cases/external-sort-urls/main.c:38

Я скрыл некоторую информацию, которая искажает вывод и не будет понятна в этой статье. Но каждая информация о стеке содержит адрес функции и переданные параметры (точно так же, как main в строке № 7).

Кроме того, вы заметили, что в стеке вызовов есть несколько merge_sort? Это результат рекурсивных вызовов :)

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

(gdb) delete 3
(gdb) continue
Continuing.
[Inferior 1 (process 53562) exited normally]

Ссылка

  • Официальные документы GDB.
    Он отлично подходит для изучения синтаксиса команд, но мне он не показался удобным для новичков, не знакомых с основами GDB. Надеюсь, после этой статьи вы уже не новичок в GDB :).