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

Автор: | 01/30/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, что бы включить отладочную информацию:

gcc debug.c -g -o debug

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

./debug
Enter a number: 1
Segmentation fault

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

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

Запуск gdb

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

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

run

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

(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.

backtrace

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

(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

Тут видно, что 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):

(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)));

print

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

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

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

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

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

(gdb) print buf
2 = 0x0

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

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

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

Попробуем исправить наш код — добавим вызов 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):

(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);

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

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

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

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

Проверяем:

(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)));

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

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

(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]

См. 10.12 Convenience Functions и 5.1.6 Break Conditions.

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

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

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

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

(gdb) r
Starting program: /home/admin/Scripts/debug
Enter a number: 1
Breakpoint 1, printSum () at debug.c:27
27          printf("%s\n",buf);

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

(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]

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

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

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 }
...
(gdb) b 8
Breakpoint 1 at 0x8ae: file debug.c, line 8.

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

(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++)

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

Пробуем next:

(gdb) n
9               sum+=i;

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

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

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

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

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

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

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

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

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

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

(gdb) x 0x7fffffffe528
0x7fffffffe528: 0x00000001

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

(gdb) p sum
3 = 1

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