GDB

Не совсем удобный, однако очень мощный инструмент для отладки приложений. Позволяет ставить break-point'ы, смотреть содержимое регистров, стэка, памяти, текущие инструкции. Если код собран с debug символами - может выдавать информацию о том, где именно произошёл сбой в коде, а если ещё и подложить source-код можно смотреть за исполнением программы непосредственно по коду.

Тестовый пример

Будем ковырять простенькую программку с парой вызовов и вложенными структурами:

#include <stdio.h>
#include <stdlib.h>
 
typedef struct data_s {
  int first;
  int second;
} data;
 
typedef struct test_s {
  int integer;
  char * string;
  data * data;
} test;
 
void initialize_data(void * test) {
  data * d = malloc(sizeof(data));
  d->first = 1;
  d->second = 2;
 
  ((struct test_s *)test)->data = d;
}
 
test * initialize_test(void) {
  test * elem = malloc(sizeof(test));
  elem->integer = 254;
  elem->string = "Test string";
  initialize_data((void*) elem);
 
  return elem;
}
 
int main(int argc, char ** argv) {
  test * t = initialize_test();
  test * t2 = initialize_test();
  free(t2->data);
  t2->data = NULL;
 
  printf("Test is:\n\tinteger: %d\n\tstring: %s\n\tData:\n\t\tfirst: %d\n\t\tsecond: %d\n", t->integer, t->string, t->data->first, t->data->second);
  printf("Generate SIGSEGV!\n");
  printf("Test 2 is:\n\tinteger: %d\n\tstring: %s\n\tData:\n\t\tfirst: %d\n\t\tsecond: %d\n", t2->integer, t2->string, t2->data->first, t2->data->second);
  return 0;
}

SEGFAULT организован намеренно, чтобы показать ниже работу с core dump.

Для отладки собираем следующим образом

# gcc -o main -g -O0 main.c

Запускаем и получаем следующий вывод:

┌─[ owlbook@workbook ]─[ 09:16:36 ]─[ ~/dev/c/test ]
└─$ ./main 
Test is:
        integer: 254
        string: Test string
        Data:
                first: 1
                second: 2
Generate SIGSEGV!
Segmentation fault (core dumped)

К слову чтобы coredump собирался - нужно сделать несколько хитростей: увеличить core file size в ulimit, а так же подкрутить sysctl

kernel.core_uses_pid = 1
kernel.core_pattern = /tmp/core-%e-%p

После этого при получение ошибки сегментирования дамп будет складываться в файлы /tmp/core-<program name>-<pid>

Начало работы

Просто передаём gdb нужный бинарь. Если последний собран с g флагом и без strip - мы получим возможность работать с debug symbols (точки останова можно будет делать по названиям методов/функций и прочие человекочитаемые удобства). Аргумент O0 в примере используется для того, чтобы компилятор не применял никакие оптимизации. И самый роскошный момент - если запустить gdb в директории с исходниками - можно будет сменить layout и шагать при дэбаге непосредственно по коду.

# gdb main

Точки остановка (break points)

Создаются достаточно просто

(gdb) b[reak] <address/routine name>

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

Как только исполнение дойдёт до нужного момента - gdb приостановит выполнение и передаст управление в терминал. Начиная с этого момента можно посмотреть текущее состояние регистров, памяти, получить значения локальных и глобальных переменных (опять же если собрано без strip), а так же привести эти переменные к типам (если собрано с debug символами) и получить содержимое, например, структуры по полям.

Выставим первый breakpoint в нашем приложении

(gdb) b initialize_test
Breakpoint 1 at 0x119b: file main.c, line 24.

Запуск приложения

Само приложение в gdb передаётся без аргументов, однако оно не запустится до явного указание на это. Для запуска есть специальная команда

(gdb) run [arguments]

Запускаем наше приложение и попадаем на первый break point

(gdb) run
Starting program: /home/owlbook/dev/c/test/main 

Breakpoint 1, initialize_test () at main.c:24
24        test * elem = malloc(sizeof(test));

Переключение между способами отображения информации

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

(gdb) layout next

Собственно аргумент next переключит текущий способ отображения на следующий. Есть например без отображения кода, с отображением исходного кода, с отображением asm инструкций, с отображением asm и кода, с отображением кода и содержимого регистров, asm инструкций и содержимого регистров.

Source code layout

Source code and registers layout

Вывод значений переменных

Для вывода используется довольно очевидная команда

(gdb) print <variable name/memory address>

Для вывода содержимого по указателю как и в C нужно указать явно, что мы хотим посмотреть что же там лежит:

(gdb) p elem 
$1 = (test *) 0x555555559260
(gdb) p elem->integer
$2 = 254
(gdb) p *elem
$3 = {integer = 254, string = 0x0, data = 0x0}

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

$4 = (void *) 0x555555559260
(gdb) p (struct test_s *)test
$5 = (struct test_s *) 0x555555559260
(gdb) p *(struct test_s *)test
$6 = {integer = 254, string = 0x555555556008 "Test string", data = 0x0}

Так же можно вывести содержимое памяти по определённому адресу

(gdb) x/<count><format><unit> <address/$register>

Где <count> это количество ячеек для вывода, <format> формат вывода (s - null-terminated строка, i - инструкции, x - hex), <unit> размер выводимых данных (b - байты (1), h - полуслова (2), w - слова (4), g - двойные слова (8))

Попутно можно так же получить некоторую информацию о текущей области видимости и методе/функции

(gdb) info locals
(gdb) info args
(gdb) info locals
d = 0x555555555070 <_start>
(gdb) info args
test = 0x555555559260

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

(gdb) display (char *)$rsp

Данная запись будет выводить строку по указателю в %rsp.

Получение цепочки вызовов (backtrace)

Для того чтобы получить текущую цепочку вызовов достаточно использовать команду

(gdb) b[ack]t[race]

На примере нашего приложения это будет выглядеть примерно вот так:

(gdb) bt
#0  initialize_data (test=0x555555559260) at main.c:16
#1  0x00005555555551ee in initialize_test () at main.c:27
#2  0x0000555555555208 in main (argc=1, argv=0x7fffffffdab8) at main.c:33

Перемещение по исполнению

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

(gdb) s[tep] [count]

Данная команда перемещает нас до следующего (или на count) по исполнению source line.

(gdb) s[tep]i [count]

Данная команда перемещает нас уже до следующей (или на count) asm инструкции.

Следует учитывать, что stepping поведёт вглубь по цепочке вызовов, показывая всё, что происходит под капотом вызова тех или иных функций/методов. Чтобы просто перейти на следующую строчку выполнения в данной функции/методе следует использовать другую команду

(gdb) n[ext]

Аналогично есть и nexti для работы с инструкциями.

(gdb) continue

Запускает исполнение приложения до достижения следующего break point.

Работа с coredump

Если у нас падает какое-то приложение и включена запись coredump - можно воспользоваться прекрасной возможностью gdb и подгрузить dump, чтобы посмотреть что именно произошло и где. (Например у нас на серверах может быть приложение без debug символов и на месте не разобраться, но мы можем локально собрать себе приложение в любом виде и с помощью coredump загрузить приложение с тем контекстом, который был пере падением)

# gdb --core /tmp/coredump-main-1234 ./main
Reading symbols from main...
[New LWP 23995]
Core was generated by `./main'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  main (argc=1, argv=0x7fff2cb512d8) at main.c:40
40        printf("Test 2 is:\n\tinteger: %d\n\tstring: %s\n\tData:\n\t\tfirst: %d\n\t\tsecond: %d\n", t2->integer, t2->string, t2->data->first, t2->data->second);

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

(gdb) p *t2
$1 = {integer = 254, string = 0x558c0450b008 "Test string", data = 0x0}

Мы-то конечно знаем где, потому что сами сделали, но ход мыслей достаточно простой: в показанной выше строчке у нас идёт обращение к переменной t2, значит сначала смотрим её и постепенно погружаемся по указателям, пока не найдём поломанный. В нашем случае это указатель data на область памяти 0x0.