Amiga, asembler i system
Większość kursów programowania w asemblerze na Amidze zupełnie pomija system operacyjny komputera i sposób korzystania z niego. Autorzy skupiają się na samym procesorze, oraz wykorzystaniu układów specjalizowanych bez pośrednictwa systemu, poprzez bezpośrednie programowanie ich rejestrów. Takie podejście jest często nieuniknione przy programowaniu gier i dem scenowych, bo daje największą szybkość. Z drugiej strony w programach użytkowych system operacyjny oddaje nieocenione usługi, z części z nich można też skorzystać pisząc grę czy demo.
Biblioteki systemu
Funkcje systemu operacyjnego Amigi pogrupowane są w biblioteki według zastosowania. Większość podstawowych bibliotek znajduje się w pamięci ROM Amigi, czyli Kickstarcie. Pozostałe znajdują się na dysku twardym lub dyskietce, w zależności od tego, z jakiego urządzenia startuje komputer. Na początek użyjemy dwóch najważniejszych bibliotek systemowych: exec.library i dos.library. Pierwsza z nich jest rdzeniem systemu. Kontroluje wielozadaniowość, uruchamianie procesów, komunikację między procesami, zarządzanie pamięcią i tak dalej. Druga zaś odpowiada za operacje wejścia/wyjścia, a więc dostęp do plików i urządzeń.
Baza biblioteki
W każdym języku programowania dostęp do funkcji biblioteki uzyskujemy przez adres w pamięci, pod którym znajduje się struktura danych zwana bazą biblioteki. Adres jest ten dynamiczny, nie tylko różny w różnych modelach Amig, ale zmieniać się może nawet przy kolejnych uruchomieniach systemu na tej samej Amidze. Adres bazy biblioteki uzyskujemy w momencie jej otwierania. Każdy program przed użyciem biblioteki musi ją najpierw otworzyć. Robi się to przy pomocy funkcji OpenLibrary() z biblioteki exec.library. Wynikiem tej funkcji jest właśnie baza biblioteki.
Wyjątkiem jest tu sama exec.library. Ponieważ to ona służy do otwierania innych bibliotek, sama musi być automatycznie otwarta przez system przy starcie. Inaczej przecież nie moglibyśmy skorzystać z funkcji OpenLibrary(). Zatem adres bazy exec.library znajduje się zawsze pod adresem $00000004 w pamięci.
Baza biblioteki rozciąga się w pamięci zarówno w górę (w stronę mniejszych adresów), jak i w dół (w stronę adresów większych) od swojego adresu. Idąc w dół napotkamy dane biblioteki. Niektóre z nich mogą być publiczne, do wykorzystania w programach, niektóre zaś są prywatne i nie należy w nich grzebać. Idąc zaś w górę napotkamy tablicę skoków do funkcji biblioteki. W tablicy tej znajdują się rzeczywiste adresy w pamięci poszczególnych funkcji. Dzięki tablicom skoków, w różnych wersjach Kickstartu funkcje mogą się znajdować pod różnymi adresami, ale programy działają na wszystkich wersjach bez modyfikacji.
Wywołanie funkcji z biblioteki
Przed wywołaniem funkcji należy w odpowiednich rejestrach procesora umieścić jej argumenty. Rozmieszczenie to jest opisane w dokumentacji systemu, dla każdej funkcji oddzielnie. Najczęściej parametry liczbowe umieszczane są w kolejnych rejestrach danych procesora, parametry będące adresami – w rejestrach adresowych. Następnie w rejestrze A6 umieszczamy adres bazy biblioteki. Musi to być zawsze A6, ponieważ większość funkcji korzysta w swym wnętrzu z tego adresu. Skok do funkcji wykonujemy poprzez tablicę skoków, wykorzystując rozkaz JSR z adresowaniem z przesunięciem względem rejestru A6. Oto przykład pokazujący wywołanie funkcji OpenLibrary:
MOVEQ #34,D0 LEA DosName,A1 MOVEA.L $00000004,A6 JSR -552(A6) ... DosName: DC.B "dos.library",0
Zgodnie z dokumentacją, funkcja OpenLibrary() przyjmuje dwa argumenty. Minimalną wersję biblioteki umieszczamy w rejestrze D0, zaś adres nazwy tej biblioteki w rejestrze A1. Adres jest zwykłym łańcuchem znaków zakończonym zerem, umieściłem go w kodzie programu korzystając z pseudoinstrukcji DC.B. Potem, ponieważ funkcja jest z exec.library, ładuję adres bazy spod stałego adresu $00000004. Ostatnim krokiem jest wykonanie skoku do tablicy skoków. O 552 bajty przed adresem bazy znajduje się instrukcja JMP skoku pod rzeczywisty adres kodu OpenLibrary(). Wykonanie funkcji odbywa się więc następująco:
Definiowanie przesunięć
Fragment kodu w stylu „JSR -552(A6)” jest mało czytelny. Dość oczywistym krokiem jest zdefiniowanie przesunięć od bazy do skoków do poszczególnych funkcji (zwanych żargonowo „offsetami funkcji”) jako stałych, np. w taki sposób:
SysBase = 4 OpenLibrary = -552 MOVEA.L SysBase,A6 JSR OpenLibrary(A6)
Ręczne deklarowanie przesunięć i innych używanych stałych jest wygodne w małych programach, używających raptem kilku funkcji. W większych projektach można skorzystać z systemowych plików nagłówkowych dla asemblera (z rozszerzeniem *.i) zawierających definicje przesunięć funkcji, pól systemowych struktur i inne systemowe stałe.
Zachowywanie zawartości rejestrów
Przyjęta w AmigaOS konwencja traktuje rejestry D0, D1, A0 i A1 jako scratch registers, a więc każda funkcja systemowa może zmienić ich zawartość i pozostawić je w stanie dowolnym. D0 jest zazwyczaj używany jako wynik funkcji. Pozostałe rejestry są niezmienione po wyjściu z funkcji systemowej. Jeżeli funkcja ich używa, przechowuje ich zawartość na stosie procesora i odtwarza przed wyjściem.
„Hello World”
Korzystanie z systemu rozpoczniemy pisząc klasyczny program wypisujący w konsoli tekstowej powyższe zdanie. Do jego wypisania użyjemy funkcji PutStr() wypisującej wskazany tekst na standardowe wyjście programu. Funkcja ta znajduje się w dos.library, więc dojdzie do tego jej otwarcie i zamknięcie. Zdefiniujmy przesunięcia używanych funkcji:
SysBase = 4 OpenLibrary = -552 CloseLibrary = -414 PutStr = -948
A oto i program w całej okazałości, liczy sobie 14 rozkazów procesora.
LEA DosName,A1 MOVEQ #36,D0 MOVEA.L SysBase,A6 JSR OpenLibrary(A6) TST.L D0 BEQ.S NoDos MOVE.L #Hello,D1 MOVEA.L D0,A6 JSR PutStr(A6) MOVEA.L A6,A1 MOVEA.L SysBase,A6 JSR CloseLibrary(A6) NoDos: CLR.L D0 RTS DosName DC.B "dos.library",0 Hello DC.B "Hello World!",10,0
Pierwsze 4 rozkazy to otwarcie dos.library. Potem sprawdzam, czy otwarcie się udało. Jeżeli nie (zero w D0), przeskakuję od razu do wyjścia z programu. Jako minimalną wersję biblioteki podaję 36 (Kickstart 2.0), bo w tej wersji pojawiła się funkcja PutStr(). Próba uruchomienia na Amigach z wcześniejszym Kickiem, spowoduje po prostu wyjście z programu po przeskoczeniu do etykiety NoDos.
Kolejny blok to wywołanie PutStr(). Biblioteka dos.library jest nieco nietypowa, bo nawet argumenty będące adresami (tu adres tekstu do wypisania) są przekazywane w rejestrach danych. Bazę biblioteki dos.library po prostu przemieszczam z D0 do A6.
Blok trzeci to zwolnienie zasobów, czyli zamknięcie dos.library. Biblioteki exec.library nie trzeba zamykać, bo i też nie musieliśmy jej otwierać. Ostatnim krokiem jest wyzerowanie D0. To co pozostawimy w tym rejestrze przy wyjściu z programu, zostanie przez system zinterpretowane jako wynik wykonania programu, wartość zero oznacza wykonanie bez błędów.
Ponieważ skorzystaliśmy do wydrukowania tekstu ze standardowego wyjścia, program poprawnie współpracuje np. z przekierowaniem wyjścia do pliku. Np. wywołanie go w taki sposób
hello >RAM:pff.txt
umieści nam tekst „Hello World!” w pliku RAM:pff.txt.
Słów kilka na temat łańcuchów tekstowych. System AmigaOS używa (w większości) łańcuchów zakończonych bajtem $00, tak jak język C. Jednak w przeciwieństwie do C, kończące zero musimy w deklaracji DC.B wpisać jawnie. W łańcuchu wypisywanym na wyjście dodatkowo dodałem znak końca linii (kod ASCII 10).
Uwaga na koniec
Powyższy program co prawda poprawnie wykona się w oknie CLI, ale gdy dodamy mu ikonę i spróbujemy uruchomić spod Workbencha, prawdopodobnie zawiesi się. Brakuje mu bowiem tak zwanego kodu startowego, czyli poprawnej obsługi uruchomienia z WB. Gdybym ją dodał, program stałby się kilkakrotnie dłuższy i trudniejszy do przeanalizowania.