Введение

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

Отладка – это процесс обнаружения и устранения существующих и потенциальных ошибок (также называемых "ошибками") в программном коде.

В наших повседневных сценариях программирования мы часто используем операторы печати (например, console.log()) для отладки. Но представьте себе этот сценарий. Вы работаете над большой кодовой базой, и компиляция всего кода занимает около 3-4 минут. Теперь, если вы где-то застряли и добавляете оператор печати, вам нужно перекомпилировать код. И если вы продолжите делать это постоянно, вам придется перекомпилировать код каждый раз, когда вы вносите изменения. Это слишком неэффективно и является огромной тратой времени.

Здесь на помощь приходят отладчики. В этой статье мы рассмотрим GDB.

Что такое ГБД?

GDB (GNU Debugger) – это отладчик для C и C++.

GDB позволяет выполнять такие действия, как запуск программы до определенного момента, затем остановка и распечатка значений определенных переменных в этой точке, или пошаговое выполнение программы построчно и вывод значений каждой переменной. после выполнения каждой строки.

GDB использует интерфейс командной строки, который поначалу может показаться пугающим, но как только вы привыкнете к командам, пользоваться им станет очень просто. В этой статье мы рассмотрим некоторые основные команды.

Начнем с установки gdb.

Установка ГБД

Если вы используете Linux, у вас, вероятно, уже есть gdb, но если вы используете Windows, вам нужно будет его установить.

Убедитесь, что у вас уже установлен gdb, выполнив в терминале следующую команду:

$ gdb --version c:\mingw\bin\gdb.exe --version

линукс

Вы можете установить gdb в дистрибутиве Linux на основе Debian (например, Ubuntu, Mint и т. д.) с помощью следующей команды.

$ sudo apt-get update sudo apt-get install gdb

Окна

Если у вас установлен MinGW, значит, у вас уже есть gdb. В противном случае загрузите последнюю версию установщика MinGW здесь.

  • Установите MinGW из установщика.
  • Запустите «Менеджер установки MinGW» (который может находиться в C:\MinGW\libexec\mingw-get\guimain.exe)
  • Убедитесь, что пакет mingw32-gdb bin установлен.

После завершения установки еще раз проверьте версию с помощью приведенных выше команд. Давайте начнем с отладки простой программы.

ПРИМЕЧАНИЕ. GDB — это хорошо, но не очень красиво, поэтому вместо этого вы можете использовать GDB-GEF. Его очень легко установить. В этой статье я буду использовать GEF, и вы должны использовать его тоже.

Запуск ГБД

Для отладки нам нужен код. Давайте напишем простую программу для вычисления факториала на C.

#include <stdio.h>

const int MAX_FACTORIAL = 20;

int factorial(int n) {
  int res = 1;
  for (int i = 1; i <= n; i++) {
    res = res * i;
  }
  return res;
}

int main() {
  int val;
  printf("Enter a number: ");
  scanf("%d", &val);
  printf("Factorial of %d is %d\n", val, factorial(val));
  return 0;
}

Скомпилируем код:

$ gcc main.c -o factorial

Теперь давайте запустим gdb в двоичном файле factorial. Мы делаем это с помощью команды gdb program.

$ gdb factorial

Вы видите такие ошибки?

Reading symbols from factorial... (No debugging symbols found in factorial)

Чтобы выйти из gdb, нажмите Ctrl+C.

Это потому, что для подготовки нашей программы к отладке с gdb мы должны скомпилировать ее с флагом -g. Итак, давайте на этот раз перекомпилируем нашу программу с флагом -g.

gcc -g main.c -o factorial
gdb factorial

Теперь мы больше не видим ошибку (No debugging symbols found in factorial). Итак, теперь мы можем использовать его gdb для отладки нашего кода.

Под капотом gdb автоматически загружал таблицу символов. Мы рассмотрим таблицу символов позже.

Точка останова

breakpoint — это место в вашей программе, где вы хотели бы временно остановить выполнение, чтобы проверить значения переменных или попытаться выяснить, где происходит сбой программы и т. д. Чтобы установить точку останова, вы используете команду break.

Помните:почти все часто используемые команды gdb имеют более короткую версию, и мы должны активно использовать их для повышения скорости.

break main # OR a shorter version 
b main

Выход:

gef➤  b main
Breakpoint 1 at 0x117f: file main.c, line 15.

Это означает, что мы установили точку останова в функции main, так что, как только указатель инструкции достигнет основной функции, она остановит выполнение и будет ждать наших команд.

Чтобы просмотреть список всех точек останова, используйте команду info break. Как следует из названия команды, она даст нам информацию обо всех установленных нами точках останова.

gef➤  info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000000117f in main at main.c:15

С каждой точкой останова связан индекс. Эти значения индекса будут использоваться, если вы хотите удалить точку останова или отключить ее. Во-первых, давайте запустим программу с установленной точкой останова. Для запуска используйте команду run (или просто r в более короткой версии).

gef➤ run 
// OR
gef➤ r

Вы увидите, что выполнение программы остановлено. Давайте просто возобновим выполнение командой continue (или просто c).

gef➤  c
Continuing.
Enter a number: 5
Factorial of 5 is 120
[Inferior 1 (process 128334) exited normally]

Вы заметите, что он запросит число, а затем отобразит факториал этого числа и завершит работу в обычном режиме. Почему? Потому что в нашем коде не было других точек останова, и поэтому выполнение больше нигде не останавливалось.

Иногда нам не нужно останавливаться на точке останова, поэтому мы можем отключить ее. Чтобы отключить точку останова, используйте команду disable.

gef➤  info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000000117f in main at main.c:15
gef➤  disable 1
gef➤  info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep n   0x000000000000117f in main at main.c:15Once, disabled you can notice the value under Enb is not y anymore. It's n representing it's not enabled anymore. If you run the program now, you will notice that it didn't stop at any place. This is because the breakpoint is disabled.

После отключения вы можете заметить, что значение под Enb больше не y. Это n означает, что он больше не включен. Если вы запустите программу сейчас, вы заметите, что она нигде не остановилась. Это потому, что точка останова отключена.

gef➤  r
Starting program: /home/arnab/Desktop/SWE-Lab/ass2/prog/factorial
Enter a number: 5
Factorial of 5 is 120
[Inferior 1 (process 411121) exited normally]

Иногда мы добавляем точку останова в функцию внутри цикла. И мы не хотим останавливаться на каждой итерации цикла. В этом сценарии мы можем игнорировать точку останова определенное количество раз, используя команду ignore. Давайте попробуем это. У нас есть цикл в нашей функции factorial(). Давайте добавим точку останова в цикл.

СОВЕТ. Код функции можно просмотреть непосредственно из терминала gdb с помощью команды list. Так что сделайте list factorial, и он отобразит исходный код factorial().

Мы хотим установить точку останова в строке 8 этого файла main.c. Итак, мы будем использовать команду b main.c:8.

gef➤  info break  # to list our breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep n   0x000000000000117f in main at main.c:15
gef➤  list factorial # to see the source code
1 #include <stdio.h>
2
3 const int MAX_FACTORIAL = 20;
4
5 int factorial(int n) {
6   int res = 1;
7   for (int i = 1; i <= n; i++) {
8     res = res * I;  # <-- breakpoint here
9   }
10   return res;
11 }

gef➤  b main.c:8 # add breakpoint to line 8 of main.c
Breakpoint 2 at 0x115c: file main.c, line 8.
gef➤  info break # to check if the breakpoint was added
Num     Type           Disp Enb Address            What
1       breakpoint     keep n   0x000000000000117f in main at main.c:15
2       breakpoint     keep y   0x000000000000115c in factorial at main.c:8

Теперь, если вы запустите программу с помощью команды r. Вам будет предложено ввести число (введите что-то вроде 5). Затем вы увидите, что он достигает точки останова в функции factorial() (помните, что мы отключили точку останова в функции main()). Введите команду continue или c, и она снова попадет в ту же точку. Потому что мы сейчас не внутри петли. Продолжайте делать это 5 раз. Наконец, когда цикл завершится, вы заметите, что отображается результат факториала.

В этом сценарии, если мы хотим игнорировать точку останова, возможно, первые 4 раза. Мы будем использовать команду ignore <index of breakpoint> 4.

gef➤  info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep n   0x000055555555517f in main at main.c:15
2       breakpoint     keep y   0x000055555555515c in factorial at main.c:8
 breakpoint already hit 5 times
gef➤  ignore 2 4
Will ignore next 4 crossings of breakpoint 2.
gef➤  info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep n   0x000055555555517f in main at main.c:15
2       breakpoint     keep y   0x000055555555515c in factorial at main.c:8
 breakpoint already hit 5 times
 ignore next 4 hits

Запустим программу. На этот раз, когда он запрашивает число, мы вводим 5. После этого он столкнется с точкой останова в функции factorial(). Нажмите c для продолжения, и программа завершится. Это связано с тем, что gdb проигнорировал точку останова в первые 4 раза и сработал только в 5-й раз, когда мы достигли точки останова.

Теперь давайте удалим эту точку останова с помощью команды delete breakpoints <index>.

gef➤  info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep n   0x000055555555517f in main at main.c:15
2       breakpoint     keep y   0x000055555555515c in factorial at main.c:8
 breakpoint already hit 5 times
gef➤  delete 2
gef➤  info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep n   0x000055555555517f in main at main.c:15
gef➤  delete 1
gef➤  info break
No breakpoints or watchpoints.

Таблица символов

Вы когда-нибудь задумывались, как компилятор запоминает, какая структура данных относится к какому типу и так далее? Ответ: Таблица символов. Это структура данных, используемая компилятором для отслеживания семантики имен переменных, таких как:

  • Тип данных переменной.
  • Когда используется: область (действующий контекст, в котором допустимо имя).
  • Где хранится: адрес хранения.

В gdb при запуске gdb factorial будет загружена таблица символов. Теперь давайте выйдем из gdb, нажав Ctrl+C или набрав q и Enter. На этот раз просто запустите gdb.

Для загрузки программы и ее таблицы символов используйте команду file <program>.

gef➤  file ./prog/factorial
Reading symbols from ./prog/factorial...

Теперь, когда он прочитал символы, мы можем взаимодействовать с ними. Запустите команду info address <symbol name>, чтобы увидеть адрес этого символа.

Теперь, когда вы знаете адрес, вы можете выполнить обратный поиск имени символа с помощью команды info symbol <address>.

gef➤  info address main
Symbol "main" is a function at address 0x1177.
gef➤  info address factorial
Symbol "factorial" is a function at address 0x1145.
gef➤  info address MAX_FACTORIAL
Symbol "MAX_FACTORIAL" is static storage at address 0x2004.

Лучшая часть этого info symbol заключается в том, что если вы не укажете точный адрес, покажите смещение от начала символа. Так что в моем случае символ main находится по адресу 0x1177. Так что давайте попробуем посмотреть, что символ имени адреса 0x1180.

gef➤  info symbol 0x1180
main + 9 in section .text

Теперь вспомните, что у нас была переменная с именем val в файле main. Давайте посмотрим адрес этого.

gef➤  info address val
No symbol "val" in current context.

Это означает, что мы еще не в правильном диапазоне. Поскольку переменная val была объявлена ​​внутри функции main(), мы не можем получить к ней доступ вне функции main(). Итак, давайте установим точку останова на главном и запустим программу, после чего мы увидим адрес val в таблице символов.

gef➤  b main
Breakpoint 1 at 0x117f: file main.c, line 15.
gef➤  r
gef➤  info address val
Symbol "val" is a complex DWARF expression:
     0: DW_OP_fbreg -20
.

Чтобы просмотреть список всех функций в таблице символов, используйте команду info func. Вы также можете фильтровать функции с помощью регулярных выражений. Давайте найдем нашу функцию factorial. Точное соответствие регулярному выражению будет ^facorial$.

gef➤  info func ^factorial$
All functions matching regular expression "^factorial$":

File main.c:
5: int factorial(int);

Мы можем сделать то же самое для глобальных переменных с помощью команды info var. Давайте найдем нашу глобальную переменную MAX_FACTORIAL.

gef➤  info var ^MAX_FACTORIAL$
All variables matching regular expression "^MAX_FACTORIAL$":

File main.c:
3: const int MAX_FACTORIAL;

В таблице символов также хранится тип переменной. Мы можем использовать команду whatis <variable name>, чтобы увидеть тип переменной.

gef➤  whatis MAX_FACTORIAL
type = const int
gef➤  whatis val
type = int

Работа с переменными

До сих пор мы видели, как устанавливать точки останова, запускать программу и получать таблицу символов. Теперь давайте посмотрим, как работать с переменными. В большинстве случаев нам не нужны все глобальные переменные, нам нужны локальные переменные. Для этого у нас есть команда info locals, которая покажет нам локальные переменные.

Во-первых, давайте установим точку останова после оператора scanf (это строка номер 17 в файле main.c). Итак, запустите:

gef➤  b main.c:17
Breakpoint 2 at 0x11a8: file main.c, line 17.

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

gef➤  info locals
val = 0xa

Он говорит, что значение переменной val равно 0xa. Мы также можем распечатать значения с помощью команды print (или просто p).

gef➤  p val
$1 = 0xa
gef➤  p/d val
$2 = 10

По умолчанию значение будет в шестнадцатеричном формате, но мы можем указать /d для отображения в десятичном формате. Вот другие форматы:

Мы также можем установить значения переменных, используя команду set variable.

gef➤  set variable val=5
gef➤  p/d val
$3 = 5

Функции

Функции играют важную роль в программах. Итак, давайте посмотрим, как gdb их обрабатывает. В нашем коде есть две функции main и factorial. Итак, начнем с установки точки останова на main.

gef➤  b main
Breakpoint 1 at 0x117f: file main.c, line 15.
gef➤  info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000000117f in main at main.c:15

Запустите программу с помощью r. GDB остановит выполнение на первой строке функции main. Прежде чем двигаться дальше, давайте попробуем понять разницу между этой командой next и другой очень похожей командой под названием stepi.

  • next: выполнить следующую строку, включая любые вызовы функций.
  • stepi: пошаговые инструкции, а не исходные строки

Таким образом, в любой момент, если мы находимся при вызове функции, выполните si (шаг внутрь), чтобы войти в функцию.

СОВЕТ. Вы можете использовать счетчик с next или stepi для многократного выполнения команды.

Выполним next 2 т.е. далее дважды. На этот раз вам будет предложено ввести число. После этого мы перейдем к инструкции, которая приведет нас к факториальной функции.

gef➤  r
gef➤  n 2 // we will be in that line where factorial is called
gef➤  si 3 // we will be in the factorial function
gef➤  n // the first line of the factorial function

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

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

gef➤  info args
n = 0xa
gef➤  p/d n
$1 = 10

Если вы знаете имя аргумента функции, мы можем использовать команду print для вывода значения этого аргумента.

Стек программ

Поскольку мы находимся внутри функции factorial(), которая была вызвана из main(), поэтому, если мы вернемся из factorial(), мы вернемся в main(). Для такой простой программы не так уж важно помнить, какая функция кем вызывается. Но для большой кодовой базы становится трудно за ней следить. Итак, у нас есть команда под названием backtrace, которая показывает нам именно это.

gef➤  backtrace
#0  factorial (n=0xa) at main.c:6
#1  0x00005555555551b2 in main () at main.c:17

Это очень ясно говорит нам о том, что мы оставили main() в строке 17, а теперь находимся в factorial() в строке 6.

Шпаргалка

+------------------------------+------------------------------------------------------------------+
|           Command            |                            Description                           |
+------------------------------+------------------------------------------------------------------+
| `gdb program`                |  to load the symbol table of the program and run the debugger    |
| `break <function name>`      |  to set a breakpoint at a function                               |
| `break *<address>`           |  to set a breakpoint at a specific address                       |
| `info break`                 |  to see the list of all the breakpoints                          |
| `delete break <index>`       |  to delete a breakpoint                                          |
| `disable break <index>`      |  to disable a breakpoint                                         |
| `ignore <index> <count>`     |  to ignore a breakpoint for count number of times                |
| `run <arglist>`              |  start your program with arglist                                 |
| `continue`                   |  continue the program execution                                  |
| `next`                       |  to execute the next line, including function calls              |
| `stepi`                      |  to step into the next machine instruction                       |
| `info address <symbol-name>` |  show where symbol s is stored                                   |
| `info func <regex>`          |  show names, types of defined functions (all, or matching regex) |
| `info var <regex>`           |  show names, types of global variables (all,or matching regex)   |
| `info locals`                |  show names, types of local variables                            |
| `info args`                  |  show names, types of function arguments                         |
| `whatis <expr>`              |  show data type of expr                                          |
| `p/<format> <expr>`          |  print the value of expr in the specified format                 |
| `backtrace`                  |  show the call stack                                             |
+------------------------------+------------------------------------------------------------------+

Заключительные примечания

Несмотря на то, что еще многое предстоит рассказать, эта статья должна дать общее представление о том, как работает отладчик GDB. Если вы работаете с большой кодовой базой, у вас не будет другого выбора, кроме как использовать отладчики, такие как GDB. При правильном использовании эти инструменты значительно облегчат вам жизнь.

Кроме того, если вы играете в CTF, вы могли столкнуться с проблемами использования двоичных файлов, где снова единственный способ получить флаг — отладить код и понять двоичный файл. В GDB есть и другие возможности, такие как просмотр регистров, поиск переменных в стеке, контрольные точки, потоки и т. д., но они будут рассмотрены в последующих статьях.

Я студент колледжа, который постоянно учится. Если у вас есть какие-либо отзывы, не стесняйтесь оставлять их в комментариях. Я был бы очень признателен за это. Кроме того, не стесняйтесь обращаться ко мне через любую из этих социальных сетей (в основном активны в Твиттере):

Первоначально опубликовано на https://arnabsen.hashnode.dev.