Набор компиляторов 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 — это метка (название функции), который надо дизассемблировать. Вы также можно указать конкретный адрес вместо имени функции (в этом случае перед адресом не применяется префикс *).