Assembler HLA (II)
Ten przedmiot to jednak programowanie niskopoziomowe. Dlatego też użyjmy HLA jako wsparcia we wczytywaniu i wypisywaniu danych na standardowe wyjście, ale używajmy go jak asemblera.
Przekazywanie wartości przez rejestry
Wiemy, że można wywoływać procedury wraz z listą argumentów. My jednak wolimy używać stosu lub rejestrów do przekazywania argumentów do procedury.
Np. kod:
program cube;
procedure objetoscSzescianu;
begin objetoscSzescianu;
mul(ebx,eax);
mul(ecx,eax);
end objetoscSzescianu;
begin cube;
mov(2,ecx);
mov(3,ebx);
mov(4,eax);
call objetoscSzescianu;
// eax ma naszą objętość czyli wynik 2 x 3 x 4
end cube;
wywoła procedurę o nazwie objetoscSzescianu, która pomnoży zawartość rejestru ebx z zawartością eax, a następnie wynik zapisany w eax pomnoży jeszcze przez ecx i zapisze w eax.
Dzięki temu iż w przekazaliśmy wartości do tych rejestrów procedura wykona operacje na tych wartościach. Po zakończeniu procedury (tak jak napotkaniu ret w NASMie) wracamy do miejsca, w którym została wywołana procedura. I w tym momencie eax zawiera nasz wynik z obliczeń dokonanych w procedurze.
Skorzystajmy jednak z biblioteki stdlib.hhf i napiszmy program, który będzie pobierał z klawiatury parametry do procedury i wyświetlał wynik jej działania.
program cube;
#include("stdlib.hhf");
procedure objetoscSzescianu;
begin objetoscSzescianu;
mul(ebx,eax);
mul(ecx,eax);
end objetoscSzescianu;
begin cube;
stdin.geti32();
mov(eax,ecx);
stdin.geti32();
mov(eax,ebx);
stdin.geti32();
stdout.puti32(ecx);
stdout.put(" x ");
stdout.puti32(ebx);
stdout.put(" x ");
stdout.puti32(eax);
stdout.put(" = ");
call objetoscSzescianu;
stdout.puti32(eax);
stdout.newln();
end cube;
Przekazywanie wartości przez stos
Co jednak jeśli mamy więcej danych niż rejestrów, lub z innych powodów nie chcemy korzystać z rejestrów podczas przekazywania danych do procedury (np. boimy się, że rejestry mogą zmienić swoją zawartość nim procedura odczyta ich zawartość.
Skorzystajmy więc ze stosu:
program cube2_0;
#include("stdlib.hhf");
procedure objetoscSzescianu;
begin objetoscSzescianu; end objetoscSzescianu;
begin cube2_0; end cube2_0;
Po wrzuceniu danych na stos znajdują się one na jego szczycie. Jednak po wywołaniu procedury, na szczyt stosu wrzucane są dane dotyczące adresu powrotnego z procedury bazy stosu itp. W sumie wrzucane są 4 słowa. Dlatego też przy odczytywaniu tych danych musimy czytać z przesunięciem o 4*4 (16). ESP trzyma aktualny wskaźnik na szczyt stosu. Dlatego gdy chcemy odczytać wartość wrzuconą na stos bezpośrednio przed wywołaniem procedury to piszemymov([esp+16],destination); i odpowiednio dla kolejnych wcześniej wrzucanych wartości zwiększając adres o kolejne 4.
Jako, że mamy do czynienia ze stosem wyciągamy dane w odwrotnej kolejności niż je wkładaliśmy.
Np jeśli wrzucaliśmy na stos n wartości a chcemy się dobrać do wartości i to piszemy:
mov([esp+16+(n-i)*4],destination);
Zwracanie wartości przez stos nie ma większego sensu, gdyż po powrocie z procedury te 4 wrzucone prze jej wywołaniem słowa są ściągane i wskaźnik stosu zmniejszany do wartości z przed wywołania procedury. Oznacza to iż pierwsza wartość wrzucona, podczas wykonywania procedury, na stos była pod adresem[esp-20], a kolejne odpowiednio o x*4 dalej. Jednakże, skoro szczyt stosu spadł, to znaczy, że tamta pamięć została zwolniona i nie mamy gwarancji na to, iż dane te nie zostały już nadpisane.
Kolejną rzeczą, którą się zajmiemy jest alokowanie pamięci. Przypuśćmy, że chcemy sobie zrobić własny stos dostępny z dowolnej procedury. Moglibyśmy użyć tablicy (i to też pokażemy), ale czasem nie znamy wielkości pamięci, która będzie nam potrzebna.
Alokowanie tablicy
Zacznijmy więc od zaalokowania pamięci dla tablicy o wielkości pobranej z klawiatury.
program tabAlloc;
#include("stdlib.hhf");
procedure fillTab;
begin fillTab; end fillTab;
begin tabAlloc; end tabAlloc;
Spróbuj wejścia:
4
1
2
3
4
I co otrzymasz? Tak ładnie wypisane:
1 2 3 4
HLA Exception (54) at line 777 in ex_InstallSignals.hla, edx=$00000000
Memory Access Violation
Pamięć na objekty
Teraz zajmijmy się naszym stosem. Będziemy alokować pamięć dla obiektu, który będzie wyglądał tak:
program stos;
#include("stdlib.hhf");
#include("excepts.hhf"); // HLA ma nam ulatwiac zycie ;]
static
stosTop:
uns32 := 0;
procedure pushOnStos;
begin pushOnStos;
push(eax);
push(ebx);
malloc(8);
mov(eax,ebx);
mov([esp+24],eax);
mov(eax,[ebx]);
mov(stosTop,[ebx+4]);
mov(ebx, stosTop);
mov([ebx],eax);
pop(ebx);
pop(eax);
end pushOnStos;
procedure popFromStos;
begin popFromStos;
push(ebx);
mov(stosTop,ebx);
mov([ebx],eax);
mov([ebx+4],stosTop);
pop(ebx);
end popFromStos;
procedure isEmptyStos;
begin isEmptyStos;
xor(al,al);
cmp(stosTop,0);
jnz falsz;
mov(1,al);
falsz:
end isEmptyStos;
begin stos; end stos;
W taki sposób możemy sobie bardzo łatwo tworzyć własne obiekty w asemblerze. Tu używaliśmy HLA, aby pominąć zabawę z wczytywaniem z klawiatury, sprawdzaniem czy wpisano liczbę oraz wypisywaniem. Można to jednak zrobić w analogiczny sposób w NASMie.
Zadania
Dziś jedno, za to trochę trudniejsze:
Napisz quickSorta, ale w taki sposób by oszczędzać zużycie pamięci.
0) pobierz dane do tablicy (n liczb typu int32 pobranych z klawiatury), i wyświetl ją,
1) wrzuć na stos granice 0 i n-1,
2) dopóki stos nie jest pusty:
(a) ściągnij ze stosu przedział tablicy, na którym będziesz operować,
(b) wywołaj na tym przedziale funkcję sortującą*,
(c) podziel przedział w miejscu wskazanym przez funkcję sortującą i wrzuć na stos większy przedział, wróć do (a),
3) wyświetl już posortowaną tablicę.
* - jak w zwyczajnym qs:
0) pobierz element wzorcowy ze środka tablicy,
0)* można zrobić jedną modyfikację i wybrać losowy index w zakresie jako element wzorcowy (poprawia szybkość w złośliwych testach),
1) zamień element wzorcowy z lewą granicą (L),
2) zaczynając od indeksu i = L + 1, oraz j = R dopóki i <= j:
(a) dopuki element o indeksie i jest mniejszy od wzorca to ++i,
(b) dopuki element o indeksie j jest większy od wzorca --j,
(c) zamień i z j, zwiększ i, zmniejsz j i wróć do (a),
3) zamień miejscami elementy o indeksach j oraz L,
4) zwróć j.