Дополнительные статьи

Введение в отладку с помощью GDB

Последнее обновление: 04.10.2023

Набор компиляторов GNU GCC содержит встроенный отладчик GNU (GDB), который также позволяет отлаживать и программы на языке ассемблера, да и теоретически вообще любую скомпилированную программу.

Для запуска отладчика GDB в консоли необходимо ввести команду gdb:

root@Eugene:~/asm# gdb
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb)

После некоторого текста справочной информации отладчик после (gdb) предложит нам ввести некоторую команду. Для начала мы можем ввести команду file и имя файла, который мы хотим отлаживать. Допустим, я хочу провести отладку исполняемого файла, который называется "hello" и который находится в текущей папке. Для этого я ввожу команду file hello:

(gdb) file hello
Reading symbols from hello...
(No debugging symbols found in hello)
(gdb)

Мы установили файл для отладки. Теперь запустим его командой run

(gdb) run
Starting program: /root/asm/hello ~/asm/hello
[Inferior 1 (process 251) exited with code 013]
(gdb)

Команда run выполнила файл. Но отладчик позволяет нам останавливаться на конкретных местах в файле. Для этого применяются точки прерывания (точки прерывания) или breakpoints.

Для установки точки прерывания на конвретный адрес в программе применяется команда break, которой после символа звездочки * передается адрес метки. Например, установка точки прерывания на метке _start

break *_start

Так, установим точку прерывания на метке _start и повторно запустим программу командой run:

(gdb) break *_start
Breakpoint 1 at 0x401000
(gdb) run
Starting program: /root/asm/hello ~/asm/hello

Breakpoint 1, 0x0000000000401000 in _start ()
(gdb)

Мы видим, что программа остановила выполнение на адресе 0x0000000000401000 - адрес метки _start

Отладчик gdb позволяет дизассемблировать программу, то есть отобразить ее ассемблерный код. Для этого надо выполнить команду disassemble:

(gdb) disassemble
Dump of assembler code for function _start:
=> 0x0000000000401000 <+0>:     mov    0x402000,%rdi
   0x0000000000401008 <+8>:     mov    0x402008,%rsi
   0x0000000000401010 <+16>:    add    %rsi,%rdi
   0x0000000000401013 <+19>:    mov    $0x3c,%rax
   0x000000000040101a <+26>:    syscall
End of assembler dump.
(gdb)

Стрелка (=>) указывает на текущую инструкцию. Длинное шестнадцатеричное число (0x0000000000401000) — это адрес инструкции. Число рядом с ним (<+0>) — это смещение от начала ближайшей предыдущей метки или функции (в данном случае метки _start). Справа от него находится следующая инструкция, которую он выполнит, и это первая инструкция в нашей программе.

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

Для просмотра состояния регистров, необходимо ввести команду info registers:

(gdb) info registers
rax            0x0                 0
rbx            0x0                 0
rcx            0x0                 0
rdx            0x0                 0
rsi            0x0                 0
rdi            0x0                 0
rbp            0x0                 0x0
rsp            0x7fffffffdd20      0x7fffffffdd20
r8             0x0                 0
r9             0x0                 0
r10            0x0                 0
r11            0x0                 0
r12            0x0                 0
r13            0x0                 0
r14            0x0                 0
r15            0x0                 0
rip            0x401000            0x401000 <_start>
eflags         0x202               [ IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
(gdb)

Это состояние регистров к началу выполнения кода из метки _start, то есть в момент начала программы. И здесь мы видим, что большинство регистров равны нулю.

Вместо печати всех регистров мы можем вывести на консоль только один - интересующий нас регистр с помощью команды print, которой передается имя регистра, предваряемое символом $. Например, для вывода значения регистра %rdi применяется команда:

print $rdi

Для перехода к следующей инструкции выполним команду stepi

Если после этого мы снова введем команду disassemble, то стрелка поместится на следующую строку кода:

(gdb) stepi
0x0000000000401008 in _start ()
(gdb) disassemble
Dump of assembler code for function _start:
   0x0000000000401000 <+0>:     mov    0x402000,%rdi
=> 0x0000000000401008 <+8>:     mov    0x402008,%rsi
   0x0000000000401010 <+16>:    add    %rsi,%rdi
   0x0000000000401013 <+19>:    mov    $0x3c,%rax
   0x000000000040101a <+26>:    syscall
End of assembler dump.
(gdb)

Если мы снова введем команду info registers, вы увидите, что для %rdi теперь установлено значение 0x5 или просто 5.

(gdb) info registers
rax            0x0                 0
rbx            0x0                 0
rcx            0x0                 0
rdx            0x0                 0
rsi            0x0                 0
rdi            0x5                 5
rbp            0x0                 0x0
rsp            0x7fffffffdd20      0x7fffffffdd20
r8             0x0                 0
r9             0x0                 0
r10            0x0                 0
r11            0x0                 0
r12            0x0                 0
r13            0x0                 0
r14            0x0                 0
r15            0x0                 0
rip            0x401008            0x401008 <_start+8>
eflags         0x202               [ IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
(gdb)

Подобным образом с помощью команды stepi мы можем пройтись и дальше по всем инструкциям.

Управление точками прерывания

Стоит отметить, что мы можем устанавливать множество точек прерывания. Как было показано выше, для этого применяется команда:

break *ADDRESS

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

break *_start+8

В данном случае устанавливается точка прерывания на 8 байтов после метки _start.

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

Чтобы получить список всех определенных точек прерывания, можно использовать команду info break:

(gdb) info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000401000 <_start>
        breakpoint already hit 1 time
(gdb)

Здесь видно, что в установлена только одна точка прерывания. Стоит отметить поле Num - оно указывает на номер точки прерывания. Так, в данном случае единственная точка прерывания имеет номер 1.

Чтобы удалить точку прерывания, надо выполнить команду:

delete НОМЕР

где НОМЕР — это номер точки прерывания, которую надо удалить. Номер можно получить из таблицы с помощью команды info break, как показано выше

Так, (как показано в информационном разрыве). Обратите внимание, что это не меняет нумерацию точек прерывания. Даже если вы добавите точку прерывания обратно, она будет создана с новым номером.

Если надо добавить точку прерывания внутрь функции, до которой мы еще не дошли командой stepi, то можно дизассемблировать эту конкретную функцию, командой

disassemble MYFUNCTION

где MYFUNCTION — это метка (название функции), который надо дизассемблировать. Вы также можно указать конкретный адрес вместо имени функции (в этом случае перед адресом не применяется префикс *).

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850