Ubuntu Light
Transkrypt
Ubuntu Light
Asembler x86 – notatki Język (bardzo) niskiego poziomu Asembler jest językiem programowania niskopoziomowego, co oznacza, że jest bezpośrednio tłumaczony na kod wykonywalny dla procesora. Nie zawiera żadnych „wspomagaczy” pisania programów, automatycznej optymalizacji itp. Jest tłumaczony dosłownie – każda instrukcja kodu źródłowego odpowiada dokładnie jednej instrukcji procesora. Budowa programu asemblerowego Program asemblerowy składa się w zasadzie wyłącznie z deklaracji etykiet, dyrektyw, instrukcji i komentarzy. Nie zawiera żadnych bloków, pętli ani innych konstrukcji programistycznych. Stąd też sama składnia asemblera jest dosyć prosta: • [mnemonik] [argument1], [argument2], […] Instrukcja języka. Argumenty po przecinkach, oddzielone od mnemoniku tabulacją. • [nazwa]: Deklaracja etykiety. Wykorzystywana w charakterze „zmiennej”, również przy instrukcji skoku. Etykiety na dobrą sprawę są adresami – można je traktować jak liczby, a więc – celem obliczenia np. długości zmiennej – odejmować. Aktualne położenie w kodzie można pobrać z etykiety z samą kropką w nazwie. • .[rodzaj] [argument1], [argument2], […] Dyrektywa. Zastosowań ma całkiem sporo, m.in. deklaracja danych, określenie punktu startowego programu, rozgraniczenie bloków danych i instrukcji, zapewne jeszcze trochę innych. • #[tekst] Komentarz. Rodzajów dyrektyw jest dosyć sporo. Zazwyczaj, choć nie zawsze, przyjmują argumenty. Dyrektywy deklarujące zmienne mogą przyjmować dowolną ilość argumentów – wtedy deklarujemy tablicę. • .equ – deklaruje stałe. Ma dwa argumenty – nazwę i wartość. • .data – określa miejsce rozpoczęcia bloku danych. • .space – przestrzeń. Ma dwa argumenty – długość oraz bajt, którym zostanie wypełniona. • .byte – jeden bajt. • .word – dwubajtowa wartość całkowita. • .long – czterobajtowa wartość całkowita. • .double – czterobajtowa wartość zmiennoprzecinkowa. • .ascii – ciąg znaków niezakończony zerem. • .asciz – ciąg znaków zakończony zerem. • .string – ciąg znaków zakończony zerem. Nie wiem, czy różni się czymś od .asciz. • .text – nie wiem, co to jest, ale nie ma argumentów i zawsze występuje przed .global. • .type – deklaruje typ symbolu. Między innymi deklaruje, że dany symbol jest nazwą funkcji. Potrzebne do oznaczania funkcji, które wywołamy np. z poziomu języka C. Najczęściej wykorzystywana w postaci .type funkcja, @function. • .global – określa etykietę, od której zaczynamy wykonanie programu. Standardowo ustawiamy _start przy kombinacji narzędzi as i ld, zaś main przy wykorzystaniu GCC. Odwoływanie się do danych W asemblerze każdym rodzajem danych można się posłużyć praktycznie w dowolny sposób – zinterpretować jako dane lub jako wskaźnik. Nie ma też żadnego sprawdzania typów ani długości tablic. Sposoby odwołań: • $[wartość] – adres zmiennej lub wartość stałej. • [wartość] – wartość zmiennej. • %[nazwa] – rejestr procesora. • [przesunięcie]([początek], [indeks], [rozmiar komórki]) – adres. Tą składnię można wykorzystać od np. pobierania konkretnego elementu z tablicy. Generalnie wzór na zwracany adres jest taki: [początek] + [rozmiar komórki] * [indeks] + [przesunięcie]. Instrukcje Instrukcji asemblera jest dosyć sporo – tyle, ile instrukcji procesora. Zestawiam te, które pojawiły się na laboratoriach (Argumenty dla skrócenia będę oznaczał przez #numer): • Arytmetyczne ◦ ADD, ADDB, ADDL – dodaje #2 i #1. Wynik zapisany pod adresem #2. ◦ DEC, DECL – zmniejsza #1 o 1. ◦ DIV – dzieli %al przez #1. Wynik zapisuje do %ax. ◦ CMP, CMPB, CMPL – porównuje #1 i #2. Wynik zapisany przez modyfikację flag. ◦ INC, INCL – zwiększa #1 o 1. ◦ MUL, MULL – mnoży %al/%ax przez #1. Wynik zapisuje do %ax/%eax. ◦ SUB, SUBB, SUBL – odejmuje #1 od #2. Wynik zapisany pod adresem #2. • Logiczne ◦ AND, ANDB, ANDL – koniunkcja #2 i #1. Wynik zapisany pod adresem #2. ◦ OR, ORB, ORL – alternatywa #2 i #1. Wynik zapisany pod adresem #2. ◦ XOR, XORB, XORL – alternatywa wykluczająca #2 i #1. Wynik zapisany pod adresem #2. ◦ SHL, SHR – przesunięcie bitowe w lewo/prawo wartości #2 o #1 bitów. Wynik zapisany pod adresem #2. • Skoki ◦ CALL – wywołuje etykietę jak funkcję, po napotkaniu RET wraca i kontynuuje. ◦ J[warunek] – cała seria skoków warunkowych. Skacze do etykiety #1, jeżeli warunek jest spełniony. Wynik poprzednich operacji wczytuje z odpowiednich flag. ▪ JZ, JE, JNZ, JNE – jeżeli zero/równe/nie zero/nie równe ▪ JA, JB, JNA, JNB – jeżeli większe/mniejsze/nie większe/nie mniejsze ▪ JAE, JBE, JNAE, JNBE, jeżeli większe lub równe/mniejsze lub równe/nie większe lub równe/ nie mniejsze lub równe. ◦ JMP – skok bezwarunkowy do adresu w #1. ◦ LOOP – skok do #1, jeżeli rejestr %ecx większy od zera. Zmniejsza o 1 %ecx. ◦ RET – wyskok z funkcji. • Systemowe ◦ INT – wywołuje przerwanie systemowe o numerze w #1. • Transferowe ◦ LODS, LODSB, LODSW, LODSL – pobiera kolejny znak (element tablicy?) z %ds/%ds:%si do %al/%ax/%eax. ◦ MOV, MOVB, MOVW, MOVL – przenosi wartość #1 pod adres #2. ◦ POP, POPL – zdejmuje ze stosu i przenosi do #1. ◦ PUSH, PUSHL – odkłada #1 na stos. ◦ XCHG, XCHGL – zamienia miejscami #1 i #2. • Pozostałe ◦ NOP – nie robi nic ◦ REP – niestety nie wiem, co to jest. Raz tylko się pojawiło, w przykładzie NFS. • Instrukcje koprocesor zmiennoprzecinkowego x87 ◦ FADD – wykonuje działanie stos(0) = stos(0) + #1. ◦ FADDP – wykonuje działanie stos(1) = stos(1) + stos(0), zdejmuje stos(0). ◦ FCOMI – porównuje stos(0) oraz #1. Wynik zapisywany we flagach procesora. ◦ FDIV – wykonuje działanie stos(0) = stos(0) / #1. ◦ FDIVP – wykonuje dzielenie stos(1) = stos(1) / stos(0), zdejmuje stos(0). ◦ FIDIV – wykonuje działanie stos(0) = stos(0) / #1 na liczbach całkowitych. ◦ FILD – ładuje liczbę całkowitą z #1 na stos(0). ◦ FIMUL – wykonuje działanie stos(0) = stos(0) * #1. ◦ FINIT – inicjalizacja koprocesora zmiennoprzecinkowego. ◦ FISTP – konwertuje stos(0) na liczbę całkowitą, zapisuje do #1 i zdejmuje ze stosu. ◦ FLDL – ładuje liczbę zmiennoprzecinkową z #1 na stos(0). ◦ FLDPI – wczytuje п na stos. ◦ FSIN – wykonuje działanie stos(0) = sin(stos(0)). ◦ FSTP, FSTPL – zapisuje stos(0) do #1. Zdejmuje stos(0). ◦ FSQRT – wciąga pierwiastek kwadratowy ze stos(0). Nadpisuje stos(0). ◦ FSUB – wykonuje działanie stos(0) = stos(0) - #1. ◦ FSUBP – wykonuje działanie stos(1) = stos(1) - stos(0), zdejmuje stos(0). Rejestry procesora Operacje wymagające argumentów pobierają dane ze ściśle określonych rejestrów procesora. Każdy z rejestrów ma swoje przeznaczenie, jak i określoną długość. Niektóre rejestry umożliwiają odwoływanie się do ich fragmentów, np.: do dwóch młodszych bajtów rejestrów %e_x możemy się odwołać przez %_x, do najmłodszego bajtu poprzez %_l, a do drugiego bajtu od końca przez %_h. Oto lista 32-bitowych rejestrów ogólnego przeznaczenia: • %eax (32) – akumulator. Przechowuje wyniki wielu operacji. ◦ Podrejestry: %ax (16), %ah (8), %al (8) • %ebx (32) – rejestr bazowy – służy do adresowania. ◦ Podrejestry: %bx (16), %bh (8), %bl (8) • %ecx (32) – rejestr licznikowy – wykorzystywany jako licznik pętli. ◦ Podrejestry: %cx (16), %ch (8), %cl (8) • %edx (32) – rejestr danych ◦ Podrejestry: %dx (16), %dh (8), %dl (8) • %esp (32) – wskaźnik na wierzchołek stosu. ◦ Podrejestry: %sp (16) • %ebp (32) – rejestr bazowy – służy do adresowania. ◦ Podrejestry: %bp (16) • %esi (32) – rejestr źródłowy – trzyma źródło łańcucha danych. ◦ Podrejestry: %si (16) • %edi (32) – rejestr docelowy – trzyma cel łańcucha danych. ◦ Podrejestry: %di (16) Funkcje systemowe Linuksa Z assemblera często wywołujemy funkcje systemowe Linuksa, z pomocą których operujemy na plikach, wypisujemy dane na ekran, wczytujemy je itd. Ich wykorzystanie wymaga umieszczenia kodu funkcji w rejestrze %eax, ewentualnych parametrów w rejestrach %ebx, %ecx, %edx, %esi oraz%edi, a następnie wywołania przerwania systemowego o numerze 0x80. Oto lista tych funkcji wraz z opisami ich wywołania: • 0x01 – sys_exit Zamyka program. %ebx: kod zakończenia programu • 0x03 – sys_read Wczytuje dane z pliku lub strumienia. %ebx: deskryptor pliku %ecx: wskaźnik na bufor %edx: ilość bajtów do wczytania • 0x04 – sys_write Zapisuje dane do pliku lub strumienia. %ebx: deskryptor pliku %ecx: wskaźnik na bufor %edx: ilość bajtów do wczytania • 0x05 – sys_open Otwiera plik. %ebx: wskaźnik na nazwę pliku %ecx: flagi %edx: tryb • 0x06 – sys_close Zamyka plik. %ebx: deskryptor pliku • 0x08 – sys_creat Tworzy plik. %ebx: wskaźnik na nazwę pliku %ecx: tryb • 0xa2 – sys_nanosleep Wstrzymuje wykonanie na okres czasu. %ebx: przespany okres czasu %ecx: jakiś wskaźnik – na zajęciach wstawialiśmy tam zero Konstrukcje W tej sekcji trochę informacji o tym, jak pisać poszczególne elementy. Szablon programu Poniższy kod jest szablonem pustego programu. Zawiera on wszystkie deklaracje stałych, które się pojawiały, dlatego też należy wybrać tylko te, które są potrzebne. W przypadku korzystania z GCC należy zmienić etykietę startową na nazwę pisanej funkcji (main, jeżeli funkcja jest całym programem i nie będzie wywoływana z funkcji napisanej w C). # Deklaracje stałych .equ kernel, 0x80 # Funkcje systemowe Linuksa .equ stdin, 0x00 # Standardowe wejście .equ stdout, 0x01 # Standardowe wyjście .equ stderr, 0x02 # Standardowe wyjście błędów .equ .equ .equ .equ .equ .equ .equ sys_exit, 0x01 # sys_read, 0x03 # sys_write, 0x04 # sys_open, 0x05 # sys_close, 0x06 # sys_creat, 0x08 # sys_nanosleep, 0xa2 Funkcja wyłączająca program Funkcja wczytująca dane z pliku Funkcja zapisująca dane do pliku Funkcja otwierająca plik Funkcja zamykająca plik Funkcja tworząca plik # Funkcja czekająca .equ mode, 0x180 # Atrybuty do tworzenia pliku .equ flags, 0x00 # Atrybuty do otwierania pliku .data # Deklaracja zmiennych .text .global _start # Ustalenie punktu startowego _start: # Kod programu Zamknięcie programu MOVL $exit, %eax MOVL $0, %ebx # lub inna wartość, jeżeli chcemy zwrócić kod zakończnia INT $kernel Sytuacja wygląda inaczej, gdy korzystamy z GCC – w takim wypadku wywołujemy funkcję exit: PUSHL $0 CALL exit Wypisanie tekstu na ekran MOVL MOVL MOVL MOVL INT NOP $sys_write, %eax $stdout, %ebx $_textStart_, %ecx $_textSize_, %edx $kernel Warunki i pętle Do tworzenia warunków i pętli należy wykorzystać instrukcje skoku (wypisane w sekcji „instrukcje”) oraz etykiety. Warunek należy sprawdzić odpowiednią instrukcją arytmetyczną (np. CMP) ew. inną. Natychmiast po tej operacji wywołujemy odpowiedni skok warunkowy. Najlepiej jest umieszczać w kodzie najpierw instrukcje wykonywane, gdy warunek nie będzie spełniony (wtedy nie będzie skoku po instrukcji warunkowej), następnie skok bezwarunkowy do dalszej części programu, a na końcu „bloku warunkowego” etykietę oraz instrukcje wykonywane, gdy warunek jest spełniony. # Sprawdzenie warunku CMP %eax, %ebx JA eax_wieksze # Instrukcje, gdy warunek nie jest spełniony (%eax <= %ebx) JMP kontynuacja eax_wieksze: # Instrukcje, gdy warunek jest spełniony (%eax > %ebx) kontynuacja: # Dalsza część programu, niezależna od warunku Również instrukcje skoku wykorzystuje się do tworzenia pętli. Przydaje się tu też rejestr %ecx w roli licznika. Uwaga – iterujemy w dół, a nie do góry! Poniżej przykład pętli wykonującej się określoną ilość razy: licznik: .long 0 # Instrukcje przed pętlą MOVL $5, %ecx forloop: MOVL %ecx, licznik # Zapisz licznik do zmiennej # Wnętrze pętli. MOVL $licznik, %ecx # Wczytaj licznik ze zmiennej LOOP forloop # Przeskakuje do forloop i zmniejsza %ecx, jeżeli %ecx != 0 # Dalsza część programu, wykonana po zakończeniu pętli W niektórych przypadkach we wnętrzu pętli wymagane będzie wykorzystanie rejestru %ecx, który przechowuje licznik pętli. W takim wypadku musimy na początku pętli przepisać wartość tego rejestru do zmiennej i wczytać ją z powrotem zaraz przed instrukcją LOOP. Alternatywą jest wykorzystanie innej instrukcji skoku do zapętlenia programu. W przypadku, w którym nie znamy ilości iteracji, jest to nawet wskazane. Parametry wywołania Jeżeli korzystamy z GCC, parametry wywołania są dostępne jak parametry funkcji main(int argc, char **argv, char **env), którą stanowi cały nasz program. W pierwszym argumencie mamy więc liczbę parametrów wywołania, dalej są wskaźniki do wszystkich argumentów w tablicy zakończonej bajtem zerowym oraz wskaźniki do zmiennych środowiskowych również w tablicy zakończonej bajtem zerowym. Funkcje Mechanizm funkcji w asemblerze opiera się na wykorzystaniu stosu oraz instrukcji CALL. Odkłada ona na stos adres powrotu z funkcji i wywołuje ją. Napotkana wewnątrz funkcji instrukcja RET zdejmie adres powrotu z funkcji i przekieruje na niego wykonanie programu. Aby zwrócić wartość, zapisujemy ją w rejestrze %al, %ax lub %eax, jeżeli ma cztery bajty lub mniej, zaś wskaźnik do niej, jeżeli ma więcej. PUSHL $2 PUSHL $1 CALL _mojafunkcja _mojafunkcja: # Tworzymy ramkę stosu PUSHL %ebp MOVL %esp, %ebp # Instrukcje funkcji MOVL 8(%esp), %eax MOVL 12(%esp), %ebx # Przenosimy 1 do %eax # Przenosimy 8 do %ebx # Niszczymy ramkę stosu MOVL %ebp, %esp POPL %ebp RET Aby przekazać argumenty do funkcji, odkładamy je na stos zaraz przed wywołaniem instrukcji CALL. Jeżeli wywołujemy funkcję napisaną w języku C, argumenty wrzucamy w kolejność odwrotnej do tej podanej w deklaracji, a po zakończeniu funkcji zwalniamy miejsce na stosie zdejmując z niego argumenty (lub zwiększając wskaźnik stosu o ich rozmiar). Jeżeli zaś w w języku Pascal – w kolejności zgodnej, a miejsca już nie zwalniamy. ================ KOD C ================ void funkcja(int a, int b); ================ KOD Asemblera ================ a: .long 1 b: .long 2 .type funkcja, @function # ... PUSHL $b PUSHL $a CALL funkcja Jeżeli piszemy w asemblerze funkcję, którą potem wywołamy z języka C, we wnętrzu tej funkcji argumenty ustawione są zgodnej z ich kolejnością w deklaracji nagłówka tej funkcji dla C. Należy jednak pamiętać, że na wierzchu stosu jest nie pierwszy argument, a adres powrotu. Aby zwrócić wartość z asemblerowej funkcji, umieszczamy ją w rejestrze %eax przed wyjściem z funkcji.