Pull to refresh

Дайте мне 15 минут, и я изменю ваш взгляд на GDB

Level of difficultyMedium
Reading time6 min
Views13K
Original author: Greg Law

Материал подготовлен на основе выступления с 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 мы смогли обнаружить, какая часть кода провоцирует ошибку в заданном примере.

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

Tags:
Hubs:
Total votes 47: ↑45 and ↓2+63
Comments16

Articles