Eigene Datentypen

TASM stellt dem Programmierer recht weitreichende Möglichkeiten zur Verfügung, um eigene Datentypen zu definieren.

Enumerations - Aufzählungen

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
var_name [=value]
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:

MeineFarbe Farben ?
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. Die symbolischen Bezeichner sind in der Quelldatei global gültig und dürfen nicht als Variablenname etc. verwendet werden..
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:
ENUM Boolean False, True
Variable:
IstEsWahr Boolean True
Es wird die Variable IstEsWahr angelegt, die mit dem Wert True initialisiert wird.

Bit-Field Records - Bitfelder

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...]]
Im MASM-Mode sieht die Definition so aus:
name RECORD [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:
Geburtstag Datum ?
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:
Geburtstag Datum {}
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 (TASM-interne Makros, keine Prozessorbefehle).

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).

Structures - Strukturen

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]
Der MASM-Mode verwendet folgende Syntax:
name STRUC
...
name ENDS

Zwischen den beiden Anweisungen STRUC und ENDS werden die Member definiert. Diese Definition verläuft analog zu den bisher gezeigten Variablenvereinbarungen.
Wie Sie sehen, kann im IDEAL-Mode die Strukturbezeichnung bei ENDS entfallen, da TASM automatisch die Schachtelungstiefe verwaltet und falsch geschlossene Strukturen automatisch erkennt. Ich empfehle dennoch, auch bei ENDS den Strukturbezeichner anzugeben. 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 PERSON

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:
DiePerson Person ?
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:
DiePerson Person {}
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:
Structurename.Membername
also zum Beispiel
DiePerson.Vorname

Unions

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
Wie so oft ist im MASM-Mode die Reihenfolge etwas anders:
Daten UNION 
      Datum1 db 30 dup (?)
      Datum2 dw 40000
      Datum3 dd 10 dup (?)
Daten 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.

Named Types - Benannte Typen

Ein Named Type ist im Grunde nur ein anderer Name für einen bereits bestehenden Datentyp:

TYPEDEF type_name complex_type
Im MASM-Mode sieht die Definitione so aus:
type_name TYPEDEF 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
Zahl dw ?