Asembler x86/Funkcje/NASM

Z Wikibooks, biblioteki wolnych podręczników.

Funkcje

GNU As
NASM
Spis treści

Definiowanie[edytuj]

Funkcje w asemblerze definiujemy po prostu jako zwykłe etykiety. Gdy chcemy przeskoczyć w wykonaniu naszego programu do naszej funkcji posługujemy się nazwą etykiety jako argumentem dla odpowiedniej instrukcji modyfikującej rejestr EIP i ew. rejestr CS tak aby razem wskazywały na początek funkcji.

Wywoływanie[edytuj]

jmp[edytuj]

Instrukcja jmp modyfikuje wartość rejestru EIP, zależnie od przekazanego parametru. Istnieją 3 odmiany tej instrukcji:

  • jmp short - korzystamy z niej, gdy procedura, którą chcemy wywołać jest oddalona o 128 bajtów w tył lub do przodu; najszybsza instrukcja z rodziny.
  • jmp near - korzystamy z niej, gdy procedura, którą chcemy wywołać jest w tym samym segmencie co procedura wywołująca (w przypadku płaskiego modelu pamięci - w tym samym programie).
  • jmp far - korzystamy z niej, gdy procedura, którą chcemy wywołać jest w innym segmencie, zmianie ulega również rejestr CS; w przypadku płaskiego modelu pamięci, korzystamy z niej gdy wywoływana funkcja znajduje się poza naszym procesem.

Oto przykład wykorzystania:

etykieta:
    jmp short etykieta

Powyższy przykład byłby wykonywany w nieskończoność, gdyż instrukcja jmp powodowałaby bez przerwy skok do swojego własnego początku.

call[edytuj]

Instrukcja call działa identycznie do instrukcji jmp z tą różnicą, że przed przeskokiem układa na stosie bieżące wartości rejestrów EIP oraz ew. CS tak aby później było można wrócić do miejsca gdzie wykonany był skok przy użyciu którejś instrukcji z rodziny ret. Istnieją dwie główne odmiany instrukcji call:

  • call near - korzystamy z niej, gdy wywoływana procedura jest w tym samym segmencie kodu (w przypadku płaskiego modelu pamięci odnosi się to do tego samego procesu)
  • call far - korzystamy z niej, gdy wywoływana procedura jest poza naszym procesem.

A oto i przykład użycia tejże instrukcji:

   call near funkcja
 ...
 funkcja:
   mov ax bx
   ret            ; w tym miejscu kodu przeskakujemy z powrotem do miejsca wywołania tj. do pierwszej linijki przykładu

przykład[edytuj]

Poniższy program wypisuje na ekranie znak znajdujący się w rejestrze AL procesora. Właściwa funkcja systemowa wyświetlająca znak na ekranie znajduje się w podprogramie _printChar.

;; 
;; Wypisuje zawartosc rejestru AL
;;

segment .data 
msg     db      'B'
        
segment .text
        global  _start

_start: 
        mov     al, 'A'         ; al='A'
        call    _printChar      ; wywołanie podprogramu _printChar

        mov     al, 'C'         ; al='C'
        call    _printChar      ; wywołanie podprogramu _printChar

        mov     al, 0x0a        ; al=0x0a    (znak końca linii)
        call    _printChar      ; wywołanie podprogramu _printChar

; wyjscie z programu  
        mov     eax, 1
        mov     ebx, 5
        int     0x80           ; wywołanie funkcji systemowej 0x80
; KONIEC PROGRAMU

_printChar:
        mov     [msg], al      ; skopiowanie zawartości rejestru AL do bufora msg,
        mov     eax, 4         ; gdyż rejestr AL będący częścią rejestru EAX będzie potrzebny dla wywołania funkcji systemowej
        mov     ebx, 1  
        mov     ecx, msg
        mov     edx, 1
        int     0x80           ; wywołanie funkcji systemowej 0x80 (80h)
        ret                    ; wyjście z podprogramu

Ramki stosu[edytuj]

Ramką stosu nazywamy każdą grupę danych odłożoną na stos powiązaną z jedną funkcją tj. kolejno:

  • argumenty
  • zachowane wartości odpowiednich rejestrów:
    • zachowana wartość EIP
    • ewentualna zachowana wartość CS
    • zachowana wartość EBP (SFP)
  • zmienne lokalne

Obrazowo było to przedstawione w rozdziale Architektura. Jako że w asemblerze kontrolujemy bezpośrednio odkładane na stos wartości, ramka stosu może nie mieć dowolnych z wyżej wymienionych elementów. Mogą to być np. same zmienne lokalne.

Argumenty[edytuj]

Funkcje wymagające do swojego działania określonych argumentów mogą je otrzymywać na kilka sposobów. Najbardziej intuicyjne z nich to przekazywanie przez:

  • rejestry
  • stos
  • określoną lokalizację w pamięci

Spośród tych trzech metod zdecydowanie najszybsze jest przekazywanie argumentów przez rejestry, jednak najpowszechniejszą praktyką przekazywania argumentów jest użycie do tego stosu. Funkcje zakodowane w niemal wszystkich językach wyższego poziomu przekazują argumenty przez stos. Swoją popularność metoda ta zawdzięcza nie swojej wydajności, lecz uniwersalności. Oto przykład obrazujący tę metodę:

   push edx
   push eax
   call near funkcja
   add esp, 8
   ...
 funkcja:
   ...

Zachowane rejestry[edytuj]

Rejestr EIP zostaje wrzucony na stos wtedy, gdy skorzystamy z instrukcji call do przeskoku w miejsce naszej funkcji. W przypadku użycia jmp stos pozostaje bez zmian.<br\ > Rejestr CS zostaje utrwalony na stosie w tych samych przypadkach co EIP, lecz dodatkowo tylko gdy stosujemy skok daleki tj. call far.<br\ > Jeśli chodzi o rejestr EBP możemy go zachować sami ręcznie na stosie w celu odzyskania jego poprzedniej wartości. Rejestr EBP wskazuje na swoją zachowaną wartość i w sam w sobie w obrębie działania funkcji jest niezmienny (w przeciwieństwie do SFP - szczytu stosu, który jest ruchomy) i dzięki temu ułatwione jest manipulowanie w obrębie stosu dzięki niemu. Aby odnieść się do dowolnej wartości na stosie jedynie dodajemy/odejmujemy od rejestru EBP odpowiednie wartości.

Zmienne lokalne[edytuj]

Zmienne lokalne w asemblerze to po prostu dane, które dana funkcja wrzuca na stos w czasie swojego działania, odliczając wyżej wymienione elementy. Dostęp do nich osiągamy poprzez dodawanie odpowiednich wartości do wartości rejestru EBP (o ile wcześniej zaktualizowaliśmy odpowiednio jego wartość) lub odejmując wartości od rejestru ESP co jest nieco utrudnione ze względu na to, że jego wartość jest zmienna w związku z ruchomością szczytu stosu.

Inne języki[edytuj]

Główne problemy[edytuj]

Łącząc użycie asemblera z innymi językami można napotkać na różne problemy zależne od języka. Aby móc tworzyć funkcje możliwe do wywoływania przez inne języki lub móc wywoływać funkcje zakodowane w innym języku musimy dokładnie znać mechanizm wywoływania funkcji oraz stałe elementy ich działania. Poniżej znajdziesz opis struktur funkcji zakodowanych w poszczególnych językach. Nie jest to opis łączenia modułów napisanych w asemblerze z językami wyższego poziomu, gdyż ta tematyka została omówiona w rozdziale Łączenie z językami wysokiego poziomu!


Język C[edytuj]


Wywoływanie funkcji[edytuj]

Aby wywołać funkcję zakodowaną w języku C najpierw musimy przekazać jej argumenty poprzez stos, przy czym przekazujemy je w odwrotnej kolejności (od końca) w stosunku do kolejności przedstawionej w jej prototypie. Dodatkowo po wywołaniu funkcji musimy zwolnić miejsce zajmowane przez argumenty na stosie dodając do rejestru ESP odpowiednią wartość (przesuwając tym samym szczyt stosu i uszczuplając stos). Oto przykładowe wywołanie funkcji C:

 void funkcja(short a, char b);
 ...
 push 0x05
 push 0x0389
 call far funkcja
 add esp 3

Jako że do funkcji przekazaliśmy argumenty o łącznej wielkości 3 bajtów, po wywołaniu funkcji zwalniamy zajęte miejsce przesuwając szczyt stosu.

Struktura funkcji[edytuj]

Funkcje zakodowane w C mają stałą procedurę działania:

  • zachowanie na stosie wartości rejestru EBP i nadanie mu nowej wartości
  • zachowanie na stosie wartości rejestrów z których będzie korzystać nasza funkcja w czasie wykonania
  • wykonanie właściwego kodu funkcji
  • zdjęcie ze stosu wcześniej zapisanych rejestrów
  • zdjęcie ze stosu rejestru EBP

Pisząc własne funkcje w asemblerze możliwe do wywołania z użyciem języka C nie musimy stosować się do powyższego schematu. Jedyne nasze ograniczenie to otrzymywanie argumentów poprzez stos w opisanej wyżej kolejności, jednak jeśli w toku naszej funkcji zmianie ulegną jakieś rejestry bez odzyskania ich wartości początkowej może to doprowadzić do nieprawidłowego działania programu.

Pascal[edytuj]


Wywoływanie funkcji[edytuj]

Pascal tak jak inne języki wyższego poziomu przyjmuje argumenty poprzez stos. W przeciwieństwie do C robi to w kolejności zgodnej z własnym zapisem tzn. od początku do końca. Dodatkowo nie jesteśmy zmuszeni do zwalniania miejsca stosu zajętego przez przekazane argumenty. Tak więc wywołanie funkcji Pascala mogłoby wyglądać tak:

 function funkcja(a:integer; b:byte):integer;
 ...
 push 0x0389
 push 0x05
 call far funkcja

Struktura funkcji[edytuj]

W języku Pascal funkcje działają wg poniższego schematu postępowania:

  • zachowanie na stosie wartości rejestru EBP i nadanie mu nowej wartości
  • zachowanie na stosie wartości rejestrów z których będzie korzystać nasza funkcja w czasie wykonania
  • wykonanie właściwego kodu funkcji
  • zdjęcie ze stosu wcześniej zapisanych rejestrów
  • zdjęcie ze stosu rejestru EBP
  • zwolnienie argumentów ze stosu instrukcją ret

Tak jak w poprzednich przypadkach pisząc funkcje w asemblerze z których będzie można korzystać z poziomu Pascala nie jesteśmy zmuszeni do trzymania się powyższego schematu.