TASM stellt dem Programmierer recht weitreichende Möglichkeiten zur Verfügung,
um eigene Datentypen zu definieren.
Dieser Datentyp entspricht der Enumeration in Pascal bzw. enum in C.
Eine Enumeration ist eine Sammlung von Werten, die in einer bestimmten Anzahl
Bits gespeichert werden. Dabei bestimmt der höchste Wert in der Enumeration,
wieviel Speicherplatz sie einnimmt.
Syntax:
ENUM name [enumvar [,enumvar...]]
|
wobei enumvar folgende Syntax hat
Man könnte zum Beispiel eine bestimmte Auswahl an Farben in einer Enumeration
erfassen.
ENUM Farben Rot, Gruen, Blau, Gelb, Braun
|
Die Werte werden von 0 (Null) beginnend durchnumeriert (Rot=0, Gruen=1...). Um
diese Reihenfolge zu durchbrechen, kann man den Werten einen anderen Startwert
zuordnen:
ENUM Farben Rot, Gruen, Blau=10, Gelb, Braun
|
Wie gehabt startet die Numerierung bei 0 (Null), allerdings erhält Blau den Wert
10, Gelb entspricht 11 und Braun entspricht 12.
Da der Höchstwert in dieser Enumeration im Augenblick 12 ist, dieser Wert mit
einem Byte dargestellt werden kann, nehmen Variablen des neu erstellten
Datentyps Farben genau ein Byte in Anspruch. Entsprechend würde folgende
Enumeration ein Word (2 Bytes) beanspruchen:
ENUM Farben Rot, Gruen, Blau=10, Gelb, Braun=30123
|
In einer Enumeration kann maximal der Wert 4.294.967.295 (DoubleWord / 4 Bytes)
zugeordnet werden.
TASM bietet eine mehrzeilige Syntax zur Definition von Enumerations:
ENUM Farben {Rot
Gruen
Blau
Gelb
Braun}
|
oder in einer kompakteren Form
ENUM Farben Rot, Gruen {Blau
Gelb, Braun}
|
Definition als Variable:
Analog zu den Datendefinitionen mit den eingebauten Datentypen schreibt man:
Sinn des ganzen ist, daß MeineFarbe nur Werte annehmen kann, die in der
Enumeration definiert wurden. Diese Werte können mit den symbolischen Namen
(Rot, Blau etc) zugewiesen werden. Der Versuch, der Variable MeineFarbe den
(nicht definierten) Wert Orange zuweisen zu wollen wird vom Assemblierer als
Fehler entlarvt.
Bei obiger Deklaration wird MeineFarbe kein initialer Wert zugewiesen. Dies kann
durch eine der folgenden Anweisungen geschehen:
MeineFarbe Farben Blau
MeineFarbe Farben 10
|
Eine andere Anwendungsmöglichkeit wäre die Definition des Datentyps Boolean:
Datentypdefinition:
Variable:
Es wird die Variable IstEsWahr angelegt, die mit dem Wert True initialisiert
wird.
Dieser Datentyp repräsentiert eine Sammlung von Bitfeldern. Jedes Bitfeld hat
eine bestimmte Breite in Bits. Die Breite des ganzen Records ist die Summe der
Breiten aller Felder.
Sollten Sie bereits mit Pascal programmiert haben, dann lassen Sie sich nicht
dadurch verwirren, daß ein Record in Pascal etwas völlig anderes ist.
Dieser Datentyp wird verwendet, um bestimmte Daten in komprimierter Form
darzustellen. Nehmen wir als Beispiel das bereits bekannte Flagregister. Wie Sie
wissen, gibt jedes Bit einen bestimmten Prozessorzustand wider (abgesehen von
den nicht belegten und reservierten Bits natürlich). Statt nun alle Zustände in
ein Word zu pressen, hätte man auch für jeden Zustand ein Byte-Register nehmen
können. Da jeder Zustand aber nur zwei Ausprägungen hat (an/aus), wäre mit
dieser Lösung jede Menge Speicher verbraucht worden. Also hat man sich für die
komprimierte Form entschieden.
Ein anderes Anwendungsgebiet wäre die Speicherung eines Datums oder einer
Uhrzeit. Ein Datum im Format TTMMJJJJ würde 22 Bits beanspruchen (max:
31.12.9999; 31 läßt sich mit 5 Bits darstellen, 12 mit 4 Bits und 9999 mit 13
Bits). Analog benötigt eine Uhrzeit im 24-Stunden-Format 17 Bits (max: 23:59.59;
23 läßt sich mit 5 Bits darstellen und die 59 mit jeweils 6 Bits). In ähnlicher
Weise verwaltet DOS die Zeitstempel, die bei der Verwaltung von Dateien
Verwendung finden. Allerdings werden bei DOS die Sekunden anders verwaltet.
Statt 6 Bits werden nur fünf Bits benötigt, da die Sekunden in Zweierschritten
gespeichert werden. Das bedeutet, daß man nach Ermittlung der Sekunden diese
noch mit 2 multiplizieren muß, um auf den korrekten Zeitpunkt zu kommen.
Hier die Syntax zur Deklaration eines Bit-Field Records:
RECORD name [rec_field [, rec_field...]]
|
Dabei hat jedes rec_field folgende Syntax:
field_name : width [=value]
|
name ist Bezeichnung des neuen Datentyps, field_name ist die Bezeichnung eines
einzelnen Bitfeldes und das optionale value gibt an, mit welchem Wert das
Bitfeld initialisiert werden soll, wenn eine Variable dieses Datentyps angelegt
wird.
Beispiel:
RECORD Datum Tag:5, Monat:4, Jahr:13
|
Ein Record nimmt abhängig von der Summe seiner Bitfelder ein Byte, ein Word oder
ein DWord in Anspruch. Das bedeutet, daß maximal 32 Bitfelder definiert werden
können (die dann alle ein Bit groß sind). Unser Datentyp Datum würde also ein DWord
beanspruchen (mehr als 16 Bits)
Um eine Variable dieses Typs anzulegen, schreiben Sie:
Unabhängig davon, ob in der Datentypdefinition ein Initialwert angegeben wurde
sind jetzt alle Bitfelder unitialisiert. Um die definierten Initialwerte in die
Variable zu übernehmen, schreiben Sie:
Mit einer Erweiterung dieser Form kann man ausgewählten (benannten) Feldern bei
der Variablenerstellung einen Wert geben:
Geburtstag Datum {Tag=20,Jahr=1977}
|
Mit einer anderen Form der Initialisierung ist es möglich, die Bitfeldnamen
nicht zu schreiben, dafür muß die Wertangabe in der Reihenfolge der Bitfelder im
Record erfolgen:
Geburtstag Datum <20,,1977>
Geburtstag Datum <,5,1977>
Geburtstag Datum <20,5>
|
Im ersten Fall wurde also den Feldern Tag und Jahr ein Initialwert mitgegeben,
im zweiten Fall den Feldern Monat und Jahr, im dritten Fall den Feldern Tag und
Monat.
Für alle Formen der Initialisierung gilt, daß bei der Erstellung nicht genannte
Bitfelder mit den Werten initialisiert werden, die bei der Datentypdefinition
angegeben wurde. Wurde dort kein Wert angegeben, dann wird die 0 (Null)
verwendet.
Aus diesem Grunde konnte im letzten Fall auch das Komma fehlen, daß angezeigt
hätte, daß da noch ein Bitfeld kommt (Jahr).
TASM stellt mit den Anweisungen SETFIELD und GETFIELD leistungsfähige
Instruktionen zur Verfügung, die es komfortabel ermöglichen, die einzelnen
Bitfelder mit Werten zu belegen oder deren Werte auszulesen.
Die Namen der Bitfelder sind global verfügbar, dürfen also nicht doppelt
verwendet werden. Dafür ist es möglich, im Verlauf des Quelltextes einen neuen
Record-Datentyp mit dem gleichen Namen zu erstellen (in unserem Fall also eine
Redefinition des Datentyps Datum
).
Wenn Sie bereits in Pascal oder C programmiert haben, dann werden Sie die
Structures als Records (Pascal) bzw struct (C) wiedererkennen.
In einer Structure lassen sich mehrere Daten mit unterschiedlichen Datentypen
zusammenfassen. Darin besteht der Unterschied zum Array, in dem sich nur Daten
gleichen Typs zusammenfassen lassen.
Die Daten innerhalb einer Structure werden Member genannt. Im Gegensatz zum
Bit-Field Record haben die Member immer die Größe von Vielfachen eines Bytes
während in einem Record die Bytes in ihre Bits aufgebrochen werden.
Die Größe einer Structure ergibt sich aus der Summe der Größe ihrer Member.
Es ist möglich, Structures zu verschachteln, also innerhalb einer Structure eine
weitere zu definieren.
Die Definition einer Structure wird folgendermaßen eröffnet und geschlossen:
STRUC name
...
ENDS [name]
|
Zwischen diesen beiden Anweisungen werden die Member definiert. Diese Definition
verläuft analog zu den bisher gezeigten Variablenvereinbarungen.
Bauen wir uns eine Structure, die die persönlichen Daten einer Person
enthält:
STRUC Person
Vorname db 30 dup (?)
Nachname db 30 dup (?)
Geburtsdatum Datum ?
Strasse db 30 dup (?)
Wohnort db 30 dup (?)
PLZ db 5 dup (?)
Telefon db 12 dup (?)
ENDS
|
Beim Schließen der Structure ist die Angabe eines Namens unnötig, da sich TASM
selbst heraussucht, zu welcher Structuredefinitionseröffnung das
ENDS
gehört.
Beispiel für eine verschachtelte Structuredefinition:
STRUC Person
Vorname db 30 dup (?)
Nachname db 30 dup (?)
Geburtsdatum Datum ?
STRUC ;verschachtelte Structures haben keinen Namen
Strasse db 30 dup (?)
Wohnort db 30 dup (?)
PLZ db 5 dup (?)
ENDS
Telefon db 12 dup (?)
ENDS
|
Es ist möglich, eine Structure um eine benannte Structure zu erweitern, was ein
wenig in Richtung der aus der Objektorientierung bekannten Vererbung geht:
STRUC Adresse
Strasse db 30 dup (?)
Wohnort db 30 dup (?)
PLZ db 5 dup (?)
ENDS
STRUC Person
Vorname db 30 dup (?)
Nachname db 30 dup (?)
Geburtsdatum Datum ?
Adresse STRUC (?)
Telefon db 12 dup (?)
ENDS
|
Diese Definition führt zum gleichen Ergebnis wie:
STRUC Person
Vorname db 30 dup (?)
Nachname db 30 dup (?)
Geburtsdatum Datum ?
STRUC ;verschachtelte Structures haben keinen Namen
Strasse db 30 dup (?)
Wohnort db 30 dup (?)
PLZ db 5 dup (?)
ENDS
Telefon db 12 dup (?)
ENDS
|
TASM erlaubt es, die Member an bestimmten Adressen auszurichten. So ist es bei
Prozessoren ab dem 80486 günstiger, wenn Daten an ganzzahlig durch 4 teilbaren
Adressen liegen, zumindest aber an Wordgrenzen. Um diese Ausrichtung
einzurichten, schreiben Sie:
STRUC Adresse
ALIGN 4
Strasse db 30 dup (?)
ALIGN 4
Wohnort db 30 dup (?)
PLZ db 5 dup (?)
ENDS
|
Die Anweisung ALIGN Grenze müssen Sie vor jeden Member schreiben, den Sie
an einer bestimmten Grenze ausgerichtet haben möchten. TASM fügt dann, falls
nötig, ein paar Leerbytes ein, d. h. Ihre Structure wird bei Ausrichtung an
DWordgrenzen je Ausrichtungsanweisung bis zu drei Bytes größer.
Um eine Variable dieses Typs zu erstellen, schreiben Sie:
Unabhängig von irgendwelchen Initialwerten, die bei der Structuredefinition
angegeben wurden, sind nach dieser Definition alle Member uninitialisiert.
Um die Initialwerte der Structuredefinition zu übernehmen, schreiben Sie:
Um einzelnen Membern bei der Variablenerstellung einen Wert zuzuweisen,
schreiben Sie:
DiePerson Person {Nachname='Helmchen', Wohnort='Berlin', PLZ='12345'}
Bei der Verwendung der nächsten Initialisierungsart werden die Membernamen nicht
geschrieben, dafür muß die Initialisierung in der Reihenfolge der in der
Structure definierten Member erfolgen:
DiePerson Person <,'Helmchen',,,'Berlin','12345'>
|
Der Zugriff auf die Member erfolgt, wie aus den Hochsprachen Pascal und C
bekannt, über folgendes Konstrukt:
also zum Beispiel
Unions sehen fast genauso aus wie Structures, auch die Terminologie ist ähnlich.
Im Gegensatz zu einer Structure bestimmt sich die Größe einer Union jedoch nicht
aus der Summe der Member, sondern wird duch den größten Member festgelegt.
Die Besonderheit an einer Union ist nämlich, daß alle Member physikalisch an der
gleichen Adresse liegen. Ein Beispiel kann dies sicherlich eher illustrieren.
Definition einer Union:
UNION Daten
Datum1 db 30 dup (?)
Datum2 dw 40000
Datum3 dd 10 dup (?)
ENDS
|
Datum1 benötigt 30 Bytes, Datum2 ein Word (2 Bytes) und Datum3 schließlich 40
Bytes (ein DWord=4 Bytes). Die Union benötigt daher einen Speicherplatz von 40
Bytes. Die Konsequenz der Speicherung an der gleichen physikalischen Adresse
ist, daß eine Änderung an einem logischen Member auch Änderungen an allen
anderen Membern verursacht. Nehmen wir an, daß in Datum1 ein kleiner Text steht.
Durch die beiden Anfangsbuchstaben dieses Textes wird auch der Wert von Datum2
bestimmt. Da die ersten 7,5 DWords ebenfalls im Bereich dieses Textes liegen,
wird deren Wert durch den Textinhalt bestimmt. Sollte jetzt in Datum2 ein neuer
Wert eingetragen werden, dann ändern sich die beiden Anfangsbuchstaben von
Datum1 und ein halbes DWord von Datum3.
Der Sinn einer Union liegt darin, eine problemlose Interpretation ein und
desselben Wertes zu erhalten.
Beispiel:
UNION Wort
ByteView db 2 dup (?)
WordView dw ?
ENDS
DasWort Wort ?
Pseudocode: DasWord.ByteView := 'AB'
|
Diese Union würde ein Word (2 Bytes) Speicher beanspruchen. Aufgrund der
Byteanordnung im Speicher würde man bei einem Zugriff auf ByteView das 'A', bei
einem Zugriff auf ByteView+1 das 'B' erhalten, bei einem Zugriff auf WordView
jedoch ein Word, dessen höherwertiges Byte den ASCII-Wert 65 und dessen
niederwertiges Byte den ASCII-Wert 66 enthält.
Die Verwendung als Variable erfolgt analog zu der einer Structure. Bei der
Initialisierung einer Union ist es allerdings nicht gestattet, mehr als einen
Initialwert anzugeben. Das ist auch logisch, wenn man bedenkt, daß alle Member,
je nach Größe, den gleichen Speicher belegen. Der Assemblierer könnte zwar so
kulant sein, nur die letzte Initialisierung gelten zu lassen, aber an dieser
Stelle ist er streng.
Ein Named Type ist im Grunde nur ein anderer Name für einen bereits bestehenden
Datentyp:
TYPEDEF type_name complex_type
|
Type_Name ist der neue Name, unter dem wir den bereits existierenden
complex_type ansprechen.
Beispiel:
TYPEDEF WinType DW
Zahl WinType ? ;WinType ist ein Named Type
|
entspricht also