GFA Bas-sembler

Compilierte GFA-BASIC-3.X-Programme sind von Haus aus schon sehr schnell und erreichen bei durchdachter Programmierung fast die Geschwindigkeit von Programmen, die in C geschrieben sind. Aber leider nur fast, bei längeren Programmen macht sich dieser Zeitunterschied doch negativ bemerkbar. Jetzt aber bietet das GFA-BASIC-3.X-Compiler-Linker-Konzept die Möglichkeit, C-oder auch Assembler-Routinen in GFA-BASIC Programme mit einzubinden. Auf die Möglichkeit, BASIC mit Assembler zu kombinieren, möchte ich im folgenden eingehen.

Programme, die in Assembler geschrieben werden, sind mit Abstand am schnellsten und auch am kürzesten. Dafür ist aber das Programmieren doch erheblich aufwendiger, und auch die Fehlersuche ist nicht gerade einfach. In GFA-BASIC ist es relativ einfach, Programme auszutesten und von Fehlern zu befreien. Hierbei leistet der Interpreter sehr gute Dienste. Erst wenn ein Programm zur Zufriedenheit läuft und fehlerfrei ist, kann es compiliert werden. Auch ist die Lesbarkeit von Programm-Listings in BASIC wesentlich einfacher als bei Assembler- oder C-Listings.

Was liegt jetzt also naher als den groben Rahmen eines Programmes in BASIC zu schreiben, wie z.B. Menüs, Dialoge, Diskettenzugriffe usw.? Und nur die Prozeduren, bei denen es auf Geschwindigkeit ankommt, werden in Assembler geschrieben. Dies könnten z.B. längere komplexere Berechnungen, große Speicherbereiche verschieben usw. sein.

Diese Prozeduren müßten dann allerdings wirklich längere Zeit in Anspruch nehmen oder häufiger aufgerufen werden. Es lohnt nämlich nicht, die Ausführungszeit einer Prozedur von z.B. 10 Sekunden auf eine Sekunde zu senken und diese dann im Programmverlauf nur einmal aufzurufen. Wird diese Prozedur aber einige tausend Mal aufgerufen, macht sich der Zeitunterschied doch schon erheblich bemerkbar. Jetzt kann man natürlich auch sagen: „Was soll diese ganze Zeitschinderei?“. Aber seien wir mal ehrlich, wenn es mehrere Programme für ein bestimmtes Problem gibt, wird man irgendwann immer das schnellste Programm verwenden. Ist ein Programm sogar wesentlich schneller als andere Programme, wird man sogar einen etwas schlechteren Bedienungskomfort in Kauf nehmen. Benutzt man allerdings ein Programm nur einmal im Jahr, spielt die Geschwindigkeit nicht unbedingt die dominierende Rolle. Es sollte also immer ein gesunder Mittelweg zwischen Geschwindigkeit und Komfort des Programmes gewählt werden.

Jetzt gibt es mehrere Möglichkeiten, Assembler-Routinen in GFA-BASIC-Programmen zu benutzen. Auf den INLINE-Befehl oder das Einlesen der Assembler-Routinen als Hex-Werte aus DATA-Zeilen möchte ich hier nicht eingehen. Es gibt nämlich eine wesentlich variablere und elegantere Alternative, die nach meiner Meinung im Compiler-Handbuch nicht ausreichend erklärt ist. Der Linker bietet die Möglichkeit, eigene Object-Dateien (Assembler oder C) zu BASIC-Programmen hinzuzulinken. Es gibt aber einige Klippen, an denen man einen ganz schönen Schiffbruch erleben kann. Wie diese Klippen umschifft werden können, werde ich versuchen zu erklären, wobei GFA-BASIC- und Assembler-Kenntnisse vorausgesetzt werden. Diese Methode eignet sich auch sehr gut, fertige GFA-BASIC-Programme nachträglich durch Assembler-Routinen etwas aufzupeppen.

Einer der wichtigsten Punkte, wenn nicht sogar der wichtigste, ist die Parameterübergabe an die Prozedur(en). Wird im BASIC-Programm eine globale Variable eingeführt, ist sie im gesamten BASIC-Programm bekannt. Sie braucht bei einem Prozeduraufruf nicht extra übergeben zu werden, da das BASIC jederzeit deren Adresse kennt. Bei Assembler-Routinen ist das anders. Diese Routine kennt diese Variablen nicht, woher auch? Deswegen müssen alle in dieser Routine verwendeten BASIC-Variablen entweder als Wert oder als Adresse übergeben werden. Diese Werte oder Adressen werden ausnahmslos auf dem Stack an die Assembler-Routine übergeben, und zwar in der Reihenfolge wie sie im Prozeduraufruf angegeben werden - also von links nach rechts, genau umgekehrt wie bei C.

Da der Stack des 68000-Prozessors word(16 Bit)-organisiert ist, dürfen nur 16-Bit(word)- oder 32-Bit(long)-Werte übergeben werden. Also bloß keine Byte- oder Float-Werte direkt übergeben. Sollen trotzdem Boolsche-Byte-Float- oder String-Variablen verwendet werden, müssen der Adreßoperator ‚*’ oder ‚V:’ angegeben werden, um die Adresse der Variablen zu übergeben. Die Assembler-Routine kann dann über die Adresse die Variable im Speicher direkt ansprechen.

Genauso muß verfahren werden, wenn die Assembler-Routine an das BASIC-Programm Werte zurückliefern soll. Das BASIC-Programm muß für diese Rückgabewerte vor dem Aufruf der Prozedur Variablen einrichten, falls diese nicht schon vorhanden sind, und deren Adressen der Assembler-Routine übergeben. Über diese Adressen kann die Routine die Inhalte der Variablen holen, verändern und wieder zurückschreiben. Danach kann auch das BASIC-Programm auf die geänderten Werte zugreifen. Werden der Assembler-Routine nur Werte übergeben, so sind sie wie lokale Variablen zu sehen. Jede Änderung in der Assembler-Routine wirkt sich im BASIC-Programm nicht aus. An dem praktischen Beispiel wird die Übergabe von Variablen und Werten hoffentlich etwas deutlicher.

Im GFA-BASIC gibt es die sogenannten VAR-Parameter. Hierbei werden von allen Variablen, die hinter dem Schlüsselwort VAR stehen, automatisch die Adressen übergeben. Dieses Verfahren funktioniert in reinen GFA-BASIC-Programmen, auch compiliert, einwandfrei. Bei Prozeduren aus Assembler-Routinen klappt die Übergabe von VAR-Parametern leider nicht. Hierbei wird vom BASIC der Stack nicht korrekt berichtigt, und nach einer bestimmten Anzahl von Aufrufen dieser Prozedur kommt es zu einem Stack-Überlauf, und der ST wirft Bomben. Adressen also nur mit dem Adreßoperator ‚*’ oder ‘V:’ an Assembler-Routinen übergeben.

Ich werde nachfolgend einmal verschiedene Möglichkeiten von Prozedurköpfen und -aufrufen darstellen. Als erstes kommt immer die Prozedur selbst und danach mehrere Aufrufe.

  1. ohne Rückgabewert

    PROCEDURE assem(a&,b%,c%,d&) GOSUB assem(a&,b%,c%,d&) GOSUB assem(5,10,wert%,e&) GOSUB assem(z&/4,e%*2,c%,2)

  2. mit Rückgabewert, wobei der erste und letzte Parameter Werte zurückliefern

    PROCEDURE rueck(a%,b&,c%,d%) GOSUB rueck(*x1,f&,g%,*d&) GOSUB rueck(*a&,10,g%+5,*n%)

Bei den Parametern, die direkte Werte enthalten, können, wie in beiden Beispielen gut zu sehen ist, auch numerische Werte oder Formeln enthalten sein. Rückgabeparameter müssen immer Adressen auf Variablen enthalten. Da Adressen immer 4 Byte (32 Bit) groß sind, müssen im Prozedurkopf an diesen Stellen immer 4 Byte (%) Variablen angegeben werden, selbst wenn die Variable nur vom Type Byte ist.

Es müssen auch immer genauso viel Parameter im Prozeduraufruf stehen, wie im Prozedurkopf angegeben sind. Werden einige Parameter nicht immer benötigt, müssen trotzdem Dummy-Werte übergeben werden.

Die Procedure in GFA-BASIC hat folgenden Aufbau:

PROCEDURE assem(a%,b&,c%)
    $X ass
    '
    Hier können noch BASIC-Befehle folgen, 
    die im Interpreter zwar ausgeführt 
    werden, aber vom Compiler ignoriert werden.
    '
RETURN

Der Prozedurname und der Name der Assembler-Routine können, müssen aber nicht gleich sein. Die Zeichenfolge $X ist ein Befehl für den GFA-BASIC-Linker, und muß immer in der ersten Zeile der Prozedur stehen. Alle nachfolgenden BASIC-Befehle werden zwar im Interpreter ausgeführt, aber vom Compiler ignoriert. Der Linker bindet an dieser Stelle die Assembler-Routine in das BASIC-Programm ein.

Aufbau des Assembler-Programms

Um Assembler-Routinen in GFA-BASIC verwenden zu können, muß der verwendete Assembler Object-Dateien im DR-For-mat erzeugen können. Diese vom Assembler erzeugte Object-Datei kann irgendeinen Namen haben, z.B. OBJECT.O. In diese eine Object-Datei kommen nun alle im BASIC verwendeten Assembler-Routinen. Jede Routine muß mit einem Label beginnen. Dieser Label-Name muß mit dem in der BASIC-Prozedur verwendeten Namen 100% übereinstimmen. Sonst findet der Linker des GFA-BASICs diese Routinen nicht. Die Label-Namen müssen auch am Anfang des Assembler-Programms als global definiert werden. Jede Routine muß am Ende noch mit einem RTS abgeschlossen werden.

Jetzt komme ich zu dem Teil, der wohl die meisten Probleme verursachen kann: der Übernahme der Parameter vom Stack. Der Stack (auch Kellerspeicher genannt) wächst von oben nach unten und wird umgekehrt von unten nach oben verkleinert. Wird ein Wert auf dem Stack abgelegt, wird der Stackpointer (sp) um 2 Byte (Word) oder 4 Byte (Long) dekrementiert. Der Stackpointer steht also immer auf dem zuletzt abgelegten Wert. Bei einem GFA-BASIC-Prozeduraufruf werden die Parameter, wie schon erwähnt, von links nach rechts auf den Stack gelegt. Der Stackpointer müßte demnach auf dem letzten, rechten, Parameter stehen. Da die Rücksprungadresse ins BASIC-Programm vom 68000 noch zusätzlich auf den Stack gelegt wird, zeigt der Stackpointer eben auf diese Rücksprungadresse. Um an den letzten Parameter heranzukommen, muß also ein Offset von 4 zum Stackpointer angegeben werden. Bei allen anderen Parametern ist der entsprechende Offset anzugeben. Hierbei muß nur beachtet werden, daß Word-Parameter nur 2 Byte lang sind.

Ist ein Parameter die Adresse einer Variablen, muß diese Adresse in ein Adreßregister geladen werden. Darüber kann nun der Wert der Variablen geholt und bearbeitet werden. Der geänderte Wert kann wieder in die Variable zurückgeschrieben werden.

Die Assembler-Routinen dürfen die Register A3-A6 und den SP nicht verändern. Diese werden vom BASIC benutzt und vor dem Prozeduraufruf nicht gerettet. Ein Verstoß gegen diese Regel wird nicht unter 2 Bomben bestraft.

In dem vorgestellten Beispielprogramm werden die hier gemachten Erklärungen hoffentlich etwas deutlicher. Aber bevor ich auf das Programm eingehe, möchte ich noch kurz die Einstellungen des Assemblers und des GFA-BASIC-Compilers/ Linkers erläutern.

Beim GFA-Assembler müssen die Menüpunkte „Objectcode erzeugen“ und „Symboltabelle anhängen“ aktiv sein, bevor das Programm assembliert wird. Dies wird bei anderen Assemblern so ähnlich sein, ansonsten ins Handbuch des jeweils verwendeten Assemblers schauen.

Um das GFA-BASIC-Programm zu compilieren, benutzte ich die dem BASIC beiliegende Shell „MENUX“. Das GFA-BASIC-Programm wird ganz normal wie immer ausgewählt. Unter dem Menü „Sets“ wählen Sie den Punkt „C-Object“ an. Hier wird die mit dem Assembler erzeugte Object-Datei eingetragen, z.B. OBJECT.O. Danach kann mit der Taste F10 compiliert und gelinkt werden. Der Linker des GFA-BASIC bindet automatisch die Assembler- Routinen in das GFA-BASIC-Programm ein. Wenn alles richtig läuft und der Linker keine Fehlermeldungen ausspuckt, kommt am Ende ein ablauffähiges Programm heraus.

Das Beispielprogramm

Die beiden Listings (Listing 1 und 2) habe ich gut kommentiert, so daß ich hier nur noch ganz kurz darauf eingehen werde. Das Programm stellt als erstes den noch verfügbaren freien RAM-Speicher fest. Danach wird dieser Speicherbereich mit einem Wert, der in der Variablen „testwert%“ steht, gefüllt. Dann wird dieser Speicherbereich noch einmal kontrollgelesen, um festzustellen ob dieser Wert auch in allen Speicherstellen steht. Zum Schluß wird die Zeit für diesen Test berechnet und auf dem Bildschirm angezeigt. Bei ca. 1,2 Millionen Bytes (ca 300 000 longwords) dauerte der Test im Interpreter 129 Sekunden, als compiliertes reines BASIC-Programm 14 Sekunden und als kombiniertes BASIC-Assembler-Programm 3 Sekunden. An diesen Zeiten sieht man sehr gut, daß compilierte reine GFA-BASIC-Programme schon relativ schnell sind - vor allem, wenn man ältere B ASICs oder BASIC-Programme auf dem „sogenannten“ Industriestandard vergleicht. Aber man sieht auch sehr gut, daß Assembler-Routinen doch um einiges schneller sind. Da das hier vorgestellte Beispiel nur sehr einfach und kurz gehalten ist, wird der Zeitunterschied bei komplexeren Assembler-Routinen noch größer sein.

Zum Schluß möchte ich noch einmal auf die Gefahren beim Arbeiten mit dem Stack und der Parameterübergabe hinweisen. Falls in eigenen Programmen Fehler auf-treten (was laut Murphy garantiert vorkommt), würde ich immer zuerst in diesem Bereich nachschauen.

' ***** Test.1  ***********
' ** Beispielprogramm *****
' ** zum GFA Bas-sembler **
' ** K.-D. Litteck ********
' ** Juli 1991 ************
' ** in GFA-Basic 3.5 ****
'
'
' Variablen Deklarieren ****
~FRE(0)
RESERVE 15000 ! Speicher zurückgeben **
CLS
anfram%=HIMEM+100 ! Anfang freies RAM * 
bildram%=LPEEK(&H44E) ! Ende freies RAM 
' Speicher auf einen durch 4 teilbaren
' Wert
anzb%=(bildram%-anfram%)/4*4
'
PRINT AT(5,1);"Es werden "+STR$(anzb%/4)+" Longwords getestet"
' Neue Anfangsadresse ** 
anfram%=bildram%-anzb%
'
' Der Testwert *** 
testwert%=&HFFFF0000 
t=TIHER 
fehler%=0 
adr%=anfram%
' Speicher beschreiben ***
GOSUB write(adr%,anzb%/4,testwert%)
'
' Kontrollesen ***
GOSUB read(adr%,anzb%/4,testwert%,*fehler) 
GOSUB zeit ! Zeit anzeigen ***
PRINT AT(5,5);"Fehler = "+STR$(fehler%) 
ALERT 1,"Programm beendet",1," OK ",a| 
END
'
' Zeit berechnen ***
PROCEDURE zeit
    t%=(TIMER-t)/200 ! Zeit in Sekunden *
    h%=t%/3600 ! Stunden ***
    t%=t%-(h%*3600) ! Stunden abziehen **
    m%=t%/60 ! Minuten ***
    t%=t%-(m%*60) ! Minuten abziehen ***
' Die nächsten 5 Zeilen sind 1 Befehl * 
    t$=LEFT$("00",2-LEN(STR$(h%)))+":"+LEFT$("00",2-LEN(STR$(m%)))+STR$(m%)+":"+LEFT$("00",2-LEN(STR$(t%)))+STR$(t%)
    PRINT AT(5,3);"Zeitdauer = "+t$
RETURN
'
PROCEDURE write(adr%, anzb%, testwert%)
	$X write ! Assemblerroutine ***
	' Dies wird vom Compiler ignoriert **
	FOR i%=1 TO anzb%
		LONG{adr%}=testwert%
		ADD adr%, 4 
	NEXT i%
RETURN
'
PROCEDURE read(adr%,anzb%,testwert%,fehler%) 
    $X read ! Assemblerroutine ***
' Wie bei write ****
	FOR i%=1 TO anzb%
		IF testwert%<>LONG{adr%}
			INC fehler%
		ENDIF 
		ADD adr%,4 
	NEXT i%
RETURN

Listing 1

; OBJECT.O
; Unterprogramm für GFA-Bassembler 
; mit GFA-Assembler 1.5 
; K.-D. Litteck Juni 1991

; Labels als global deklarieren

    .GLOBL write 
    .GLOBL read

    .TEXT

;Unterroutine write 
write:

; Testwert vom Stack holen 
move.l      4(sp),d0
; Anzahl der Longwords vom Stack 
move.l      8(sp),d1
; Startadresse vom Stack 
movea.l     12(sp),a0

long_t:
; Testwert in Speicher 
move.l      d0,(a0)+
; Zähler dekrementieren 
subq.l      #1,d1 
; Ist Zähler 0 ? 
bne.s       long_t

; Rücksprung zum Basic-Programm 
rts

; Unterroutine read 


read:

;adresse von fehler%
movea.l     4(sp),a1 
move.l      8(sp),d0    ;testwert% 
move.l      12(sp),d1   ;anz%
movea.l     16(sp),a0   ;adr%

; d5 als Fehlerzähler löschen 
moveq.l     #0,d5

long:
cmp.l       (a0)+,d0    ; Vergleich 
; Bei Fehler kein Sprung 
beq.s       weiter

addq.l      #1,d5       ; Fehler erhöhen


weiter:
subq.l      #1,d1       ; Zähler dekrement
bne.s       long        ; Zähler 0 ?

; Wert des Fehlerzählers zurück 
; in die Basic-Variable fehler% 
move.l d5,(a1)



rts

; Falls im GFA-Basic Programm noch 
; mehr Assembler-Routinen verwendet 
; werden, so müßen diese hier folgen,
; da der GFA-Basic Linker nur eine 
; Objectdatei verarbeiten kann.

    .END

Listing 2


Klaus-Dieter Litteck
Aus: ST-Computer 06 / 1992, Seite 106

Links

Copyright-Bestimmungen: siehe Über diese Seite