Der erste Teil dieses Kurses zeigte ausgehend von einem gegebenen Assemblerlisting
den Weg über Compiler und Linker hin bis zum ausführbaren COM-Programm.
Hierzu noch ein Nachtrag:
Von Jan Laitenberger gibt es den Freeware-Assembler JASMIN,
der unkomprimiert nur knapp 20 KByte groß ist und speziell für
den Einsatz auf dem Portfolio konzipiert wurde. Der begrenzte Funktionsumfang
(keine Makros, nur 8086/88, nur COM-Programme) ermöglicht eine sehr
einfache Bedienung, so daß JASMIN besonders für Einsteiger gut
geeignet ist. Trotz seiner Eigenheiten (Labels müssen mit @ beginnen,
Speicherzugriffe sind durch eckige Klammern kenntlich zu machen) ist dieser
Assembler ein Muß für Portfolio-Fans!
(Bezugsquelle: http://www.franksteinberg.de/ZIPS/JASMIN16.ZIP)
Vielleicht wartet der eine oder andere Leser bereits ungeduldig darauf, endlich die ersten 8086-Befehle (der 8088 ist bekanntlich kompatibel) zu lernen, um selbst ein Assemblerprogramm zu "stricken". Und in der Tat ist man mit einem Dutzend gelernten Befehlen schon ganz brauchbar gerüstet. Zunächst sollte der Neuling jedoch die (zugegebenermaßen etwas chaotische) Architektur des Prozessors kennenlernen:
Die Register des Prozessors
Die Hauptbeschäftigung des Prozessors ist es, Befehle und Daten aus dem Speicher zu holen, die Daten zu verarbeiten und z.B. an eine andere Stelle des Speichers zurückzuschreiben, etwa in den Grafikspeicher. Zur Zwischenspeicherung im Prozessor dienen dabei die Register. Die 14 Register des 8088, die alle 16 Bit breit sind, sollte man beim Programmieren stets vor Augen haben. Es folgt hier deshalb eine Übersicht mit knapper Erläuterung. Das 2-Zeichen-Kürzel zur Benennung eines Registers weist auf seinen bevorzugten Verwendungszweck hin, denn jedes Register besitzt besondere Fähigkeiten. Einige der dazu benötigten Spezialbefehle sind in der Übersicht aufgeführt, auch wenn der Anfänger sich darum zunächst nicht zu kümmern braucht.
1. Die vier Allzweckregister (z.B. für arithmetische und logische
Operationen):
Diese Register sind am vielseitigsten einsetzbar. Die oberen bzw. unteren
8 Bits lassen sich auch getrennt verwenden, d.h. es stehen hiermit bis
zu acht 8-Bit-Register zur Verfügung. Für die ersten Assembler-Gehversuche
reichen die Allzweckregister bereits.
AX = AH|AL Akkumulator
MUL, DIV
BX = BH|BL Basisregister
XLAT
CX = CH|CL Count-Register
LOOP
DX = DH|DL Daten-Register
IN, OUT
2. Die Index- und Pointerregister:
Abgesehen von SP und IP dürfen auch diese Register nach Belieben
verwendet werden. Insbesondere eignen sie sich aber zur indirekten Adressierung
und für die sog. Stringbefehle.
IP Instruction Pointer (Finger weg!)
SP Stackpointer (Finger weg!) PUSH, POP
BP Basepointer
SI Sourceindex
LODSB
DI Dest.index
STOSB
3. Die Segmentregister:
Die Segmentregister haben eine spezielle Funktion, die später
noch genauer erklärt wird. Nur ES steht zur freien Verfügung.
CS Codesegment (Finger weg!)
DS Datensegment (Vorsicht!)
SS Stacksegment (Vorsicht!)
ES Extrasegment
STOSB
4. Das Flag-Register:
Dieses Register, auf das nicht direkt zugegriffen werden kann, benötigt
der Prozessor intern zur Speicherung von Status-Bits, den sog. Flags. Das
für uns wichtigste Flag ist das Zero-Flag, welches die Gleichheit
der beiden Operanden des letzten Compare-Befehls (CMP) signalisiert. Die
Abfrage erfolgt mit dem bedingten Sprungbefehl JZ (jump if zero), der nur
bei gesetztem Zero-Flag vollzogen wird.
Die Sache mit den Segmenten
Standardmäßig werden alle Speicherzugriffe über 16-Bit-Adressen
abgewickelt, schließlich stehen uns ja auch nur 16 Bit breite Register
zur Verfügung. Beispielsweise lädt der Befehl "mov al,[si]"
das Register al mit dem Byte-Wert, der an der Speicherstelle steht, die
durch das Register si bestimmt ist (indirekte Adressierung). Bekanntlich
lassen sich mit 16 Bit aber nur 65536 Speicherplätze, sprich 64 KByte
adressieren. Mit den vier Segmentregistern können vier Bereiche zu
je 64 KByte aus dem gesamten Adreßraum des Prozessors (1 MByte) ausgewählt
werden. Diese Segmente lassen sich in 16-Byte-Schritten plazieren und können
sich auch überlappen. Für kleine Programme ist es aber oft überhaupt
nicht nötig, die Segmente zu verschieben.
Wie die Namen der Segmentregister vermuten lassen, ist es vorgesehen,
für Programmcode, Daten und den Stack (Zwischenspeicher für Unterprogramme)
jeweils ein eigenes Segment zu spendieren. Für uns spielt das aber
keine Rolle, weil das Betriebssystem bei COM-Programmen die drei Segmentregister
CS, DS und SS mit dem selben Wert initialisiert. Es besteht also zum Glück
kaum die Chance, einen Befehl zu verwenden, der sich nicht auf das gewünschte
Segment bezieht. Allerdings muß sich ein COM-Programm deshalb mit
höchstens 64 KByte begnügen, was aber für die meisten Portfolio-Programme
mehr als ausreichend ist. Zudem sind compilierte Assemblerprogramme so
kompakt, daß ihre Größe in der Regel nur etwa 1/10 von
der des Quelltexts beträgt.
Eine handvoll Befehle
Wie gesagt läßt sich mit einigen wenigen Assemblerbefehlen bereits eine Menge (im Prinzip alles, nur nicht immer sehr elegant) anfangen. Da der Befehlssatz des 8088 zu komplex für eine komplette Betrachtung an dieser Stelle ist, beschränken wir uns auf folgende oft benötigte Befehle: MOV, CMP, JMP, JZ, ADD, SUB, IN, OUT, INT, CALL, RET. Die Wirkungsweise dieser (und anderer) Befehle läßt sich dank der Registeranzeige gut mit DEBUG erforschen. Viele Befehle erlauben nur bestimmte Adressierungsarten oder sind nicht auf alle Register anwendbar. Hier heißt es studieren oder probieren!
MOV Ziel,Quelle
Der vielseitigste und meist genutzte Assemblerbefehl ist sicherlich
der MOV-Befehl (move). Er überträgt ein Datenwort z.B. aus dem
Speicher in ein Register oder aus einem Register in ein anderes. Zudem
unterstützt dieser Befehl verschiedene Adressierungsarten für
Speicherzugriffe. Hierzu eine Auswahl an Beispielen:
mov al,2 Lädt
das Register al mit dem Wert 2
mov al,[2] Lädt al mit
dem Byte-Wert, der an der
Speicherstelle 2 im Datensegment steht
mov ax,bx Kopiert bx
nach ax
mov al,[bx] Lädt al mit dem
Byte-Wert, der an der Speicherstelle
bx im Datensegment steht (indirekte Adressierung)
mov cl,[si+3] Lädt cl mit dem Byte-Wert,
der an der Speicherstelle
si+3 im Datensegment steht (indirekte indizierte Adr.)
mov [7],dh Speichert dh an
der Speicherstelle 7 im Datensegment
Diese Beispiele wickeln den Datenaustausch mit dem Speicher nur über
die 8-Bit-"Halbregister" ab, wodurch genau 1 Byte im Speicher angesprochen
wird. Es sind aber auch 16-Bit-Speicherzugriffe möglich. Dabei ist es
oft hilfreich zu wissen, daß gemäß Intel-Konvention an
der angegebenen Speicherstelle das niederwertige Byte (Lowbyte) und an
der darauffolgenden Speicherstelle das höherwertige Byte (Highbyte)
angesiedelt ist.
Wer sich mit DEBUG einmal den Maschinencode für obige Varianten
des MOV-Befehls ansieht, kann feststellen, daß wir es hier eigentlich
mit verschiedenen Maschinenbefehlen zu tun haben:
B002 MOV
AL,02
A00200 MOV AL,[0002]
89D8 MOV
AX,BX
8A07 MOV
AL,[BX]
8A4C03 MOV CL,[SI+03]
88360700 MOV [0007],DH
Besonders effizient codiert wurden die ersten beiden Zeilen: Die Befehlscodes
(OP-Codes) bestehen dort aus einem einzigen Byte (B0 bzw. A0), gefolgt
von einem bzw. zwei Bytes für den zweiten Parameter. Man sieht auch,
daß die 16-Bit-Konstante 0002 im Format Lowbyte/Highbyte gespeichert
ist.
Die anderen vier - etwas weniger gebräuchlichen - Varianten benötigen
jeweils 2 Bytes für den OP-Code und bis zu zwei weitere Bytes für
Konstanten.
CMP Operand1,Operand2
Wie erwähnt, dient der Befehl CMP dem Vergleich zweier (gleichberechtigter)
Operanden. Intern führt der Prozessor dazu eine Subtraktion durch
und setzt das Zero-Flag, falls das Ergebnis null ist.
JZ Sprungziel (jump if zero)
Dieser bedingte Sprungbefehl setzt den Instruction pointer auf die
angegebene Adresse, falls das Zero-Flag gesetzt ist. Die Kombination aus
CMP und JZ entspricht etwa folgender BASIC-Anweisung:
IF Operand1=Operand2 THEN GOTO Sprungziel
Zum Befehl JZ existiert die alternative Schreibweise JE (jump if equal),
die im Einzelfall zum besseren Verständnis verwendet werden kann.
Soll ein Sprung erfolgen, wenn der letzte Vergleich keine Übereinstimmung
ergab, setzt man den Befehl JNZ (jump if not zero) oder JNE ein.
Beispiel:
cmp al,27 ; ist al=27?
jz Escape ; Ja: Springe zur Marke 'Escape'
JMP Sprungziel
Unbedingter Sprungbefehl. BASIC-Äquivalent: "GOTO Sprungziel"
INT Nummer
Dieser Befehl ruft die durch Nummer spezifizierte Interruptroutine
auf. Der INT-Befehl stellt die Schnittstelle zum Betriebssystem dar, denn
sowohl BIOS als auch DOS belegen etliche Interruptvektoren mit Routinen,
die von Anwendungsprogrammen genutzt werden können (und sollen). Je
nach Funktion müssen zuvor diverse Parameter in bestimmten Registern
abgelegt werden. Beispiel:
mov ah,7 ; 7=Zeicheneingabe nach
al
int 21h ; 21=wichtigster DOS-Interrupt
mov dl,al ; eingelesenes Zeichen
mov ah,6 ; 6=Zeichenausgabe (dl=ASCII)
int 21h
Übrigens lautet der Maschinencode für Int 21h 'CD 21' (dezimal
205, 33) und dürfte eine der häufigsten Bytekombinationen in
DOS-Programmen darstellen.
IN Register,dx
Eine wichtige Eigenart der 80x86-Prozessorfamilie sind die Portadressen.
Parallel zum Arbeitsspeicher existiert ein 64 KByte großer Adreßraum,
über den Peripheriebausteine (z.B. paralleles Interface) angesprochen
werden. Der Prozessorbefehl IN liest einen 8-oder 16-Bit-Wert von der Portadresse
dx ein. Folgendes Beispiel übernimmt den Zustand der Statusleitungen
des Druckerports in das Register al:
mov dx,807Ah
in al,dx ; nur dx möglich!
OUT dx,Register
Als Pendant zu IN schreibt OUT einen 8-oder 16-Bit-Wert an die Portadresse
dx. IN und OUT werden benötigt, wenn am Betriebssystem "vorbeiprogrammiert"
werden soll, um Hardwarezugriffe zu beschleunigen. Der Grafikmodus des
Portfolio läßt sich so überhaupt erst effizient nutzen.
Der nächste Kursteil wird sich damit genauer befassen.
ADD Operand1,Operand2
Addiert zum Operand1 den Operand2. Falls das Ergebnis zu groß
für Operand1 ist, wird das Carry-Flag gesetzt (Abfrage z.B. über
den bedingten Sprungbefehl JC - jump if carry), welches somit das im Ergebnis
fehlende höchste Bit repräsentiert. Beispiel:
mov cx,1997 ; cx=1997
add cx,3 ; cx=2000
SUB Operand1,Operand2
Subtraktion analog zu ADD. Beispiel:
mov bx,0605h ; bh=6, bl=5
sub bl,bh ; bl=FFh
Hier wird das Carry-Flag gesetzt, was bedeutet, daß FFh als -1
zu interpretieren ist.
CALL Sprungziel und RET
Der pure Luxus: Strukturierte Programmierung in Maschinensprache durch
Unterprogramme! CALL springt an den Anfang des Unterprogramms und
sichert automatisch die Rücksprungadresse auf dem Stack. RET beendet
das Unterprogramm, indem es die Rücksprungadresse wieder vom Stack
holt, um das Programm mit dem Befehl hinter 'CALL' fortzusetzen. Ein anständiges
Unterprogramm sichert zunächst alle verwendeten Register mit PUSH
auf dem Stack und stellt sie am Ende mit POP wieder her (Diese Befehle
sind allerdings - wie auch CALL und RET - mit etwa 20 Takten recht langsam).
Beispiel:
...
call PortAus
...
PortAus: push dx
; Register
push al ; retten
mov dx,8078h ; Pofo-Druckerport
mov al,0
out dx,al
pop al ; umgekehrte
pop dx ; Reihenfolge
ret
Weitere Prozessorbefehle
Aus Platzgründen kommen hier leider viele nützliche Befehle
zu kurz. Wenigstens nicht unerwähnt bleiben sollten diese Instruktionen:
AND, OR, XOR bitweise Verknüpfungen
INC, DEC Operand
um 1 erhöhen/vermindern
ADC, SBC Addition/Subtr.
mit Übertrag
LOOP
Schleife mit cx als Zähler
SHL,SHR ROL,ROR Schieben und Rotieren
LODSB, STOSB Stringbefehle
Ein Wort zur Ausführungsgeschwindigkeit
Als klassischer CISC-Prozessor geizt der 8088 nicht gerade mit den Taktzyklen, die pro Instruktion benötigt werden. Man kann mit etwa 2 bis 20 Takten pro Befehl rechnen, einige Spezialbefehle (insbes. MUL, DIV) können sogar über 100 Taktzyklen in Anspruch nehmen. Zur Erinnerung: Der Prozessortakt des Portfolio liegt standardmäßig bei 4.9 MHz.
Zwischenbilanz
Das Programmbeispiel aus dem ersten Kursteil sollte nun eigentlich nicht
mehr mit Hieroglyphen zu verwechseln sein. Auch das damalige Programmende
kann jetzt erklärt werden:
mov ax,4c80h
out dx,al
int 21h
Die Funktion 4Ch (ah=4Ch) des DOS-Interrupts 21h beendet das Programm.
Im Register al wird der Wert 80h nur benötigt, um damit den Lautsprecher
per Portausgabe abzuschalten. Als Nebeneffekt (feature?) ist 80h gleichzeitig
der Rückgabewert des Programms, der mit ERRORLEVEL abgefragt werden
kann.
Ausblick
Der nächste Kursteil wird sich voraussichtlich mit der Ein- und Ausgabe auf dem Portfolio befassen. Dabei wird der Verwendung von Betriebssystemfunktionen der direkte Zugriff auf die Hardware gegenübergestellt. Weiterhin steht noch eine Beschreibung zu Variablen- und Konstantendefinitionen aus.
Fragen und Anmerkungen bitte an .
Eventuelle Korrekturen oder Ergänzungen werden zu finden sein
auf der WWW-Seite:
http://leute.server.de/peichl/pf.htm