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.