Assembler HLA

Instrukcja instalacji HLA

Struktura programu w HLA

program program_Id;

DEKLARACJE

begin program_Id;

OPERACJE

end program_Id;

program_Id to nazwa programu, którą podajemy w pierwszej linii pliku a następnie przy początku instrukcji programu głównego i na samym końcu.

DEKLARACJE - tu znajdują się deklaracje typów, zmiennych, stałych, procedury i inne obiekty programu HLA.

OPERACJE - tu umieszczamy instrukcje programu głównego.

Podobnie jak w C, biblioteki można dołączać instrukcją

#include(“stdlib.hhf”);

Standardowa biblioteka HLA Header File zawiera wiele przydatnych procedur takich jak stdout.put(), stdin.get() i tym podobnych, które poruszymy w dalszej części tego kursu.

Aby napisać pierwszy program "Hello World!" wykorzystamy pojedynczą instrukcję z biblioteki stdlib stdout.put().

program helloWorld;

#include("stdlib.hhf");

begin helloWorld;

stdout.put("Hello World!" nl);

end helloWorld;

// zapisz plik jako helloWorld.hla

/* skompiluj program w konsoli:

$ hla helloWorld.hla

// uruchom jak zwykle:

$ ./helloWorld

Hello World!

$

*/

DEKLARACJE

Co możemy deklarować. Składnia deklaracji wygląda np. tak:

static

W bloku static można przypisać jedynie stałe wartości (nie można przypisać wartości z innej zmiennej lub rejestru). HLA nie zezwala także na jedno liniowe przypisanie wartości do wielu zmiennym (po przecinku).

Wartość boolean jest trzymana w jednym bicie jako 0 lub 1, ale można ja inicjalizować przy pomocy słów kluczowych false i true. Także procedurastdout.put(boolVar) wyświetla wartość boolean jako false lub true.

Definiując typ, można w późniejszym kodzie przypisać go w bardzo prosty sposób:

type

arrPtr:

pointer to int32;

Tab:

arrPtr;

Procedury w HLA mają podobną budową jak cały program.

procedure procedure_Id(initialization_list);

DEKLARACJE

begin procedure_Id;

OPERACJE

end procedure_Id;

Różnicą jest tu słowo kluczowe procedure i możliwość podania listy inicjalizacyjnej w postaci:

Var1:int8; Var2:char...

Wywołanie takiej procedury wygląda podobnie jak w C:

procedure_Id(var_1,var_2);

Podobnie jak w C dwa slashe // rozpoczynają komentarz jednolinijkowy

/* shash i gwiazdka rozpoczyna komentarz wielolinijkowy,

a gwiazdka i slash kończy go */

Rejestry w HLA:

HLA korzysta z 32 bitowego assembly, więc dysponujemy tu 8 rejestrami 32/16/8 bitowymi:

32 bit:

16 bit:

8 bit:

EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP

AX, BX, CX, DX, SI, DI, BP, SP

AL, AH, BL, BH, CL, CH, DL, DH

Jak w NASMie 16 bitowe rejestry są to rejestry na 16 najmłodszych bitach rejestrów 32 bitowych a 8 bitowe to 8 wyższych i 8 niższych bitów czterech ogólnych rejestrów 16 bitowych.

Rejestry ogólne:Rejestry adresowe:

ESI

ESP

EBP

(source index) oraz EDI (destination index) - do operacji na stringach

(stack pointer) - wskazuje na szczyt stosu

(base pointer) - wskazuje na na najgłębsze miejsce na stosie dla danej procedury, w którym znajduje się adres powrotny do procedury wywołującej.

Jako ciekawostkę podam jeszcze:

EFLAGS to 32 bitowy rejestr zawierający flagi procesora na poszczególnych bitach. Ważniejsze flagi dla nas to:

Rejestr EPI (instruction pointer) - to wskaźnik na adres w pamięci zawierający kolejną instrukcję procesora.

OPERACJE

Podstawowe operacje w HLA są takie same jak w NASM. Różnią się jednak zapisem. Argumenty zapisuje się w nawiasach, tak jak wywołanie funkcji, a i kolejność jest odwrotna.

MOV(Źródło,Cel);

Pierwszy argument to źródło (rejestr, zmienna w pamięci, lub stała), a drugi to miejsce docelowe (rejestr, zmienna w pamięci). Zwykłe operacje procesora nie pozwalają na użycie obu argumentów jako zmiennych w pamięci, ale w przypadku operacji MOV w HLA, taka operacja jest automatycznie konwertowana na dwie dokonujące zamierzonego wyniku. Jednakże w przypadku innych operacji tj. ADD() czy SUB(), ta zasada już nie obowiązuje.

HLA nie jest nazywane assemblerem wysokiego poziomu bez powodu. Daje nam ono bowiem operacje tj.: IF, WHILE, REPEAT itp.

Do sterowania tymi trzema operacjami używamy wartości logicznych postaci:

flag_spec

!flagspec

rejestr

!rejestr

boolean

!boolean

operand1 relacja operand2

rejesrt IN DolnaGranica..GornaGranica

rejestr NOT IN DolnaGranica..GornaGranica

flag_spec - to flagi procesora, mamy do nich dostęp przez symbole:

relacja - to wyrażenia definiujące relacje między operand1 (zmienna z pamięci lub rejestr) a operand2 (zmienna z pamięci, rejestr lub stała). Po obu stronach nie mogą być jednocześnie zmienne z pamięci.

= lub ==

<> lub !=

<

>

<=

>=

IN DolnaGranica..GornaGranica to operator badający czy rejestr znajduje się w zbiorze domknieym <DolnaGranica, GornaGranica>

NOT IN DolnaGranica..GornaGranica jak można się domyślić sprawdza czy jest z poza zakresu, czyli czy znajduje się w (-∞, DolnaGranica) ∪(GornaGranica,+∞)

(oczywiście ∞ jest tu rozumiane jako zakres typu)

if (expr1) then

OPERACJE

elseif (expr2) then

OPERACJE

else

OPERACJE

endif;

while (expr) do

OPERACJE

endwhile;

for (inicjalizacja_zmiennych, warunek_zakonczenia, post_body_operacje) do

OPERACJE

endfor;

repeat

OPERACJE

until(expr);

Działanie tych procedur jest takie jak w C. Pętla repeat until działa tak jak do while co zapewnia wykonanie się kodu z sekcji OPERACJE co najmniej jeden raz.

break;

breakif(expr);

Te komendy pozwalają na przerwanie wykonywania sekcji kodu po napotkaniu break, lub po spełnieniu warunku expr w breakif. Główne zastosowanie tych komend to zatrzymanie pętli forever, która działa jak while(true).

forever

OPERACJE

endfor;

switch(rejestr)

case(const_list1)

OPERACJE

case(const_list2)

OPERACJE

default

OPERACJE

endswitch;

Switch musi zawierać co najmniej jedną etykietę case i może dodatkowo zawierać etykietę default działającą w momencie nie napotkania na żadną pasującą etykietę.

Switch przyjmuje jedynie rejestr, nie można stosować zmiennej z pamięci.

Case może zawierać listę a nie tylko jedną wartość, więc można zapisać:

case(10) lub case(1, 5, 21)

HLA jak przystało na język wysokiego poziomu posiada obsługę wyjątków.

try

OPERACJE

exception (exception_Id1)

OPERACJE

exception (exception_Id2)

OPERACJE

endtry;

Po napotkaniu błędu w bloku operacji w try automatycznie zostanie wykonany kod z bloku exception o exception_Id (unsigned int) odpowiadającym napotkanemu błędowi, bez wykonywania dalszych operacji w bloku try. Po wykonaniu kodu poprawnie przechodzi do wykonywania pierwszej operacji poendtry; Jeśli napotkany błąd nie dopasuje swojego ID do żadnego bloku exception, program zostanie przerwany z wiadomością o błędzie.

Nagłówek "excepts.hhf" definiuje kilka podstawowych exception_ID (przykładowo: ex.ConversionError występujący przy prubie przypisania znaku do zmiennej liczbowej, ex.ValueOutOfRange gdy liczba wychodzi poza zakres zadeklarowanej zmiennej). Jednakże można twożyć nowe w razie potrzeby.

Standardowa biblioteka wejścia/wyjścia HLA:

Biblioteka ta zawiera podstawowe procedury typu put - wypisująca na standardowe wyjście, czy get pobierająca z wejścia standardowego. Ponadto biblioteka zawiera wiele zdefiniowanych symboli takich jak:

wszystkie te znaki znajdują się w przestrzeni nazw stdio, więc muszą być nią poprzedzone np. stdio.tab (w przypadku nl wykorzystywanym jako znak nowej linii w procedurze stdout.put())

Jeśli chcemy coś wypisać coś na konsolę, to nie musimy się już bawić z przerzucaniem danych do wypisania do rejestrów i przerwaniami procesora. Wystarczy, że wywołamy procedurę stdout.put(“string do wypisania z EAX: ”, eax, ”;” nl); a wypisze ona na konsoli podany tekst wraz z zawartością rejestru EAX, średnikiem i przejściem do nowej lini.

Procedura ta ma kilka wersji:

stdout.putiXSize(regX, width, char);

regX

width

char

- to odpowiedniego rozmiaru rejestr, który chcemy wypisać

- ilość miejsca które ma zająć liczba (od -256 do 256)

- jaki znak będzie zajmował nadmiarowe miejsce

Aby wypisać liczbę rzeczywistą w postaci dziesiętnej wystarczy użyć procedury:

stdout.putrX(realX, width, prec)

stdout.puteX(realX, width)

- wyświetla X bitową (32/64/80) liczbę zmiennoprzecinkową zajmującą width miejsca (wliczając znak dziesiętny) z precyzją prec

- wyświetla X bitową (32/64/80) liczbę zmiennoprzecinkową na width miejscach (wliczając e+ znak dziesiętny i wykładnik).

Do czytania ze standardowego wejścia możemy używać procedur:

stdin.getc()

stdin.getiX()

stdin.get(intVal1, intVal2)

- czyta znak z klawiatury (buffora) do eax,

- czyta liczbę całkowitą X bitową do eax,

- wczytuje bezpośrednio do intVal1 i intVal2 dwie kolejne wartości z bufora.

Jako, że procedury get czytają z bufora, to może się zdarzyć, że mając program:

stdout.put(“Wpisz pierwszą liczbę 8 bitową: “);

stdin.geti8();

mov(eax, intVal1);

stdout.put(“Wpisz drugą liczbę 16 bitową: “);

stdin.geti16();

mov(eax, intVal2);

Wpisując przy pierwszym pytaniu 1 234 pierwsze wywołanie geti8 wczyta 1 i od razu geti16 wczyta 234 zamiast czekać na wyświetlenie komunikatu z prośbą i kolejną liczbę.

Można jednak skorzystać z procedury stdin.readLn, która ignoruje pozostałość w buforze i wymaga wczytanie nowej linii od użytkownika, lub stdin.flushInput, która również ignoruje zawartość bufora i wymaga wprowadzenia nowej linii. Konwencja tych procedur jest taka, że readLnjest wywołuje się ją przed czytaniem z bufora, lub flushInput po czytaniu z bufora.

Zadania

    1. Napisz prosty kalkulator (dodawanie, odejmowanie, mnożenie i dzielenie) korzystając z procedur dla każdej operacji.

    2. Napisz program, który na podstawie wpisanego przebiegu samochodu, będzie wyświetlał statystyczny wiek pojazdu (15’000 km na rok), oraz typ oleju który powinien używać (do 150’000 km syntetyk, od 150’001 do 300’000 km półsyntetyk, a powyżej mineralny).

    3. Napisz, na co najmniej dwa sposoby, program, który będzie wczytywał z klawiatury aż do momentu podania przez użytkownika poprawnej 8 bitowej liczby bez znaku.

    4. Napisz program, który na podstawie wprowadzonych odległości między miastami, wypisze sformatowaną tabelkę (macierz) tych odległości.