Lab 2: extra

Dopowiedzenia

Polecam przeczytać jeszcze raz, na spokojnie, zwłaszcza dzisiejszą część scenariusza -- jest tam wiele ważnych informacji.

Odnośnie instrukcji skoków warunkowych, zwracam Państwa uwagę na ramkę (bo chyba nie zrobiłem tego na zajęciach):

Ważne

Do porównywania liczb bez znaku używa się innych instrukcji skoku niż do porównywania liczb ze znakiem.

Cześć materiałów dostępnych w Internecie (w tym podlinkowanych przeze mnie) dotyczy architektury x86 (32-bitowej), więc opisują instrukcje 32-bitowe. Oczmaywiście, na architekturze x86_64 (64-bitowej) większość z nich (wszystkie?, prawie wszystkie?) mają odpowiedniki 64-bitowe -- prawie zawsze (zawsze?) wystarczy użyć tej samej instrukcji, tylko że z rejestrem 64-bitowym.

Co dokładnie robi instrukcja, jakie argumenty przyjmuje, jakie flagi ustawia, itd, oczywiście najdokładniej opisane jest w dokumentacji Intela (i innych producentów CPU x86_64). Jest tam wykorzystywany następujący format opisu instrukcji, np.: ADD r/m64, imm32 -- oznacza to, że lewym argumentem tej instrukcji ADD może być albo 64bit rejestr, albo 64bit miejsce w pamięci, natomiast prawym 32bit liczba wpisana w kodzie. Oprócz tego znajdziemy słowny opis (z ew. "specjalnymi" właściwościami instrukcji). Np. ta konkretna:

Add imm32 sign-extended to 64-bits to r/m64.

Zadanie A5

Pominęliśmy zadanie A5, rozwiązanie dla zainteresowanych:

  1. Co w powyższy kodzie robi instrukcja xor?:
    Zeruje rejestr ebx.
  2. Po co w powyższych kodach jest instrukcja nop?:
    Nic nie robi, ale dzięki niej adres pod który skacze jne jest wyrównany.
  3. Dlaczego kompilator wybrał do przechowywania zmiennej i rejestr ebx, a nie np. rejestr r10d, którego wartości nie musiałby zachowywać na stosie (zaoszczędziłby dwie instrukcje)?:
    Ponieważ zgodnie z konwencją rejestr ebx nie będzie zmodyfikowany przez funkcję bar, a rejestr r10d mógłby być -- trzeba by było odłożyć na stos wartość rejestru r10 przed wywołaniem funkcji bar i przywrócić jego wartość po powrocie z funkcji. Więc też zużylibyśmy 2 instrukcje.
  4. Dlaczego przed wywołaniem funkcji bar wartość rejestru ebx jest przepisywana do rejestru edi?:
    Ponieważ, 1. argument funkcji, zgodnie z konwencją, jest przekazywany przez rejestr rdi.

gdb

Krótkie wprowadzenie, jak korzystać z gdb do debuggowania 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:

r
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.

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

Składnia AT&T

Czytając różne materiały w Internecie mogą się Państwo spotkać z inną składnią zapisu asemblera -- składnią AT&T, np:

push   %rbp
mov    %rsp,%rbp
mov    $0x0,%eax
movl   $0x0,-0x4(%rbp)
movl   $0x5,-0x8(%rbp)
mov    -0x8(%rbp),%ecx
add    $0x6,%ecx
mov    %ecx,-0xc(%rbp)
pop    %rbp
ret

Zestawienie różnic można znaleźć np. tu.

Najbardzie (chyba) podchwytliwa -- kolejność argumentów: mov eax, 1 = movl $1, %eax. Podchwytliwe to jest przy porównywniu!!

Zwracanie 0 zamiast adresu funkcji

Pan Wojciech (nie mylę imienia?) znalazł ciekawy przykład. Program:

  int* fun(){
  int b;
  return (&b);
}
kompiluje się do:
0000000000000000 :
   0:	31 c0                	xor    eax,eax
   2:	c3                   	ret 
Dlaczego zwracana jest wartość 0?

Odpowiedź można znaleźć na przykład tu:

The compiler has generated the right instructions. What you are doing is not defined by the C standard, so the compiler is free to do whatever it pleases. In this case it seems that GCC pleases to return a null pointer, probably so that your program will fail as fast as possible.
Rzeczywiście, zwracanie adresu zmiennej lokalnej funkcji jest słabym pomysłem.