Wstęp do assemblera (Linux)

Zrozumienie poniższych programów wymaga znajomości rejestrów procesora oraz wywołań systemowych

Pierwsze programy

Program 32 bitowy

Listing 1. Asemblerowe "Hello world!"

; NASM 32 bit: program zapisany w pliku hello.asm

; kompilacja: nasm -felf hello.asm -o hello.o

; linkowanie: ld hello.o -o hello

; linkowanie w systemach 64-bit: ld -m elf_i386 hello.o -o hello


section .text ; początek sekcji z kodem


global _start ; linker ld domyślnie rozpoczyna wykonywanie

; programu od etykiety _start

_start: ; musi ona być widoczna na zewnątrz (global)


mov eax, 4 ; numer funkcji systemowej:

; 4= sys_write - zapisz do pliku

mov ebx, 1 ; numer pliku, do którego piszemy.

; 1 = standardowe wyjście = ekran

mov ecx, tekst ; ECX = adres (offset) tekstu

mov edx, dlugosc ; EDX = długość tekstu

int 80h ; wywołujemy funkcję systemową


mov eax, 1 ; numer funkcji systemowej

; (1=sys_exit - wyjdź z programu)

int 80h ; wywołujemy funkcję systemową


section .data ; początek sekcji danych.


tekst db "Czesc", 0ah ; nasz napis, który wyświetlimy + enter

dlugosc equ $ - tekst ; makro obliczające długość napisu

; (to nie jest zmienna)

Program 64 bitowy

Listing 2.

; wersja NASM na system 64-bitowy (x86-64); kompilacja: nasm -felf64 hello.asm -o hello.o; linkowanie: ld hello.o -o hello; linkowanie: ld -m elf_x86_64 hello.o -o hellosection .text ; początek sekcji kodu.global _start ; linker ld domyślnie rozpoczyna ; wykonywanie programu od etykiety _start ; musi ona być widoczna na zewnątrz (global)_start: ; punkt startu programu mov rax, 1 ; numer funkcji systemowej: ; 1=sys_write - zapisz do pliku mov rdi, 1 ; numer pliku, do którego piszemy. ; 1 = standardowe wyjście = ekran mov rsi, tekst ; RSI = adres tekstu mov rdx, dlugosc ; RDX = długość tekstu syscall ; wywołujemy funkcję systemową mov rax, 60 ; numer funkcji systemowej ; (60=sys_exit - wyjdź z programu) mov rdi, 0 ; RDI - kod wyjścia syscall ; wywołujemy funkcję systemowąsection .data ; początek sekcji danych. tekst db "Czesc", 0ah ; nasz napis, który wyświetlimy dlugosc equ $ - tekst ; długość napisu

Podstawowe operacje

MOV

mov eax, 3 ; zapis 3 do EAX (3 jest natychmiastowym operandem)

mov bx, ax ; zapis wartości z AX do BX

ADD

Instrukcję ADD używamy do dodawania liczb całkowitych.

add eax, 4 ; eax = eax + 4

add al, ah ; al = al + ah

SUB

Instrukcję SUB używamy do odejmowania liczb całkowitych.

sub bx, 10 ; bx = bx - 10

sub ebx, edi ; ebx = ebx - edi

INC i DEC

inc ecx ; ecx++

dec dl ; dl--

MUL i IMUL

Instrukcja MUL jest używana do mnożenia liczb całkowitych bez znaku, a instrukcja IMUL do mnożenia liczb całkowitych ze znakiem. Składnia instrukcji MUL i IMUL jest następująca:

mul source

imul dest, source, source1

Akcja wykonywana przez instrukcje mnożenia zależy od rozmiaru danych, co przedstawiono wTabeli 1.

Tabela 1 Zestawy parametrów i wyniki instrukcji MUL (pierwsze 3 wiersze) i IMUL

mul cl ; AX = AL*CL

mul bx ; DX:AX = AX*BX

mul esi ; EDX:EAX = EAX*ESI

mul rdi ; RDX:RAX = RAX*RDI

imul eax ; EDX:EAX = EAX*EAX

imul ebx,ecx,2 ; EBX = ECX*2

imul ebx,ecx ; EBX = EBX*ECX

imul si,5 ; SI = SI*5

DIV i IDIV

Instrukcje DIV i IDIV wykonują dzielenia na liczbach całkowitych bez znaku (DIV) i ze znakiem (IDIV), wyliczając jednocześnie resztę z dzielenia. Składnia obu operacji jest identyczna:

div source

idiv source

Jeśli operand source jest 8-bitowy, to AX jest dzielony przez operand. Wynik dzielenia całkowitego jest umieszczany w AL a reszta z dzielenia w AH. Jeśli operand jest 16-bitowy, to dzielna znajduje się w DX:AX. Wynik dzielenia całkowitego jest umieszczany w AX a reszta w DX. Jeśli operand jest 32-bitowy, to dzielna jest pobierana z EDX:EAX, wynik trafia do EAX a reszta do EDX. Wynik nie mieszczący się w rejestrze powoduje błąd wykonania.

Przed wykonaniem dzielenia IDIV rejestry DX (EDX) należy zainicjalizować instrukcją cdq. Instrukcja ta rozszerza AX do DX:AX wykorzystując bit znaku z AX.

div cl ; AL = (AX div CL), AH = (AX mod CL)

div bx ; AX = (DX:AX div BX),

; DX = (DX:AX mod BX)

div edi ; EAX = (EDX:EAX div EDI),

; EDX = (EDX:EAX mod EDI)

div rsi ; RAX = (RDX:RAX div RSI),

; RDX = (RDX:RAX mod RSI)

Deklarowanie zmiennych

Informacje o deklarowaniu danych i tematach pokrewnych są dostępne po kliknięciu na poniższe linki:

Pamięć w programie asemblerowym może być zarezerwowana na dwa sposoby. W pierwszym przypadku rezerwujemy miejsce na dane, a w drugim: rezerwujemy miejsce i inicjalizujemy dane. Pierwsza metoda wykorzystuje dyrektywę RESX, gdzie X zastępujemy literą określającą rozmiar obiektu (byte B, word W, double word D, quad word Q, ten bytes T). Druga metoda wykorzystuje dyrektywę DX, gdzie X ma to samo znaczenie, jak w przypadku RESX. Zarezerwowane miejsca w pamięci oznacza się etykietami, które pozwalają w prosty sposób uzyskać dostęp do adresu komórki pamięci jak i do danych tam wpisanych.

L1 db 0 ; byte labeled L1 with initial value 0

L2 dw 1000 ; word labeled L2 with initial value 1000

L3 db 110101b ; byte initialized to binary 110101 (53 dec)

L4 db 12h ; byte initialized to hex 12 (18 in decimal)

L5 db 17o ; byte initialized to octal 17 (15 in decimal)

L6 dd 1A92h ; double word initialized to hex 1A92

L7 resb 1 ; 1 uninitialized byte

L8 db "A" ; byte initialized to ASCII code for A (65)

L9 db 0, 1, 2, 3 ; defines 4 bytes

L10 db "w", "o", "r", ’d’, 0 ; defines a C string = "word"

L11 db ’word’, 0 ; same as L10

L12 times 100 db 0 ; equivalent to 100 (db 0)’s

L13 resw 100 ; reserves room for 100 words

Etykiety używa się na dwa sposoby. Etykieta oznacza adres zmiennej, a więc pełni tą samą rolę co wskaźnik w C. Jeśli etykieta jest umieszczona w nawiasach kwadratowych, oznacza wartość wpisaną pod adresem skojarzonym z etykietą (wyłuskanie w C).

mov al, [L1] ; copy byte at L1 into AL

mov eax, L1 ; EAX = address of byte at L1

mov [L1], ah ; copy AH into byte at L1

mov eax, [L6] ; copy double word at L6 into EAX

add eax, [L6] ; EAX = EAX + double word at L6

add [L6], eax ; double word at L6 += EAX

mov al, [L6] ; copy first byte of double word at L6 into AL

Asembler nie sprawdza, czy etykiety są używane poprawnie, co może być źródłem wielu błędów. Przy próbie wpisywania danych do pamięci należy pamiętać o konwersji typów np.:

mov [L6], 1 ; próba zapisu 1 do L6 - niepoprawna, zwróci błąd operation size not specified

mov dword [L6], 1 ; próba zapisu 1 do L6 - poprawna

Debugger

Pisanie programów w assemblerze i szukanie błędów znacznie ułatwia debugger, który pozawala śledzić wykonanie programu instrukcja po instrukcji. Więcej informacji....

Geany :

Zbuduj -> ustawienia kompilacji:

(nasm "%f" -felf64 -o "%e".o) && (ld -m elf_x86_64 "%e".o -o"%e")

Zadanie 1

Skompiluj przykładowe przykłady, zmodyfikuj je w ten sposób aby pytały o imię, wczytywały je z klawiatury a następnie wypisywały spersonalizowane przywitanie.

Zadanie 2

Napisz program, który wczyta dwie liczby (jedno cyfrowe) doda je do siebie i wyświetli wynik.

Wskazówka: Użyj dwukrotnie sys_read aby wczytać dwa teksty, pierwsze znaki skonwertuj na liczby odejmując kod ASCII '0', dodaj je a potem skonwertuj na napis (można uzupełnić odpowiednio przygotowany tekst) i wypisz.

Zadanie 3

Napisz program który otwiera plik do zapisu (jeżeli go nie ma to go tworzy) i wpisuje do niego tekst np. Twoje imię i nazwisko a następnie go zamyka.

Zadanie 4

Napisz program, który odczyta czas systemowy i wypisze aktualna godzinę.

Wskazówki:

  • Możesz wykorzystać wywołanie systemowe sys_time (13 lub 201).

  • Jako wynik wywołania systemowego otrzymujemy liczbę sekund od 1 stycznia 1970.

  • Reszta z dzielenia przez 10 da nam cyfrę jedności liczby sekund, następnie dzielenie przez 6 da nam cyfrę dziesiątek liczby sekund itd.

    • Przed każdym dzieleniem należy wyzerować EDX/RDX

    • Wygodnie jest przygotować sobie tekst typu

    • db "XX:XX:XX", 0ah

    • i uzupełnić miejsca X odpowiednimi cyframi.