Материал подготовлен на основе выступления с CppCon 2015 "Greg Law: Give me 15 minutes & I'll change your view of GDB" (доступно по ссылке ). Многие моменты я изменял и корректировал, поэтому стоит учесть, что перевод достаточно вольный.

И да, вынесем за скобки вопрос о том, насколько GDB в целом удобная или неудобная программа, и что в принципе лучше использовать для дебаггинга: в данной статье будет рассматриваться именно работа с GDB.

В статье будет рассматриваться отладка кода на C в ОС Linux.

Вступление

GDB - это невероятно мощный инструмент, и хоть его очень легко начать использовать, GDB нельзя назвать интуитивно понятным: многие возможности утилиты скрыты от глаз пользователя. И чтобы начать использовать GDB "на полную", нужно потратить много времени на изучение документации.

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

В этой статье будут рассмотены некоторые из них.

Типичный пример использования GDB

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

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

#include <stdio.h>

int main(void)
{
  int i = 0;
  printf("Hello world!\n");
  printf("i is %d\n", i);
  i++;
  printf("i is now %d\n", i);
  return 0;
}

Пример не сильно сложнее типичного "Hello World", но в нём есть несколько строчек, так что мы можем рассмотреть его в дебаггере.

Скомилим программу и запустим её в GDB:

gcc -g hello.c
gdb a.out

В самом дебаггере мы сталкиваемся с примерно таким интерфейсом:

Интерфейс GDB по умолчанию
Интерфейс GDB по умолчанию

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

Да и будем честны, такой интерфейс достаточно неряшливый, и как будто пришёл к нам из семидесятых.

Text User Interface

Активация и что это такое

И здесь нам приходит на помощь такой режим использования GDB, как TUI, или же Text User Interface (что, конечно, не самое хорошее название, потому что по умолчанию в GDB и так в какой-то форме используется текстовый интерфейс).

Чтобы активировать TUI, нужно нажать сочетание клавиш Ctrl+X A (не спрашивайте, почему именно такое), или же сразу запустить отладчик командой gdb -tui.

После активации TUI видим следующее:

Text User Interface в GDB
Text User Interface в GDB

Перед нами псевдографика в GDB с предпросмотром кода исполняемой программы!

Конечно, этот интерфейс тоже выполнен в стиле ретро, но он удобный и функциональный.

В окне с кодом программы нам показываются брейкпоинты и текущая строка выполняемого кода.

Стандартный вывод и перерисовка окна

В этом интерфейсе есть и свои минусы: если пару раз прописать next для нашей тестовой программы, её вывод немного сломает текстовый интерфейс:

Влияние stdout на TUI
Влияние stdout на TUI

Но это легко исправить, прожав шорткат Ctrl+L, который "перерисовывает" экран GDB.

Конфигурация окон

Исходный код программы и командная строка - это далеко не вся информация, которую можно просматривать в TUI. Если нажать Ctrl+X 2, то у нас откроется ассемблерный код исполняемой программы:

Ассемблерный код исполняемой программы в TUI
Ассемблерный код исполняемой программы в TUI

Если ещё понажимать Ctrl+X 2, то у нас будут открываться другие режимы TUI c другими окнами.

Так же можно изменить отображение каких-то окон напрямую через командную строчку, например с помощью команды tui reg float.

С помощью стрелок вверх/вниз можно пролистнуть исходный код программы. Для навигации по надавним командам GDB используются шорткаты Ctrl+P и Ctrl+N (Previous и Next).

Таким образом можно очень легко настроить интерфейс дебаггера под себя без лишних затрат по времени.

Интерпретатор Python

Да, в GDB (начиная с 7й версии) есть встроенный интерпретатор питона!

С помощью встроенного интерпретатора можно писать программы на питоне "общего назначения", то есть те, которые могли бы работать и отдельно от отладчика.

Например, вот так выглядит получение PID текущего процесса:

(gdb) python
> import os
> print("my pid is %d" % os.getpid())
> end
my pid is 5228
(gdb)

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

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

(gdb) python print(gdb.breakpoints())
<gdb.Breakpoint object at 0x.....> <gdb.Breakpoint object at 0x.....>

Или создать новый брейкпоинт (в данном примере на 7й строке):

(gdb) python gdb.Breakpoint('7')
breakpoint 4 at 0x....:  file hello.c, line 7.

Reversible Debugging

Возьмём для примера новую тестовую программу, bubble_sort.c:

#include <stdlib.h>
#include <time.h>
#include <stdbool.h>

void sort(long* array)
{ удобнее
  int i = 0;
  bool sorted;

  do {
    sorted = true;

    for (int i = 0; i < 31; i++)
    {
      long *item_one = &array[i];
      long *item_two = &array[i+1];
      long swap_store;

      if (*item_one <= *item_two)
      {
        continue;
      }

      sorted = false;
      swap_store = *item_two;
      *item_two = *item_one;
      *item_one = swap_store;
    }
  } while (!sorted);
}

int main()
{
  long array[32];
  int i = 0;
  srand(time(NULL));
  for (i = 0; i < rand() % sizeof array; i++)
  {
    array[i] = rand();
  }

  sort(array);

  return 0;
}

Перед нами очень простая программа, которая сортирует массив из 32х случайных чисел типа long методом пузырька.

Но проблема заключается в том, что, хоть и редко, эта программа выдаёт ошибку Segmentation fault (core dumped) (или же, как эта ошибка вывелась на моей машине, *** stack smashing detected ***: terminated).

Обычный порядок действий в такой ситуации для отладки был бы следующий:

ls -lth core*
gdb -c core.xxxxx

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

Итак, в такой ситуации нам и поможет обратный дебаггинг в GDB.

Нам нужно найти контекстную информацию по программе в момент, когда в ней происходит segfault, но при этом, чтобы этот segfault произошёл, нам нужно запустить программу достаточно много раз. Что же делать?

Итак, для начала просто запустим GDB:

gdb a.out
(gdb) start
(gdb) next

Затем поставим точки останова на входе в функцию main и в точке _exit.c:30 (служебный файл, код из которого вызывается при завершении работы программы).

(gdb) b main
(gdb) b _exit.c:30

И далее, для этих брейпоинтов, нам надо прописать следующий код:

(gdb) command 3
run
end
(gdb) commnand 2
record
continue
end
(gdb) set pagination off

Что же мы написали?

  • Когда выполнение программы дойдёт до точки останова 3 (то есть до _exit.c:30), программа начинает своё выполнение сначала

  • Когда выполнение программы дойдёт до точки останова 2 (то есть до точки входа в функцию main), GDB начнёт "запись" просиходящих в программе событий и продолжит выполнение

  • Ну и команда set pagination off просто делает вывод GDB чуть более удобным для чтения.

И теперь, когда будет выполнена команда run, GDB будет в цикле запускать исходную программу, пока не нарвётся на нужную нам ошибку.

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

  1. Включаем режим TUI, чтобы убедиться, на каком этапе выполнения мы находимся. Поначалу нам покажет какую-то непонятную строчку из libc. Чтобы получить полезную информацию о работе bubble_sort.c , с помощью команды reverse-step дойдём до момента, когда у нас выполнялась последняя строчка функции main.

  2. На этом этапе смотрим, когда же мы ушли в аварийное состояние: включая просмотр текущего ассемблерного кода через Ctrl+X 2 ясно видим, после какой инструкции мы уходим в аварийное состояние:

Обнаружение момента повреждения стэка через TUI
Обнаружение момента повреждения стэка через TUI
  1. Если дальше прожать несколько раз step, увидим, что мы переходим к коду, который уже пишет сообщение об ошибке (*** stack smashing detected ***: terminated). Получается, ошибка именно в том, что кто-то повредил стэк. Посмотрим стэковый указатель:

Стэк в конце выполнения программы
Стэк в конце выполнения программы
  1. Здесь мы видим адрес, на который ссылается стэковый указатель. Посмотрим историю взаимодействия с данными по этому адресу:

(gdb) watch *(long**) 0x......
(gdb) reverse-continue

...или же, можем расставить брейкпоинты по ходу выполнения программы, и далее с помощью command <4/5/....> настроить для каждого брейкпоинта вывод текущего адреса, на который ссылается стэковый указатель.

  1. Таким образом, мы "переместимся назад во времени" и увидим, кто же сломал наш стэк. В итоге приходим к тому, что изменение стэка произошло на 39й строке, где мы записываем данные в массив array (что в принципе было ожидаемо). После просмотра вывода команды print i становится очевидно, что при заполнении array случайными числами счётчик i вышел за границы массива.

Ошибка заключается в выражении i < rand() % sizeof array, где мы должны считать количество элементов в массиве, а не количество байт.

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

Полезные ссылки

Комментарии (15)


  1. bfDeveloper
    26.06.2024 09:22
    +7

    GDB - очень крутой инструмент, надо только не бояться консоли. С ним есть ощущение всемогущества. Я, может быть, плохо освоил отладчик MS VS, но он выглядит гораздо менее мощным по фичам и вообще не кастомизируемым.


    1. Seenkao
      26.06.2024 09:22
      +2

      Взгляни сюда и ты поймёшь в чём его сила брат. )))


  1. erley
    26.06.2024 09:22
    +3

    А ещё GDB цепляется к ViM/Emacs и некоторым другим редакторам/средам


  1. marxxt
    26.06.2024 09:22
    +5

    Рекомендую еще попробовать скрипт gef, очень удобный

    https://hugsy.github.io/gef/


  1. buldo
    26.06.2024 09:22

    Хорошо, что я дебажу linux проги из VisualStudio


  1. feelamee
    26.06.2024 09:22

    честно говоря, не переубедили.

    я использую gdb постоянно, но довольно плохо с ним знаком.

    reverse-* кстати не работают в многопоточной программе, хотя наверняка я просто не знаю какого то хака.

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

    Иначе я все ещё умею дебажить бабл сорт, но трачу часы на воспроизведение и поиск причины ошибки в многопоточной гуи


    1. Seenkao
      26.06.2024 09:22
      +1

      Выставляем необходимые breakpoint и работаем с ними. Останавливаться будет вся программа, а не какой-то отдельный процесс.


  1. vk6677
    26.06.2024 09:22

    Меня смущает это: "Ошибка заключается в выражении i < rand() % sizeof array, где мы должны считать количество элементов в массиве, а не количество байт.".

    Тут вообще случайная верхняя граница.


    1. randomsimplenumber
      26.06.2024 09:22

      Тут вообще случайная верхняя граница.

      Там ещё деление по модулю есть ;)

      Ну, синтетический пример же.


      1. vk6677
        26.06.2024 09:22

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


        1. randomsimplenumber
          26.06.2024 09:22
          +1

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


  1. Seenkao
    26.06.2024 09:22

    Не зачёт. К статье не подготовлен!

    Важная ссылка, хотите ознакомится почти полностью с GDB прочитайте её.

    мы будем исполнять команды по типу stepinextdisas или list.

    Серьёзно? Ни чего что используются короткие их значения? Да ещё и команды run, continue и другие пропущены.

    Используйте их короткие формы: r, c, s, n, d и l.

    Ещё есть отладчик EDB основанный на GDB но в графической оболочке. Но этот отладчик вроде только для ассемблера (врать не буду, не знаю точно).

    По поводу отладки на разных ЯП, GDB сейчас не мало даёт возможностей отлаживать для ЯВУ. Многие IDE его используют для отладки кода. Потому что есть возможность просматривать как высокоуровнейвый код, так и ассемблерный (машинный).

    За то что ознакомился с GDB - зачёт.

    За то что лишь поверхностно и очень мало - не зачёт.


    1. IGR2014
      26.06.2024 09:22
      +1

      Не зачёт. К комментариям не подготовлен!
      https://man7.org/linux/man-pages/man1/gdb.1.html - вотс ссылка, хотите ознакомится почти полностью с GDB прочитайте её


    1. Djivs Автор
      26.06.2024 09:22

      За незачёт конечно спасибо, но, к счастью, в моём вузе вы не преподаёте)

      А вообще, почему из всей статьи ваше внимание привлекло только одно предложение из вступления? Команды там приведены просто для примера, а не чтобы описать все сценарии использования GDB...

      Ну и судя по остальному содержанию комментария, думаю, вам нужно написать свою статью)


      1. Seenkao
        26.06.2024 09:22

        А вообще, почему из всей статьи ваше внимание привлекло только одно предложение из вступления?

        А не привлекло бы, если бы дальше во всей статье они в таком формате не использовались.

        Ну и судя по остальному содержанию комментария, думаю, вам нужно написать свою статью)

        Я писал статью про ассемблер где указал именно ссылки на использование GDB. По простой причине, что я его не достаточно изучил, чтоб писать целые статьи по нему. Да ещё и обзор сделать больше, чем сделали до это до меня.

        не забываем нажать минус! Ведь я же не прав. )))