C: отладка с gdb – примеры

Автор: | 30/01/2019

gdb (GNU Project Debugger) используется при отладке/дебаге кода.

Ниже приводятся примеры работы с кодом на С.

Используем такой код:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

char * buf;
int sum_to_n(int num) {

    int i,sum=0;
    for(i=1;i<=num;i++)
        sum+=i;

    return sum;
}

void printSum() {

    char line[10];
    
    printf("Enter a number: ");
    fgets(line, 10, stdin);
    
    if(line != NULL) 
        strtok(line, "\n"); 
    
    sprintf(buf,"sum=%d", sum_to_n(atoi(line)));
    
    printf("%s\n",buf);
}

int main(void) {

    printSum();
    return 0;
}

Собираем его с опцией -g, что бы включить отладочную информацию:

[simterm]

$ gcc debug.c -g -o debug

[/simterm]

Запускаем, и получаем ошибку:

[simterm]

$ ./debug 
Enter a number: 1
Segmentation fault

[/simterm]

В результате получаем Segmentation fault, которая сигнализирует об ошибочном обращении к памяти.

Теперь используем gdb, что бы найти причину проблемы.

Запуск gdb

Запускаем gdb, и первым аргументом передаём исполняемый файл для запуска:

[simterm]

$ gdb debug
GNU gdb (Debian 7.12-6) 7.12.0.20161007-git
...
Reading symbols from debug...done.
(gdb) 

[/simterm]

run

Теперь можно начать выполнение программы, используя команду run (или r):

[simterm]

(gdb) r
Starting program: /home/admin/Scripts/debug 
Enter a number: 1

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7aaebcd in __GI__IO_default_xsputn (f=0x7fffffffe340, data=<optimized out>, n=4) at genops.c:450
450     genops.c: No such file or directory.

[/simterm]

backtrace

backtrace указывает gdb на необходимость вывести список всех вызываемых функций из стека программы:

[simterm]

(gdb) backtrace
#0  0x00007ffff7aaebcd in __GI__IO_default_xsputn (f=0x7fffffffe340, data=<optimized out>, n=4) at genops.c:450
#1  0x00007ffff7a80e36 in _IO_vfprintf_internal (s=s@entry=0x7fffffffe340, format=format@entry=0x5555555549b7 "sum=%d", ap=ap@entry=0x7fffffffe468) at vfprintf.c:1320
#2  0x00007ffff7aa3afb in __IO_vsprintf (string=0x555555554720 <_start> "1\355I\211\321^H\211\342H\203\344\360PTL\215\005Z\002", format=0x5555555549b7 "sum=%d", args=args@entry=0x7fffffffe468) at iovsprintf.c:42
#3  0x00007ffff7a89357 in __sprintf (s=<optimized out>, format=<optimized out>) at sprintf.c:32
#4  0x00005555555548ef in printSum () at debug.c:26
#5  0x000055555555490c in main () at debug.c:33

[/simterm]

Тут видно, что debug.c в строке 33 вызывает printSum()printSum () в строке 22 вызывает sprintf(), которая далее начинает выполнение более низкоуровневых функций, и в результате падает.

Всё, что выполняется после sprintf() нам неподконтрольно, и проблема возникает в данных, которые мы передаём в sprintf(), так что давайте изучим их внимательнее:

...
26     sprintf(buf,"sum=%d", sum_to_n(atoi(line)));
...

Далее – используем breakpoint-ы, что бы изучить значения переменных.

Break Point

Для того, что бы приостановить выполнение программы на каком-то этапе – мы можем задать один или цепочку брейкпоинтов, используя break (b).

Например, что бы сделать паузу перед вызовом sprintf() на строке 26 – указываем break 26, и запускаем программу заново – run (r):

[simterm]

(gdb) b 26
Breakpoint 1 at 0x8c2: file debug.c, line 26.
(gdb) r
Starting program: /home/admin/Scripts/debug 
Enter a number: 1

Breakpoint 1, printSum () at debug.c:26
26          sprintf(buf,"sum=%d", sum_to_n(atoi(line)));

[/simterm]

print

С помощью print (p) можно получить текущие значения переменных.

Тут в буфер sprintf() мы передаём переменую line – проверим её значение:

[simterm]

(gdb) print line
$1 = "1\000\000IUUUU\000"

[/simterm]

Видим нашу единицу, за ней null terminator – ‘\0‘, далее – мусор. С этим всё хорошо.

Проверим содержимое buf:

[simterm]

(gdb) print buf
$2 = 0x0

[/simterm]

Теперь проблема становится очевидной: мы пытаемся скопировать данные в буфер, на который указывает buf, но под него не выделена память, что и приводит к Segmentation fault.

К счастью – тут buf является глобальной переменной и была проинициализирована со значением 0 (null pointer). Если бы это было не так, и она находилась бы внутри функции – мы получили бы какое-то произвольное значение вида:

[simterm]

(gdb) p buf
$2 = 0x555555554720 <_start> "1\355I\211\321^H\211\342H\203\344\360PTL\215\005Z\002"

[/simterm]

Попробуем исправить наш код – добавим вызов malloc() для выделения памяти под buf:

...
void printSum() {

    char line[10];

    printf("Enter a number: ");
    fgets(line, 10, stdin);

    if(line != NULL) 
        strtok(line, "\n"); 

    char * buf = malloc(1024);
    sprintf(buf,"sum=%d", sum_to_n(atoi(line)));

    printf("%s\n",buf);
}
...

list

После обновления исходного кода – номера строк изменились.

Что бы увидеть исходный код из самого gdb – используем list (l):

[simterm]

(gdb) l
18          printf("Enter a number: ");
19          fgets(line, 10, stdin);
20
21          if(line != NULL) 
22              strtok(line, "\n"); 
23
24          char * buf = malloc(1024);
25          sprintf(buf,"sum=%d", sum_to_n(atoi(line)));
26
27          printf("%s\n",buf);

[/simterm]

condition – break point с условиями

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

Например – сделать паузу, если значение переменной line будет равно “1” можно так:

  1. задаём точку паузы – (gdb) b 25
  2. задаём условие использовать breakpoint под номером 1, используя функцию $_streq(gdb) condition 1 $_streq(line, "1")

Проверяем:

[simterm]

(gdb) b 25
Breakpoint 1 at 0x920: file debug.c, line 25.
(gdb) condition 1 $_streq(line, "1")
(gdb) r
Starting program: /home/admin/Scripts/debug 
Enter a number: 1

Breakpoint 1, printSum () at debug.c:25
25          sprintf(buf,"sum=%d", sum_to_n(atoi(line)));

[/simterm]

Enter a number: 1 – тут в line мы передаём единицу в виде string, условие срабатывает – получаем остановку выполнения.

При другом значении – программа выполнится полностью:

[simterm]

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/admin/Scripts/debug 
Enter a number: 2
sum=3
[Inferior 1 (process 3404) exited normally]

[/simterm]

См. 10.12 Convenience Functions и 5.1.6 Break Conditions.

Аналогично используется $_regex.

Зададим условие, но иначе – прямо при указании брейкпоинта, без использования condition, и используем if для самого break:

[simterm]

(gdb) break 27 if $_regex(buf, "^sum=1")
Breakpoint 1 at 0x920: file debug.c, line 27.

[/simterm]

Запускаем, и указываем 1:

[simterm]

(gdb) r
Starting program: /home/admin/Scripts/debug 
Enter a number: 1

Breakpoint 1, printSum () at debug.c:27
27          printf("%s\n",buf);

[/simterm]

Если в sum= будет другое значение – условие не сработает:

[simterm]

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/admin/Scripts/debug 
Enter a number: 2
sum=3
[Inferior 1 (process 3663) exited normally]

[/simterm]

Возвращаясь к проблеме с buf – проверим её значение сейчас, после того, как мы добавили malloc():

[simterm]

(gdb) p buf
$2 = 0x555555756830 "sum=1"

[/simterm]

next, step, until – шаги выполнения

  • next (n): выполняет текущую инструкцию, и переходит к началу следующей
  • step (s): аналогична next, но с отличиями: когда вы находитесь в начале функции, и используете next – функция будет выполнена полностью, и вернёт значение. Если использовать step – выполнение перейдёт к первой строке внутри функции
  • until (u): аналогична next, но в случае, если вы находитесь в начале цикла – выполнение будет продолжаться до конца выполнения этого цикла

Пример.

Запускаем программу, задаём точку на строке 8 – перед началом цикла:

  1 #include <stdio.h>
  2 #include <string.h>
  3 #include <stdlib.h>
  4 
  5 int sum_to_n(int num) {
  6 
  7     int i,sum=0;
  8     for(i=1;i<=num;i++)
  9         sum+=i;
 10 
 11     return sum;
 12 }
...

[simterm]

(gdb) b 8
Breakpoint 1 at 0x8ae: file debug.c, line 8.

[/simterm]

Начинаем выполнение:

[simterm]

(gdb) r
Starting program: /home/admin/Scripts/debug 
Enter a number: 1

Breakpoint 1, sum_to_n (num=1) at debug.c:8
8           for(i=1;i<=num;i++)

[/simterm]

Остановились в начале цикла.

Пробуем next:

[simterm]

(gdb) n
9               sum+=i;

[/simterm]

Ещё раз next – снова возращаемся к началу цикла:

[simterm]

(gdb) 
8           for(i=1;i<=num;i++)

[/simterm]

Но если вызовем until – весь цикл будет пройден полностью:

[simterm]

(gdb) u
11          return sum;
(gdb) p sum
$1 = 1

[/simterm]

Другие команды

  • list (l): уже упоминалась выше, отобразит исходный код
  • delete (d): удаляет брейкпоиинты. Если вызвать без агрументов – удалит все точки, если указать номер (d 1) – то удалит заданную по номеру
  • clear function_name: удалить все брейкпоинты в указанной функции
  • x: отобразить содержимое адреса

Пример использования x.

Получаем адрес переменной sum (используя &):

[simterm]

(gdb) p &sum
$2 = (int *) 0x7fffffffe528

[/simterm]

Получаем значение памяти по адресу 0x7fffffffe528:

[simterm]

(gdb) x 0x7fffffffe528
0x7fffffffe528: 0x00000001

[/simterm]

Вот и наша единичка – 0x00000001:

[simterm]

(gdb) p sum
$3 = 1

[/simterm]

Ссылки по теме