|
|||||||
Как отлаживать программы без исходного кода в GDB
Время создания: 15.06.2023 10:13
Автор: Xintrea
Текстовые метки: linux, gdb, отладка, debug, nasm, ассемблер, assembler, исходный код, main, машинный код
Раздел: Компьютер - Программирование - Отладчик GDB
Запись: xintrea/mytetra_syncro/master/base/1686813203st8hk502a3/text.html на raw.github.com
|
|||||||
|
|||||||
Бывают ситуации, когда в Linux надо отладить не программу на языке C/C++, а бинарник, созданный с помощью ассемблера NASM. То есть, когда нет высокоуровнего исходного кода, и отладку надо производить, по сути, в машинных кодах. Предполагается, что для отладки используется отладчик GDB. Выяснение точки входа Если программа написана на NASM, то в ней обычно даже нет функции main(). Есть просто точка входа - то есть адрес, с которого запускается код программы после того, как она будет размещена в памяти. Для выяснения адреса точки входа,надо выполнить следующие действия. Вначале надо запустить сам GDB указав ему отлаживаемую программу: gdb hello.elf Далее, уже в консоли GDB, следует напечатать команду: > info files Symbols from "/home/xi/work/develop/assembler/00001_sys_call/hello.out". Local exec file: `/home/xi/work/develop/assembler/00001_sys_call/hello.out', file type elf64-x86-64. Entry point: 0x401000 0x0000000000401000 - 0x000000000040101d is .text 0x0000000000402000 - 0x000000000040200e is .data Здесь интерес представляет строка Entry point. В ней написан адрес старта программы. Установка брекпоинта на первую инструкцию программы Зная адрес старта программы, можно установить на нее брекпоинт: > b *0x401000 (полная команда - break *0x401000) Особенность синтаксиса команды - это указание перед адресом звездочки *. Если этого не сделать, то адрес будет считаться просто именем функции, и gdb выдаст запрос: Function "0x401000" not defined. Make breakpoint pending on future shared library load? (y or [n]) n Если видна такая ошибка, стоит проверить, не забыта ли в команде звездочка * перед шестнадцатеричным адресом. Проверить, что брекпоинт установился, можно командой: > info b Num Type Disp Enb Address What 1 breakpoint keep y 0x0000000000401000 <_start> (полная команда - info breakpoints) Запуск программы и просмотр исполняемых инструкций Запуск программы осуществляется следующей командой: > r (полная команда - run) Программа будет запущена, и тут же остановится на первой инструкции: Starting program: /home/xi/work/develop/assembler/00001_sys_call/hello.out Breakpoint 1, 0x0000000000401000 in _start () Так как высокоуровнего исходного кода на C/C++ нет, то команда получения листинга кода программы list работать не будет. Вместо нее придется использовать просмотр машинных инструкций. Для просмотра машинных инструкций используется команда просмотра участка памяти x. Эта команда умеет выводить участок памяти в нескольких форматах, в том числе доступен формат дизассемблирования участка памяти. В качестве параметра данная команда принимает адрес начала выводимого участка памяти, и для его задания можно воспользоваться значением program pointer (PC), так как он указывает на текущую исполняемую команду, на которой произошел останов. Значение program pointer и регистра IP - это, по сути, одно и то же. Поэтому в качестве значений параметра команды x можно указывать как $rip так и $pc. Следующая команда покажет 5 ассемблерных инструкций. Первая инструкция - это та, которая выполняется в данный момент: > x/5i $pc => 0x401000 <_start>: mov $0xe,%edx 0x401005 <_start+5>: mov $0x402000,%ecx 0x40100a <_start+10>: mov $0x1,%ebx 0x40100f <_start+15>: mov $0x4,%eax 0x401014 <_start+20>: int $0x80 По-умолчанию используется синтаксис AT&T, в котором в командах mov источник находится слева, а приемник - справа, в отличие от синтаксиса INTEL, где все наоборот. Пошаговое выполнение инструкций Классические команды step (продолжение выполнения программы с входом вовнутрь вызываемых функций) и next (продолжение выполнения программы без входа в функции), для прохода по машинным инструкция работать не будут. Вместо них следует использовать команды перехода на следующую инструкцию nexti (или ni) и stepi (или si). > ni 0x0000000000401005 in _start () Пра каждом шаге показывается адрес команды, на котором произошел останов. Для просмотра состояния всех основных регистров можно использовать команду: > info registers rax 0x0 0 rbx 0x1 1 rcx 0x402000 4202496 rdx 0xe 14 rsi 0x0 0 rdi 0x0 0 rbp 0x0 0x0 rsp 0x7fffffffdfe0 0x7fffffffdfe0 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 0x40100f 0x40100f <_start+15> eflags 0x202 [ IF ] cs 0x33 51 ss 0x2b 43 ds 0x0 0 es 0x0 0 fs 0x0 0 gs 0x0 0 В данной таблице указывается имя регистра, его шестнадцатеричное значение и примечание. В примечании может, например, отображаться десятеричное значение, или что из себя представляет адрес, какое имеет смещение от известной точки. Просмотреть значение одного конкретного регистра можно командой print (сокращенно p): > print/x $rcx $1 = 0x402000 Суффикс /x означает, что значение надо выводить в HEX-представлении, иначе значение будет стремиться отображаться в десятеричном (DEC) виде. Примечание. При печати единичных значений, каждому такому значению присваивается индексный номер, который пишется в начале строки после символа доллара $. Он нужен для того, чтобы можно было использовать результат работы команды просмотра значения print в других командах. Например, если нужно распечатать содержимое памяти (через команду x), начиная с адреса со смещением, которое было ранее распечатано как значение какого-то регистра, то для вычисления такого адреса можно указать номер индексного значения, например: > x/8b (0x7 + $1) 0x402007: 0x77 0x6f 0x72 0x6c 0x64 0x21 0x0a 0x00 > x/8c (0x7 + $1) 0x402007: 119 'w' 111 'o' 114 'r' 108 'l' 100 'd' 33 '!' 10 '\n' 0 '\000' Естественно, в качестве индексного значения будет использоваться именно распечатанное значение, а не текущее значение регистра. Кстати, можно просматривать не только базовые регистры, но и, например, регистры SSE: > p $xmm1 $4 = {v8_bfloat16 = {0, 0, 0, 0, 0, 0, 0, 0}, v4_float = {0, 0, 0, 0}, v2_double = {0, 0}, v16_int8 = {0 <repeats 16 times>}, v8_int16 = {0, 0, 0, 0, 0, 0, 0, 0}, v4_int32 = {0, 0, 0, 0}, v2_int64 = {0, 0}, uint128 = 0} Изменение значения регистра "на лету" В ходе отладки программы, имеется возможность поменять значение регистра перед тем, как продолжить выполнение инструкций программы. Для этого служит команда set. Это универсальная команда, которая, в частности, может устанавливать значение регистра: > set $rcx = $1 + 0x3 Просмотр стека вызовов (stacktrace) Так как программа написана на чистом Ассемблере и не имеет классических C/C++ функций, то сделать просмотр стека вызовов функций просто невозможно. Вместо этого можно просматривать ближайшие значения от вершины стека. Для этого нужно знать, что регистр SP (Stack Pointer) всегда указывает на вершину стека. Просмотр десяти ближайших значений на стеке производится командой просмотра памяти x: > x/10gx $sp 0x7fffffffdfe0: 0x0000000000000001 0x00007fffffffe338 0x7fffffffdff0: 0x0000000000000000 0x00007fffffffe371 0x7fffffffe000: 0x00007fffffffe381 0x00007fffffffe393 0x7fffffffe010: 0x00007fffffffe3a6 0x00007fffffffe3ba 0x7fffffffe020: 0x00007fffffffe3d3 0x00007fffffffe40d Здесь 10g означает, что должно быть выведено 10 Giant word (Giant word = 2 * Word = 64 bit). Буква x после g означает, что значения должны выводиться в HEX-виде. Примечание. Для архитектуры x86 и x86_64 в GDB приняты следующие обозначения размеров: b - byte h - halfword (16-bit value) w - word (32-bit value) g - giant word (64-bit value) В остальном работа с чистым машинным кодом не отличается от отладки C/C++ кода. Например, для продолжения выполнения машинных команд с остановкой на следующем брекпоинте, можно использовать классическую команду continue (сокращенно - c). Хорошее описание GDB-команд можно найти в следующей документации: GDB Command Reference |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|