Jak korzystać z gdb debugując kod w asemblerze?

Krótkie wprowadzenie, jak korzystać z gdb do debugowania kodu w asemblerze.

Wykorzystajmy poniższy kawałek kodu:

  ; "#define":
SYS_EXIT  equ 60
ARRAY_SIZE equ 10

global _start


section .bss
  ; dane zainicjalizowane zerami, można czytać i pisać
alignb 16           ; z wyrównanymi adresami CPU może działać szybciej
array_to: resq ARRAY_SIZE

section .data
  ; dane (mogą być już zainicjalizowane), można czytać i pisać
alignb 16
array_from: dq 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

section .rodata
  ; dane, można tylko czytać
extra: db 42          ; 8-bitowa liczba


section .text
  ; kod wykonywalny

_start:
  ; for (i=0; i < ARRAY_SIZE; i++)
  ;  array_to[ARRAY_SIZE - 1 - i] = array_from[i] + 42

  ; Poniżej:
  ; rax = i
  ; rcx = &array_to[ARRAY_SIZE - 1 - i]
  ; r8 = 42

  mov rax, 0                     ; i = 0
  mov rcx, array_to + (ARRAY_SIZE - 1)* 8 ; rcx = &array_to[ARRAY_SIZE - 1] (tę stałą wyliczy nasm)
  movsx r8, byte [extra]         ; r8 = 42 (8bit -> 64bit)
  jmp loop_cond
loop_body:
  mov rdx, [array_from + rax*8] ; rdx = array_from[i]
  add rdx, r8                   ; rdx = array_from[i] + 42
  mov [rcx], rdx                ; array_to[ARRAY_SIZE - 1 - i] = array_from[i] + 42
  inc rax                       ; i++
  lea rcx, [rcx - 8]            ; rcx = --(&array_to[ARRAY_SIZE - 1 - i])
loop_cond:                      ; while (i < ARRAY_SIZE)
  cmp rax, ARRAY_SIZE
  jl loop_body                  ; i traktuję jako signed

  ; exit(0)
  mov eax, SYS_EXIT
  xor edi, edi
  syscall

Kompilujemy z opcjami -g -F dwarf:

nasm -f elf64 -g -F dwarf -o ./prog.o ./prog.asm
ld -o ./prog ./prog.o

Uruchamiamy gdb na naszym programie:

gdb ./prog

Ustawiamy składnię intelową:

set disassembly-flavor intel

Ustawiamy breakpoint na etykiecie _start:

breakpoint _start
Można pisać polecenia skrótowo:
b _start
Poniżej formę skróconą będę pisał po "|".

Uruchamiamy wykonywanie programu:

run | r

Wykonywanie programu zatrzyma się na ustawionym breakpoint – u nas na początku _start.

Deasemblujemy kod:

disassemble | disas
Zobaczymy podobny widok jak ten z objdump. Strzałką zaznaczona jest instrukcja, która jest kolejna do wykonania.

Podglądamy aktualne wartości rejestrów:

info registers | i r

Wykonujemy krok programu:

stepi | si

Patrzymy, że faktycznie wykonaliśmy 1 instrukcję:

disas

Wykonujemy kolejny krok:

si

Podglądamy wartość rejestru r8:

print $r8 | p $r8
Dziesiętnie:
p/d $r8

Podglądamy bajt w pamięci pod adresem extra dziesiętnie:

x/db &extra

Wykonujemy krok:

si

Podglądamy wartość r8:

p/d $r8
Powinno być 42.

Patrzymy, co wykona się następnie (gdzie skoczymy):

si
kika razy. Po 6 razach powinniśmy być przed inc rax.

Patrzymy, co jest pod adresem zapisanym w rcx (wyświetlamy dziesiętnie wartość 64bit):

x/dg $rcx

Patrzymy, jak wygląda aktualnie "tablica" array_to:

p (long long[10])array_to

Dodajmy breakpoint po pętli, przed wywołaniem sys_exit:

b prog.asm:50
w 50 linii programu z pliku prog.asm.

Kontynuujemy wykonanie do kolejnego breakpointu:

continue | c

Patrzymy, czy tablica została poprawnie wypełniona:

p/d (long long[10])array_to

Sprawdzamy, czy rejestry mają oczekiwane przez nas wartości:

i r

Kończymy pracę z gdb:

quit | q

To jest krótki pokaz użycia gdb. Oczywiście, potrafi ono dużo więcej (np. modyfikować wartości rejestrów w czasie wykonywania programu). Kto zainteresowany, odsyłam do Internetu.

Ściągawka poleceń gdb: GDB cheatsheet

Jeśli ktoś używa dużo gdb, polecam PEDA - Python Exploit Development Assistance for GDB.