Vorheriges Kapitel (Assemblerbefehle) |
Der Assemblersprache hängt nach wie vor der Ruf an, daß sich mit ihr nur schwer
lesbarer Spaghetticode erzeugen ließe. Dabei bieten gerade die neueren Assembler
aus den Häusern Borland/Inprise und Microsoft vielfältige Möglichkeiten, ähnlich
komfortabel wie in Hochsprachen zu programmieren (natürlich abgesehen von den
Programmierwerkzeugen, wo man nur noch Bildschirmelemente zusammenschiebt und
der Code dann vom Programm erzeugt wird).
In diesem Kapitel wird es um die Möglichkeiten gehen, die Ihnen TASM bietet, um
sauberen, strukturierten und gut lesbaren Code zu erzeugen.
Es gibt zwei verschiedene Arten von Konstanten: Literalkonstanten und 'echte'
Konstanten.
Mit Literalkonstanten arbeiten Sie immer dann, wenn Sie einen bestimmten Wert
direkt angeben. So ist zum Beispiel in der Anweisung mov ax,34567
die Zahl 34567 eine Literalkonstante oder kurz ein Literal.
Eine 'echte' Konstante wird hingegen irgendwo im Quelltext definiert. Dabei
sieht es so aus, daß einem Symbol (Name) ein Wert zugewiesen wird. Anders als
bei einer Variablenvereinbarung wird aber kein Speicherplatz eingerichtet. Das
bedeutet, daß Sie beruhigt viele Konstanten definieren können, allein durch
diese Definition aber noch keinen Speicher verbrauchen.
Damit die Konstanten einen Sinn erfüllen, müssen Sie irgendwo im Quelltext
(einer Variable oder einem Register) zugewiesen werden. Während der Übersetzung
des Programms werden die Konstanten dann durch ihren Wert ersetzt.
Daraus ergibt sich die Konsequenz, daß der Wert einer Konstanten während der
Laufzeit eines Programms nicht verändert werden kann. Muß der Wert aber während
der Laufzeit verändert werden, so müssen Sie auf Variablen ausweichen.
Konstantenname EQU Wert |
Konstantenname = Wert |
USt = 16 PI = 3.14159 Version EQU 1 |
In den meisten Fällen besteht zwischen den beiden Möglichkeiten kein Unterschied. TASM gibt Ihnen jedoch die Möglichkeit, mit '=' definierte Konstanten im Verlaufe des Programmtextes zu ändern (wohlgemerkt nur im Quellcode, nicht zur Laufzeit).
TASM bietet Ihnen die Möglichkeit, mit Konstanten zu rechnen und Konstanten die
Ergebnisse von Rechnungen zuzuweisen. Die Operanden der Rechnungen müssen
natürlich zur Übersetzungszeit feststehen. Sollte dies nicht der Fall sein, so
müssen Sie auf Variablen ausweichen.
Beispiele:
PI = 3.14159 Durchmesser = 4 Umfang = PI * Durchmesser String db 'Ein kleiner Text',0 StringLaenge = SIZE String |
TASM skaliert Konstanten automatisch entsprechend dem Kontext. Wird die oben
definierte Konstante Durchmesser beispielsweise einem Byte-Register wie AL oder CH
zugewiesen, dann passiert nichts. Bei einer Zuweisung an ein Word-Register wie
AX oder CX wird automatisch eine Erweiterung auf 16 Bit durchgeführt. Umgekehrt
sieht es schon etwas kritischer aus. Definiert man folgende Konstante
BiggerThanByte = 400 |
mov al,BiggerThanByte |
Makros stellen in Assembler eine Möglichkeit dar, eine bestimmte Anzahl Befehle
zusammenzufassen und über einen Namen wie einen eingebauten Assemblerbefehl zu
verwenden.
Eine Makrodefinition wird durch die Worte MACRO und ENDM eingeschlossen. Zur
Verdeutlichung:
MACRO MacroName ... ENDM |
Makros können erst nach ihrer Definition verwendet werden. Die Verwendung läuft analog zu allen anderen Assemblerbefehlen:
mov ax,2 MacroName cmp al,34 jne Nein ... ... Nein: |
Wählen wir ein einfaches Beispiel. In verschiedenen Publikationen zu Assembler wird zum Beispiel dieses Makro verwendet:
MACRO DosInt int 21h ENDM |
Wie Sie unschwer erkennen können, macht dieses Makro nichts anderes, als den
sogenannten DOS-Interrupt mit der Nummer 21h aufzurufen.
Für ein Anwendungsbeispiel ein etwas längerer Codeausschnitt:
DATASEG msg db 'Hallo',13,10,'$' MACRO DosInt int 21h ENDM CODESEG start: STARTUPCODE mov ah,09h mov dx,OFFSET msg DosInt EXITCODE end start |
Wir vereinbaren also im Datensegment die Byte-Variable msg, die einen String
darstellt, der für eine Funktion des Betriebssystem ein zur Ausgabe gültiges
Format besitzt.
Danach wird das Makro definiert. Wie Sie sehen können, ist es TASM relativ
egal, wo die Definition stattfindet. Man könnte eigentlich davon ausgehen, daß
Makros, die ja Programmcode darstellen, auch nur im Codesegment definiert
werden dürften. Warum die Definition an anderer Stelle aber erlaubt ist, wird in
Kürze erklärt.
Mit der Anweisung CODESEG
eröffnen wir das Codesegment.
Nebenbei: sowohl STARTUPCODE als auch EXITCODE sind Makros, die abhängig vom
eingestellten Prozessor-, Sprach- und vor allem Speichermodell automatisch Code
für den Einstieg und das Beenden des Programms erzeugen.
Ins Register AH laden wir den Wert 09h, ins Register DX laden wir die Adresse
des im Datensegment liegenden Strings und mit DosInt schließlich rufen wir den
Interrupt 21h auf, der die in den Registern stehenden Werte interpretiert. Dabei
stellt er anhand der Zahl 09h in AH fest, daß eine Stringausgabe verlangt wird.
Aufgrunddessen erwartet er im Register DX die Offsetadresse des auszugebenden
Strings. Die Funktion gibt ab der in DX stehenden Adresse so lange Zeichen auf
dem Bildschirm aus, bis sie auf das Zeichen '$' trifft. Das hat die logische
Konsequenz, daß Sie mit dieser Funktion keine Strings ausgeben können, die das
Zeichen '$' enthalten. Jetzt wissen Sie, was das Dollarzeichen am Ende der
Initialisierung von msg zu bedeuten hatte. Aber was sollen die beiden Zahlen
davor? Ganz einfach: die Funktion interpretiert sie als ASCII-Codes für Carriage
Return (13) und Line Feed (10), also ein normaler Zeilenvorschub.
Wie funktionieren nun Makros?
An jeder Stelle, an der TASM auf den Aufruf eines Makros trifft, wird eigentlich
nichts anderes durchgeführt als eine Textersetzung. Dabei wird der Name des
Makros durch den Code ersetzt, den es repräsentiert, im obigen Falle also
int 21h
. Nach der Ersetzung sähe der obige Programmcode also so
aus:
STARTUPCODE mov ah,09h mov dx,OFFSET msg int 21h EXITCODE |
Mit den jetzigen Fähigkeiten wären Makros nur in der Lage, häufig wiederkehrende
Codesequenzen zusammenzufassen. Doch Makros können einiges mehr.
So ist es zum Beispiel möglich, Makros mit Parametern zu versehen, die zudem
auch noch als optionale Parameter behandelt werden können.
Es gibt im Video-Bios eine Funktion, die den Videomodus einstellen kann. Diese
wird mit der Funktionsnummer 00 in AH und dem gewünschten Videomodus in Register
AL aufgerufen. Der Standardvideomodus ist Modus 3 mit 25 Zeilen á 80
Zeichen, hellgrauer Schrift auf schwarzem Hintergrund. Wir können jetzt ein
Makro verfassen, daß einen als Parameter übergebenen Videomodus einstellt oder,
wenn kein Parameter angegeben wurde, den Standardmodus einstellt.
MACRO SetVideoMode mode ;setzt den Videomodus mode bzw Modus 003 IFB <mode> mov ax,00003h ELSE xor ah,ah mov al,mode ENDIF int 10h ENDM |
Unser Makro trägt also den Namen SetVideoMode. Der Parameter trägt den Namen
mode. Wie Sie sehen können, hat der Parameter keinen Datentyp wie Byte oder
Word.
Mit der Anweisung IFB <mode>
wird geprüft, ob der Parameter
mode überhaupt vorhanden ist (IFB=IF Blank). Sollte das nicht der Fall sein
(mode ist also 'blank'/leer), dann wird der Standardwert gesetzt. Hier sehen Sie
auch gleich eine Möglichkeit der Optimierung. Statt bei bekannten Werten die
beiden 8-Bit-Teilregister von AX mit zwei mov-Befehlen zu füllen, verwenden wir
nur einen mov-Befehl, der das komplette Register AX füllt.
Für den Fall, daß der Parameter mode nicht leer ist, tritt der Teil hinter ELSE
in Erscheinung. Da hier die Werte nicht bekannt sind, müssen AL und AH einzeln
beschrieben werden. Sie können auch sehen, daß der Parameter mode wie eine
Konstante behandelt wird - die Skalierung erfolgt entsprechend dem Kontext. Wäre
mode größer als 255, dann würde TASM einen Fehler melden, weil der Wert nicht
mehr in AL untergebracht werden kann. Mit ENDIF endet die Abfrage auf BLANK.
Anschließend wird dann noch der Interrupt 10h aufgerufen, der die
Registerinhalte interpretiert.
Im folgenden wird genau geschildert, welcher Code im Falle eines Makroaufrufes mit und ohne Parameter erzeugt wird.
Aufruf ohne Parameter:
DATASEG msg db 'Hallo',13,10,'$' MACRO OutString str ;gibt den String str auf STDOUT aus mov ah,09h mov dx,OFFSET str int 21h ENDM ; MACRO SetVideoMode mode ;setzt den Videomodus mode bzw Modus 003 IFB <mode> mov ax,00003h ELSE xor ah,ah mov al,mode ENDIF int 10h ENDM CODESEG start: STARTUPCODE SetVideoMode OutString msg EXITCODE end start |
CODESEG start: STARTUPCODE mov ax,00003h int 10h mov ah,09h ;Funktionsnummer mov dx,OFFSET msg int 21h EXITCODE end start |
DATASEG msg db 'Hallo',13,10,'$' MACRO OutString str ;gibt den String str auf STDOUT aus mov ah,09h mov dx,OFFSET str int 21h ENDM ; MACRO SetVideoMode mode ;setzt den Videomodus mode bzw Modus 003 IFB <mode> mov ax,00003h ELSE xor ah,ah mov al,mode ENDIF int 10h ENDM CODESEG start: STARTUPCODE SetVideoMode 13h OutString str EXITCODE end start |
CODESEG start: STARTUPCODE xor ah,ah ;Funktionsnummer mov al,mode ;Videomodus int 10h mov ah,09h ;Funktionsnummer mov dx,OFFSET msg int 21h EXITCODE end start |
Wie Sie gesehen haben, habe ich auch noch ein Makro hinzugefügt, das die Ausgabe
eines mit '$' terminierten Strings erledigt. Es sollte jetzt eigentlich klar
sein, wie mit Makros mit einem Parameter umzugehen ist.
Ein Makro mit mehreren Parametern wird folgendermaßen definiert und aufgerufen:
MACRO ManyParams param1, param2, param3 ... IFB <param2> .. ELSE .. ENDIF ... ... ENDM ; ;Im folgenden die Aufrufmöglichkeiten ; ManyParams p1,p2,p3 ;oder ManyParams p1,,p3 |
Sowohl bei der Definition als auch beim Aufruf werden die Parameter durch Komma getrennt. Kann einer der Parameter als optional behandelt werden und beim Aufruf weggelassen, so muß zumindest das Komma gesetzt werden, um anzuzeigen, an welcher Stelle der Parameter ausgelassen wurde. Sollte der erste bzw. letzte Parameter optional sein, so muß die Parameterliste entsprechend mit einem Komma angefangen werden oder mit dem letzen nichtoptionalen Parameter beendet werden.
TASM bietet Ihnen die Möglichkeit, sich sogenannte Makrobibliotheken anzulegen.
Diese Bibliotheken sind einfache Textdateien, die eine Anzahl Makrodefinitionen
enthalten und dann bei Notwendigkeit in die eigentliche Programmdatei eingefügt
werden können.
Im Laufe dieses Tutorials werden noch mehr Makros entstehen und die bereits
erstellten Makros finden noch weitere Verwendung. Deswegen beginne ich bereits
jetzt, mit den Makros OutString und SetVideoMode eine Bibliothek aufzubauen, die
später erweitert und in den folgenden Programmen nur noch eingebunden wird. Die
Makrobibliothek nenne ich imacros.mak (i wie IDEAL). Sie sieht so aus:
;;imacros.mak MACRO OutString str ;;gibt den String str auf STDOUT aus mov ah,09h mov dx,OFFSET str int 21h ENDM ; MACRO SetVideoMode mode ;;setzt den Videomodus mode bzw Modus 003 IFB <mode> mov ax,00003h ELSE xor ah,ah mov al,mode ENDIF int 10h ENDM |
CODESEG INCLUDE "imacros.mak" ... |
Sie können auch einen kompletten oder relativen Pfadnamen angeben.
Ab dem Zeitpunkt der Einbindung sind alle Makros verwendbar.
Falls Sie sich über die doppelten Semikola wundern: TASM kann Ihnen als Resultat
der Kompilierung eine Listingdatei erstellen, in der zu jedem Befehl (Mnemonic)
der Maschinencode aufgeführt ist. Kommentare, die mit einem doppelten Semikolon
eingeleitet werden, werden nicht in die Listingdatei übernommen.
Die Ersetzung der Makros und deren Parameter findet zum Zeitpunkt der
Kompilierung statt. Das bedeutet, daß im Kompilat bereits der vollständig
aufgelöste Makrocode steht.
Da Makros ihren eigenen Aufruf ersetzen, sind sie selbst nicht adressierbar. Es
wäre also unsinnig, mit einem Befehl wie lea dx,SetVideoMode
in das
Register DX die Offsetadresse des Makros zu laden. TASM würde in diesem Fall
auch einen Fehler anzeigen.
Es ist zu bedenken, daß bei jedem Makroaufruf der komplette aufgelöste Makrocode
an der Aufrufstelle eingefügt wird. Ruft man also fünfmal SetVideoMode mit oder
ohne Parameter auf, dann wird fünfmal der dem Aufruf entsprechende Code
eingefügt. Dies hat zur Folge, daß ein Programm, das mehrere Male ein längeres
Makro aufruft, u. U. dramatische Größenzunahmen zu verzeichnen hätte. Eine
tiefere Diskussion dieser Problematik finden Sie im Abschnitt über Prozeduren.
Aufgrund dieser Problematik ist es auch nicht möglich, innerhalb von Makros
Sprungziele festzulegen. Wie Sie wissen, müssen Sprungziele innerhalb eines
Programms einmalig sein. Bei mehrmaliger Codeersetzung sind sie das aber nicht.
Trotzdem haben Sie die Möglichkeit, innerhalb von Makros Sprünge durchzuführen,
wenn Sie die Sprungziele bei der Makrodefinition deklarieren:
MACRO Spruenge LOCAL j1,j2 ;normaler Code j1: .. j2: .. .. ENDM |
TASM kümmert sich selbst darum, daß bei der Kompilierung die Sprungziele in den Makros so aufgelöst werden, daß keine Namensprobleme entstehen.
Abgesehen von STARTUPCODE
und EXITCODE
verfügt TASM
noch über drei weitere eingebaute Makros, die sehr leistungsfähig sind.
Zum ersten wäre da die Blockwiederholung mit REPT
zu erwähnen:
REPT 10 db 0 ENDM |
Dieses Makro erzeugt zehn Bytes, die mit dem Wert 0 initialisiert werden. Es
wird also alles, was zwischen REPT
und ENDM
sooft
ausgeführt, wie es der Parameter von REPT
angibt.
Nun werden Sie sich vielleicht fragen, wo der Sinn dieses Makros liegt, vor
allem, wo man den obigen Effekt besser mit db 10 dup (0)
erreicht
hätte.
Wenn man die Blockwiederholung mit Konstanten kombiniert, dann kann man sich
einiges an Tipparbeit ersparen:
Zaehler = 0 REPT 10 db Zaehler Zaehler = Zaehler+1 ENDM |
db 0 db 1 db 2 db 3 ... db 9 |
IRP
anzuführen:
IRP MeinWert, <0,2,4,8,16,32> db MeinWert ENDM |
Dieses Makro wird sooft wiederholt, wie Werte zwischen den eckigen Klammern
stehen (im Beispiel sechs mal). Beim ersten Durchlauf wird der erste Wert (also
0), beim zweiten Durchlauf der zweite Wert (2) usw. verwendet. Dieser Wert wird
dem ersten Parameter von IRP
zugewiesen. MeinWert erhält also
nacheinander alle Werte in eckigen Klammern.
Im obigen Beispiel wird folgende Liste erstellt:
db 0 db 2 db 4 db 8 db 16 db 32 |
IRP Register, <AX,CX,DI,SI,DS> push Register ENDM |
Natürlich ist auch dieses Beispiel mit TASM unnötig, da TASM von sich aus über die Fähigkeit verfügt, mehrfach zu pushen/poppen.
Die dritte Möglichkeit ist die Blockwiederholung mit IRPC
:
... IRPC Zeichen, ahgtz cmp al, '&Zeichen&' je Enthalten ENDM ... Enthalten: ... |
IRPC
arbeitet so ähnlich wie IRP
, nur daß hier Zeichen
eines Strings verwendet werden. Nacheinander erhält Zeichen also die Werte a, h,
g, t und z. Innerhalb des Makros wird verglichen, ob das Register AL einen Wert
enthält, der Zeichen entspricht. Um die Verwendung von '&' zu erklären, hier
die Expansion des Makros und danach die Expansion, wie sie erfolgen würde,
hätten wir keine '&' eingefügt:
IRPC Zeichen, ahgtz cmp al, '&Zeichen&' je Enthalten ENDM ;wird im ersten Durchlauf expandiert zu cmp al,'a' ; IRPC Zeichen, ahgtz cmp al, 'Zeichen' je Enthalten ENDM ;wird im ersten Durchlauf expandiert zu cmp al,'Zeichen' ;und damit einen Fehler verursachen |
Hätten wir im letzten Beispiel auch noch die Hochkommata entfernt, wäre es im
ersten Durchlauf zu folgender Expansion gekommen:
cmp al,a
Dabei hätte TASM nach einer Konstante oder Variablen mit dem Namen 'a' gesucht.
Wäre dieses Symbol nicht aufzufinden, würde eine entsprechende Fehlermeldung
ausgegeben.
Wie Sie bemerkt haben, verfügen die Blockwiederholungen über keinen eigenen
Namen (nach dem Muster: MehrfachPush IRP Reg,<AX,CX,DI,SI,DS>
).
Sie müssen diese Makros also tatsächlich dort einfügen, wo sie ausgeführt
werden sollen.
Vorwiegend sind diese Blockwiederholungen zum erstellen von Daten oder für
häufig nacheinander auszuführenden Code geeignet.
Denken Sie bei diesen Makros immer daran, daß sie keine Schleife darstellen,
sondern abhängig von den Parametern, die die Anzahl der Wiederholungen vorgeben,
mehrfach in den Quellcode expandiert werden.
Es kann vorkommen, daß sich innerhalb eines Makros die Situation einstellt, daß
Sie das Makro vorzeitig verlassen müssen bzw. können. Diese Möglichkeit wird
Ihnen durch die Anweisung EXITM
zur Verfügung gestellt. Sie ist nur
innerhalb eines Makros gültig und wird einfach an einer Stelle in das Makro
eingefügt, an der ein normaler Assemblerbefehl oder eine Makrobedingung stehen
könnte:
MACRO MitExitM ... ... EXITM ... ... ENDM |
Prozeduren stellen unter TASM die zweite Möglichkeit dar, gleichartige
Codeabschnitte zusammenzufassen und über einen Namen aufzurufen.
Die Syntax einer Prozedurdefinition sieht im IDEAL-Mode folgendermaßen
aus:
PROC name [[language modifier] language] [distance] [ARG argument_list] RETURNS [item_list] [LOCAL argument_list] [USES item_list] Befehle ENDP [name] |
Der Name der Prozedur ist frei wählbar. Es ist allerdings zu beachten, dass
TASM Symbolnamen nur bis zum 32. Buchstaben unterscheidet. Werden also mehrere
Prozeduren definiert, deren Namen sich erst im 33. Buchstaben unterscheiden,
dann wird TASM diesen Fehler bei der Übersetzung melden.
Durch die Definition einer Prozedur wird im Grunde nichts weiter als ein Label
definiert, ähnlich einer Sprungmarke. Wie dieses Label verwendet wird und
welche Möglichkeiten der genaueren Definition es gibt, dazu im folgenden mehr.
Untersuchen wir die einzelnen Bestandteile der Syntax.
Der language modifier weist TASM an, besonderen Prolog- oder Epilog-Code
einzufügen, wenn die Prozeduren mit Windows oder dem VROOM-Overlay-Manager in
Verbindung stehen. TASM stellt die Modifizierer NORMAL, WINDOWS, ODDNEAR und
ODDFAR zur Verfügung.
Sie haben bereits bei der MODEL-Direktive die Möglichkeit, einen language
modifier einzustellen. Diese Einstellung wird automatisch von allen Prozeduren
verwendet, die ihrerseits keinen eigenen Modifizierer einstellen.
Ist kein Modifizierer angegeben, wird automatisch NORMAL verwendet. Diese
Einstellung bewirkt die Erzeugung von Standard-Prolog- und Epilog-Code.
WINDOWS erzeugt Code, der die Prozedur von Windows aus aufrufbar macht. Eine
Voraussetzung dafür ist allerdings, dass es sich um eine FAR-Prozedur handelt
(dazu später mehr). ODDNEAR und ODDFAR werden in Verbindung mit dem
VROOM-Overlay-Manager verwendet.
Höhere Programmiersprachen bieten in vielen Fällen für eine Reihe von Problemen
keine adäquate Lösung. So sind sie teilweise zu langsam, nicht maschinennah
genug oder verbieten bestimmte Dinge einfach. Für solche Fälle, so es die
Programmiersprache anbietet, können Module aus anderen Sprachen eingebunden
werden. So kann z. B. ein schneller Suchalgorithmus in Assembler geschrieben
und dann über einen sprachabhängigen Mechanismus in die höhere
Programmiersprache eingebunden werden.
Die verschiedenen Sprachmodelle unterstützen Sie als Programmierer dabei. Da
viele höhere Sprachen unterschiedlichen Konventionen beim Aufruf von
Unterprogrammen folgen, kann die Codeerzeugung für diesen
schnittstellenspezifischen Teil TASM überlassen werden.
TASM stellt die folgenden Sprachmodelle zur Verfügung: NOLANGUAGE, BASIC,
FORTRAN, PROLOG, C, CPP (für C++), SYSCALL, STDCALL und PASCAL.
Angaben zum Sprachmodell können auch schon in der MODEL-Direktive getätigt
werden. Diese Angabe gilt als Voreinstellung für alle weiteren Prozeduren.
Sollte keine Angabe vorliegen, wird automatisch NOLANGUAGE angenommen. In
diesem Fall müssen Sie eventuell eigenen Prolog/Epilog-Code schreiben.
Abhängig vom von Ihnen verwendeten Speichermodell arbeiten Sie mit einem oder
mehreren Codesegmenten. Dies kann Auswirkungen auf den Aufruf Ihrer Prozeduren
haben. Da der Code einer Prozedur für gewöhnlich an anderer Stelle als der
aufrufende Code steht, muß der Prozessor nach Beendigung der Prozedur wissen,
an welcher Stelle im Codesegment er fortfahren soll. Diese Stelle ist der
Befehl hinter dem Prozeduraufruf. Die Adresse dieses Befehls (Register IP) wird
auf den Stack gepusht und nach Beendigung der Prozedur wieder vom Stack ins
Register IP gepopt.
Befindet sich die Prozedur im gleichen Segment wie der aufrufende Code,
dann kann die Prozedur als NEAR definiert werden. Bei einem NEAR-Aufruf wird
nur das Register IP auf den Stack gepusht.
Befindet sie sich in einem anderen Segment, muss sie als FAR definiert werden.
Die Definition findet über das Element distance statt. Als Voreinstellung dient
das verwendete Speichermodell. So ist in den Modellen TINY, SMALL und COMPACT
die Standardadressierungsart NEAR, in allen anderen FAR.
Argumente können auf zwei Arten an eine Prozedur übergeben werden: über
Register und über den Stack. Darüber, was sinnvoller ist, streiten die
Gelehrten schon seit Jahren. Man kann davon ausgehen, dass der Weg über den
Stack der etwas langsamere ist, da Speicherzugriffe immer langsamer als
Zugriffe auf Register sind. Ab einer bestimmten Anzahl von Argumenten kann man
allerdings in Bedrängnis mit den zur Verfügung stehenden Registern kommen. Da
diese in den Prozeduren ebenfalls verwendet werden, kann man sinnvollerweise
nur auf unbenutzte Register ausweichen, die je nach Aufgabe vielleicht nur
spärlich vorhanden sind.
Für die Übergabe über den Stack bietet TASM einen einfachen Mechanismus an, der
die umständliche manuelle Adressberechnung vermeidet.
Hinter ARG können Sie die Parameter, die Sie an die Prozedur übergeben wollen,
mit einem Namen und einem Datentyp versehen.
Prozeduren die Parameter erwarten, erhalten in aller Regel einen eigenen
sogenannten Stackrahmen. Durch diesen Stackrahmen erhält das Register BP den
Wert der aktuellen Stackspitze. Würde man eine Prozedur ohne Sprachmodell und
mit eigenem Stackrahmen erstellen wollen, müsste man etwa folgendes schreiben:
PROC name push bp ;BP auf den Stack mov bp,sp ;aktuelle Stackspitze nach BP . . pop bp ;BP wieder vom Stack holen ret ;Rücksprung ENDP name |
Wenn die Prozedur Parameter auf dem Stack erwartet, dann könnte das
folgendermaßen aussehen:
MODEL SMALL DATASEG msg DB 'Parameter 1',0 wiederhole dw 1000 CODESEG start: STARTUPCODE push OFFSET msg push wiederhole call WriteX EXITCODE ; PROC WriteX ;einen String mit X Wiederholungen schreiben push bp ;BP auf den Stack mov bp,sp ;aktuelle Stackspitze nach BP mov cx,[bp+4] ;Wiederholungszähler laden mov dx,[bp+6] ;Stringoffset laden . . pop bp ;BP wieder vom Stack holen ret ;Rücksprung ENDP WriteX end start |
So würde üblicherweise (selbstverständlich gibt es auch hier mehrere
Möglichkeiten) der Zugriff auf Parameter auf dem Stack aussehen.
Wie im Kapitel über den Stack angesprochen,
wächst die Adresse der Stackspitze in Richtung kleiner werdender Adressen.
Nachdem BP auf den Stack gebracht wurde, zeigt SP auf die Adresse, an der
BP liegt. Um ohne Stackbefehle auf diese Adresse zugreifen zu können, würde man
sie mit [SP + 0] oder einfach [SP] ansprechen.
Der Stackrahmen wird aufgabaut, damit man eine gleichbleibende Basis zum
Zugriff auf die Parameter hat. Würde man SP zur Adressierung verwenden, dann
müsste man bei jeder Stackveränderung (z. B. durch PUSH oder POP) die aktuelle
Stackspitze neu berechnen, was einfach zuviel Aufwand bedeutet.
Da BP nach Eintritt in die Prozedur den Wert von SP erhält, zeigt also [BP + 0]
auf seinen eigenen ehemaligen Wert. An der Adresse [BP + 2] liegt die
Rücksprungadresse, die durch den CALL-Befehl auf den Stack gebracht wurde. Da
wir uns (dank der Direktive SMALL) in einem NEAR-Speichermodell befinden, wird
nur der Offset-Anteil der Rücksprungadresse (IP) auf den Stack gebracht. In
einem FAR-Speichermodell oder bei einer FAR-Deklaration (die auch einen
FAR-CALL notwendig gemacht hätte) wären 4 Bytes gepusht worden. Die beiden
Parameter wurden vor dem Aufruf der Prozedur gepusht. Der zuletzt gepushte Wert
(der Wiederholungszähler) ist über [BP + 4] adressierbar, der Stringoffset kann
über [BP + 6] angesprochen werden. Unter den Bedingungen eines FAR-CALLs hätten
zu korrekten Adressierung der beiden Parameter noch jeweils 2 Bytes addiert
werden müssen ([BP + 6],[BP + 8]).
Wäre das Programm nach Aufruf der Prozedur nicht beendet gewesen, hätten wir
uns noch um die Bereinigung des Stacks kümmern müssen. BP erhält vor dem
Rücksprung zwar noch seinen ursprünglichen Wert, aber die vor dem
Prozeduraufruf gepushten Parameter werden nicht wieder vom Stack entfernt. Dies
könnte entweder in der Prozedur geschehen oder nach der Rückkehr hinter dem
Aufruf. Der Einfachheit halber würde man in solchen Fällen nicht zu mehrfachen
PUSH-Befehlen greifen, sondern einfach entsprechend dem beanspruchten Platz auf
dem Stack (in unserem Fall 4 Bytes) durch einen Befehl wie add sp,4
die Stackkorrektur vornehmen (das klappt innerhalb der Prozedur nur dann, wenn
die Korrektur nach dem Zurückladen von BP durchgeführt wird).
Durch die erweiterten Möglichkeiten bei der Prozedurdefinition wird einem die
Last mit der manuellen Adressberechnung abgenommen. Die Parameter werden
einfach über einen Namen angesprochen, die eine Adresse reräsentieren, deren
Berechnung vom Assemblierer vorgenommen wird.
MODEL SMALL,PASCAL DATASEG msg DB 'Parameter 1',0 wiederhole dw 1000 CODESEG start: STARTUPCODE call WriteX,OFFSET msg,wiederhole EXITCODE ; PROC WriteX near MsgOfs:word,counter:word ;einen String mit X Wiederholungen schreiben mov cx,[counter] ;Wiederholungszähler laden mov dx,[MsgOfs] ;Stringoffset laden . . ret ;Rücksprung ENDP WriteX end start |
Wie man sieht, hat die Definition der Prozedur eine eher hochsprachenähnliche
Form angenommen. Die erweiterte Definition ist nur in Verbindung mit einem
Sprachmodell möglich. Die Angabe von Parametern kann auch ohne ARG erfolgen.
Was im Hintergrund passiert ist folgendes:
Abhängig vom Sprachmodell werden die Parameter auf den Stack gepusht. Beim hier
festgelegten PASCAL-Sprachmodell werden die Parameter von links nach rechts in
der Reihenfolge ihres Auftretens auf den Stack gepusht, also zuerst MsgOfs und
dann counter (wie im Beispiel ohne Sprachmodell). Die Prozedur verwendet
ebenfalls das PASCAL-Modell (programmweit eingestellt mit MODEL). Aus diesem
Grunde weiß TASM, welche Adressen MsgOfs und counter repräsentieren. Wenn man
zum Beispiel mit mov cx,[counter]
auf den zweiten Parameter
zugreift, dann erzeugt TASM daraus Code wie im vorherigen Beispiel
(mov cx,[BP + 4]). Ebenso entfällt die manuelle Erzeugung eines Stackrahmens.
Dies wird auch von TASM erledigt. Ein Stackrahmen wird bei Verwendung eines
Sprachmodells nur dann erstellt, wenn auch Parameter definiert werden. Sollte
dies der Fall sein, dann werden zum Auf- und Abbau des Stackrahmens die Befehle
ENTER und LEAVE verwendet.
Eine Eigenschaft der PASCAL-Aufrufkonventionen ist, dass der Stack von der
aufgerufenen Prozedur bereinigt wird. Der Code für die Bereinigung wird
ebenfalls von TASM erzeugt.
Man kann also bereits an dieser Stelle sehen, dass einiges an Aufwand dem
Assemblierer überlassen werden kann. Was man aber beim Prozeduraufruf unbedingt
beachten muss, ist die Tatsache, dass Parameter mindestens Word-Größe haben
müssen. Für den Fall, dass ein Byte übergeben werden soll, folgt hier eine
andere Version des obigen Programmes.
MODEL SMALL,PASCAL DATASEG msg DB 'Parameter 1',0 wiederhole db 100 ;jetzt als Byte CODESEG start: STARTUPCODE call WriteX,OFFSET msg,[word wiederhole] EXITCODE ; PROC WriteX near MsgOfs:word,counter:byte ;einen String mit X Wiederholungen schreiben mov cl,[counter] ;Wiederholungszähler laden xor ch,ch ;HighByte löschen mov dx,[MsgOfs] ;Stringoffset laden . . ret ;Rücksprung ENDP WriteX end start |
Beim Aufruf wird das Byte einfach in ein Word gecastet. Die Definition eines Parameters vom Typ Byte ist problemlos möglich. In der Prozedur kann ebenso problemlos auf den Parameter zugegriffen werden. Falls später das gesamte Register CX verwendet werden soll, dann muss noch das Register CH auf Null gesetzt werden, da der Inhalt dieses Registers bis zu dieser Stelle undefiniert ist.
Falls Sie bereits in PASCAL programmiert haben, dann werden sie die Ähnlichkeit zwischen PROC und PROCEDURE festgestellt haben und erwarten jetzt vielleicht auch ein Äquivalent zur FUNCTION. In Assembler gibt es keine expliziten Funktionen. Eigentlich ist auch in PASCAL eine Funktion nur eine Prozedur, die einen Wert zurückliefert. Dabei werden in PASCAL Bytes und Words in den Registern AL bzw. AX hinterlassen, größere Werte oder Pointer in Registerkombinationen. Dieses Prinzip ist selbstverständlich auch in Assembler anwendbar. Die Rückgabe von Werten im Register AX ist gewissermaßen als stille Übereinkunft anzusehen. Natürlich kann dafür auch jedes andere geeignete Register gewählt werden. Beim Schreiben von Modulen für eine Hochsprache ist nach deren Konventionen vorzugehen. Man kann also eine Prozedur schreiben, die zum Beispiel eine ganzzahlige Berechnung durchführt und das Ergebnis danach im Register EAX ablegt. Dort kann sich der aufrufende Programmteil dann das Ergebnis abholen.
Man sollte darauf achten, dass alle Register, die in einer Prozedur manipuliert
werden, vorher gesichert werden (gilt nicht für Register, die Ergebnisse
zurückliefern sollen). Am geeignetsten dafür ist der Stack. Werden in einer
Prozedur die Register AX, CX, SI und DI verwendet, dann könnte man mit den
erweiterten Stack-Befehlen folgendes schreiben:
PROC WriteX near MsgOfs:word,counter:byte ;einen String mit X Wiederholungen schreiben push ax cx si di mov cl,[counter] ;Wiederholungszähler laden xor ch,ch ;HighByte löschen mov dx,[MsgOfs] ;Stringoffset laden . . pop di si cx ax ret ;Rücksprung ENDP WriteX |
Als erstes in der Prozedur werden die Register gesichert, die verwendet werden.
Unter TASM ist dafür nur ein Befehl notwendig. Vor dem Rücksprung aus der
Prozedur werden die Register wiederhergestellt. Beachten Sie die umgekehrte
Registerreihenfolge.
Insbesondere beim Zurückladen der Register drohen Fehlerquellen. Wie leicht
wird die Reihenfolge verdreht oder ein Register ausgelassen.
Für diese Fälle existiert die Anweisung USES:
PROC WriteX near MsgOfs:word,counter:byte ;einen String mit X Wiederholungen schreiben USES AX,CX,SI,DI mov cl,[counter] ;Wiederholungszähler laden xor ch,ch ;HighByte löschen mov dx,[MsgOfs] ;Stringoffset laden . . ret ;Rücksprung ENDP WriteX |
Anstelle des POP-Befehls wird hier USES verwendet und die zu sichernden Register sind durch ein Komma getrennt. TASM fügt vor dem abschließenden Rücksprungbefehl selbständig Code ein, der die Register wiederherstellt. Man muss sich hier also nur noch um die Sicherung kümmern, (die Codeerzeugung für) die Rückspeicherung übernimmt TASM.
Selbstverständlich kann auch in Assembler mit lokalen Variablen gearbeitet
werden. Lokale Variablen werden auf dem Stack eingerichtet. Dazu muss nach
Aufbau des Stackrahmens Platz geschaffen werden. Dazu wird von SP ein Wert
abgezogen, der dem benötigten Speicherplatz entspricht (aufgerundet auf ein
Vielfaches von 2). Der Zugriff auf eine lokale Variable erfolgt dann über
Konstrukte wie
TASM bietet eine Vereinfachung für die Adressberechnung bei lokalen
Variablen:
PROC WriteX near MsgOfs:word,counter:byte ;einen String mit X Wiederholungen schreiben USES AX,CX,SI,DI LOCAL MyLocalVar:word mov cl,[counter] ;Wiederholungszähler laden xor ch,ch ;HighByte löschen mov dx,[MsgOfs] ;Stringoffset laden mov [MyLocalVar],dx ;die lokale Variable mit Wert belegen . ret ;Rücksprung ENDP WriteX |
Durch die Anweisung LOCAL wird die Definition lokaler Variablen eingeleitet.
Die Datentypdefinition erfolgt eher in Hochsprachenform als so, wie man es aus
Variablendeklarationen im Datensegment kennt.
Das augenblickliche Problem ist, dass das Symbol MyLocalVar, obwohl eigentlich
lokal definiert, im gesamten Modul sichtbar ist. Um das zu verhindern, gibt es
die Direktive LOCALS. Durch Verwendung dieser Direktive ist es möglich,
Symbole, die mit '@@' beginnen, als lokal zu betrachten. Statt der zwei
@-Zeichen kann auch eine alternative Kennzeichnung festgelegt werden. So ist es
mit LOCALS #
PROC WriteX near MsgOfs:word,counter:byte ;einen String mit X Wiederholungen schreiben USES AX,CX,SI,DI LOCAL @@MyLocalVar:word mov cl,[counter] ;Wiederholungszähler laden xor ch,ch ;HighByte löschen mov dx,[MsgOfs] ;Stringoffset laden mov [@@MyLocalVar],dx ;die lokale Variable mit Wert belegen . ret ;Rücksprung ENDP WriteX |
Auf die gleiche Art und Weise müssen Sprungzeile innerhalb einer Prozedur gekennzeichnet werden. Da auch Label im gesamten Modul sichtbar sind, kommt es zu Compilermeldungen, wenn in zwei Prozeduren das gleiche Sprungziel verwendet wird. Durch Voranstellen des Präfix wird nach den Sprungzielen nur noch im lokalen Bereich gesucht.
Auch in Assembler können Prozeduren verschachtelt werden. Die
Prozedurdefinition unterscheidet sich nicht von der unverschachtelter
Prozeduren. Ohne das LOCAL-Präfix ('@@') können die internen Prozeduren auch
von anderer Stelle als der übergeordneten Prozedur aufgerufen werden.
Vorsicht ist geboten, wenn man in einer internen Prozedur auf lokale Variablen
der übergeordneten Prozedur zugreifen möchte. Hat die interne Prozedur einen
eigenen Stackrahmen, dann unterscheidet sich höchstwahrscheinlich der Inhalt
von BP von dem Wert, auf dem die lokalen Variablen der übergeordneten Prozedur
beruhen. Was in der äußeren Prozedur noch über
Vor allem im Hinblick auf Prozedurbibliotheken (s. nächstes Kapitel) und die Einbindung von Assemblermodulen in Hochsprachen muss noch geklärt werden, wie Prozeduren außerhalb des aktuellen Moduls sichtbar werden. Die Definition öffentlicher Prozeduren gestaltet sich ähnlich wie bei den Variablen:
MODEL SMALL,PASCAL CODESEG start: PUBLIC WriteX PROC WriteX near MsgOfs:word,counter:byte ;einen String mit X Wiederholungen schreiben mov cl,[counter] ;Wiederholungszähler laden xor ch,ch ;HighByte löschen mov dx,[MsgOfs] ;Stringoffset laden . . ret ;Rücksprung ENDP WriteX end start |
Durch die Anweisung PUBLIC Symbolname wird das Symbol (Prozedurname,
Variable...) außerhalb des aktuellen Moduls sichtbar.
Mit der Anweisung EXTRN Symbolname kann aus einem anderen Modul heraus auf das
Symbol zugegriffen werden. Die Definition der Prozedur kann in diesem Modul
entfallen. Ebenso entfällt die Definition eventueller Prozedurparameter
innerhalb der der EXTRN-Anweisung.
MODEL SMALL,PASCAL DATASEG msg DB 'Parameter 1',0 wiederhole db 100 ;jetzt als Byte CODESEG EXTRN WriteX:PROC start: STARTUPCODE call WriteX,OFFSET msg,[word wiederhole] EXITCODE end start |
MODEL SMALL,PASCAL DATASEG msg DB 'Parameter 1',0 wiederhole db 100 ;jetzt als Byte CODESEG GLOBAL WriteText:PROC WriteX:PROC start: STARTUPCODE call WriteX,OFFSET msg,[word wiederhole] EXITCODE ; PROC WriteX near MsgOfs:word,counter:byte ;einen String mit X Wiederholungen schreiben mov cl,[counter] ;Wiederholungszähler laden xor ch,ch ;HighByte löschen mov dx,[MsgOfs] ;Stringoffset laden . . ret ;Rücksprung ENDP WriteX end start |
Das Symbol WriteText ist im aktuellen Modul nicht definiert. Deshalb wird es
als EXTRN behandelt. Da WriteX im aktuellen Modul definiert ist, wird es als
PUBLIC behandelt.
Wie Sie ebenfalls sehen können, ist als Datentyp hinter den Prozedurnamen das
Wort PROC angegeben. Dies ist nur als Platzhalter zu verstehen. Die
tatsächliche Datentypgröße hängt vom aktuellen Speichermodell ab. In einem
NEAR-Modell wäre es Word, in einem FAR-Modell wäre es DWord. Natürlich können
Sie auch direkt Word oder DWord schreiben um die durchs Speichermodell
bestimmte Größe zu überlagern.
Sie können Prozedurtypen definieren und dann mit diesen Typen Prozeduren
definieren
PROCTYPE MyType near :word :byte ; PROC WriteX MyType near MsgOfs:word,counter:byte . . ret ;Rücksprung ENDP WriteX |
Hier wird der Prozedurtyp MyType definiert und anschließend gewissermaßen eine Prozedur dieses Typs erstellt. Dabei wird geprüft, ob die Parameter sowohl in Art als auch in Zahl mit denen des Prozedurtypen übereinstimmen. Sollte dies nicht der Fall sein, so gibt es bei der Übersetzung Meldungen des Compilers.
Ebenso wie in der Sprache C haben Sie die Möglichkeit, Prototypen von
Prozeduren (in C heißen sie Funktionsprototypen) zu erstellen. Ebenso wie die
Prozedurtypen dienen die Prototypen der Überprüfung von Art und Anzahl sowie
Aufrufkonvention von Prozeduren.
PROCDESC WriteX near :word :byte ; PROC WriteX MyType near MsgOfs:word,counter:byte . . ret ;Rücksprung ENDP WriteX |
Im Gegensatz zu Makros stehen Prozeduren nur einmal im Speicher und werden bei
jedem Aufruf angesprungen. Bei einem Prozeduraufruf entsteht durch den
call-Befehl selbst und durch das Pushen und Poppen von Parametern ein teilweise
beträchtlicher Overhead. Dieser Overhead ist nur zu rechtfertigen, wenn der
eigentliche Prozedurcode eine bestimmte Größe überschreitet. An dieser Größe
läßt sich auch die Entscheidung festmachen, ob Sie einen Codeabschnitt als Makro
oder als Prozedur in Ihre Anwendung aufnehmen. Ein weiteres Kriterium ist die
Aufrufhäufigkeit. Da bei jedem Makroaufruf der komplette Makrocode erneut
eingefügt wird, kann die Codegröße ganz schnell beträchtlich anwachsen. Man kann
also sagen, wenn ein Makro länger als 10 Zeilen ist, lohnt bereits die
Überlegung, ob man nicht lieber eine Prozedur verwendet.
Man kann den Quelltext der Prozeduren in Dateien hinterlegen und ähnlich wie bei den Makros zum Beispiel mit INCLUDE "procs.inc"
in jedes Programm einbinden.
Das Problem dabei ist, dass bei jeder Übersetzung der Quelldatei alle in der Include-Datei enthaltenen Prozeduren übersetzt werden, auch wenn nur ein kleiner Teil der Prozeduren verwendet wird. Dadurch leidet natürlich die Übersetzungszeit, was besonders bei größeren Programmen den Compile-Test-Zyklus stark verlängern kann.
Stattdessen ist es möglich, die Object-Dateien in einer Bibliothek zusammenzufassen.
Für die Erstellung und Bearbeitung dieser Bibliotheken gibt es im TASM-Programmsystem die Datei tlib.exe.
Wenn Prozeduren aus einer Bibliothek verwendet werden, dann muss stätestens beim Linken der Object-Dateien (mit tlink.exe) die Bibliothek angegeben werden, in der sich die Prozeduren befinden. Um sich die Unannehmlichkeit zu ersparen, bei der Eingabe der Kommandozeile eine Bibliothek zu vergessen, kann die entsprechende Bibliothek bereits im Quelltext angegeben werden. Dazu dient folgende Anweisung:
INCLUDELIB "datname.lib" |
Damit die Prozeduren aus der Bibliothek in Ihrem Programm verwendet werden können, ist folgendes beispielhaftes Vorgehen notwendig.
... INCLUDELIB "datname.lib" EXTRN ProcName1:PROC ProcName2:PROC ;auch GLOBAL moeglich CODESEG start: STARTUPCODE .. .. call ProcName1 EXITCODE END start |
Während der Kompilierung einer Datei führt TASM Buch über die aktuelle Position im Quelltext. Dieser Offset kann im Programmtext über die Spezialvariable '$' angesprochen werden.
Das macht genau dann Sinn, wenn man z. B. die Größe von Zeichenketten oder Strukturen ermitteln möchte. In dieser Form kann die Größe der Struktur nur direkt nach ihrer Definition erfolgen. Die Länge einer Speicherstruktur ergibt sich dann aus der Differenz zwischen aktueller Position und dem Offset der Speicherstruktur.
DATASEG MyWord dw ? msg db "Dieser Text ist zu lang um ihn manuell zu zaehlen",0 msglen = $ - msg ;der Offset von msg steht beim Kompilieren fest |
msglen
hinterlegte Größe kann dann zum Beispiel in Stringausgabefunktionen oder in Kopierfunktionen verwendet werden.
Vorheriges Kapitel (Assemblerbefehle) | Nach oben | |
Zum Inhaltsverzeichnis | ||
Zur Startseite |