Adok's Way to Assembler
Folge 6
Begleit-Dateien:
Hallo Freunde! Für heute habe ich mir folgende Themen vorgenommen:
- Prozeduren, Funktionen und Makros
- Arithmetikbefehle
- Stringbefehle
- Assembler und Hochsprachen
Womit fangen wir am besten an? Machen wir's einfach der Reihe nach.
+++ Prozeduren und Funktionen +++
Für diejenigen, die noch nie in einer Hochsprache programmiert haben:
Prozeduren sind Routinen, die man von jeder Stelle des Programms aus aufrufen
kann - quasi selbstdefinierte Befehle. Es können Parameter übergeben werden,
was meistens mit Hilfe des Stacks getan wird. Funktionen sind ähnlich wie
Prozeduren, können aber auch einen Wert zurückgeben. In Hochsprachen wird der
Rückgabewert meistens im AX-Register bzw. bei 8-Bit-Werten in AL und bei
32-Bit-Werten in dem Registerpaar DX:AX gespeichert.
In Assembler werden Prozeduren mit dem CALL-Befehl aufgerufen. Als Parameter
muß man den Namen des Labels oder den Offset angeben, an der/dem die Prozedur
beginnt. CALL sichert den Rücksprungsoffset auf den Stack und führt einen
JMP-Befehl aus. Am Ende jeder Prozedur muß RETN (bzw. bei Interrupt-Prozeduren
IRET) stehen. Sobald der Prozessor auf RETN stößt, holt er den Offset der
Rücksprungsadresse vom Stack und führt dorthin einen JMP aus (er springt also
dorthin, von wo die Prozedur aufgerufen wurde).
Um das Programm-Listing übersichtlicher zu gestalten, kann man Prozeduren und
Funktionen auch anstelle eines Labels mit
PROC procname NEAR ;bzw. FAR
definieren. Am Ende der Prozedur schreibt man dann:
procname ENDP
Wozu dient FAR? Ganz einfach: Als FAR definierte Prozeduren lassen sich auch
aus anderen Code-Segmenten als dem, in welchem sie sich befinden, aufrufen.
Beim CALLen ändert sich für den Programmierer nichts. Der Assembler wandelt
alle CALL-Aufrufe, die sich auf FAR-Prozeduren beziehen, automatisch in
Far-CALLs um. Man muß lediglich beachten, daß ein Far-CALL nicht nur den
Offset, sondern auch das Segment der Rücksprungadresse auf den Stack schreibt.
Jedoch muß statt RETN der Befehl RETF geschrieben werden.
Neben der Übersichtlichkeit dient PROC noch einem weiteren Zweck: Alle
RET-Anweisungen werden vom Assembler automatisch abhängig vom Typ der Prozedur
(NEAR oder FAR) als RETN bzw. RETF interpretiert.
Hier nun ein Beispielprogramm zum Thema Prozeduren, welches gleichzeitig auch
die Verwendung des Stacks und der Portbefehle demonstriert.
MODEL TINY
CODE SEGMENT
ASSUME CS:CODE,DS:CODE
ORG 100h
start:
JMP main
PSET PROC NEAR ;Kopf der Prozedur PSET
POP CX ;Zwischenspeichern des Rücksprungsoffsets
MOV AX,0A000h ;Video-Segment in ES
MOV ES,AX ;
MOV AL,15 ;Farbe 15
POP BX ;Parameter (Video-Offset) laden
MOV ES:[BX],AL ;Pixel setzen
PUSH BX ;Parameter und
PUSH CX ;Rücksprungsoffset auf Stack legen
RET ;Zurück zum Aufruf der Prozedur
PSET ENDP ;Ende der Prozedur PSET
main:
MOV AX,13h ;Bildschirmmodus 13h (320x200x256)
INT 10h ;mittels INT 10h Fkt. 0 einstellen
MOV AX,32159 ;Prozedur PSET mit Parameter 32159
PUSH AX ;aufrufen
CALL PSET
l1: ;Auf ESC warten (bis Port 60h=1)
IN AL,60h ;
CMP AL,1 ;
JNE l1 ;
MOV AX,3 ;Textmodus einstellen
INT 10h ;
MOV AX,4C00h
INT 21h
CODE ENDS
END start
Wie ihr seht, benutzt das Proggy einen neuen Interrupt und andere Sachen, die
wir jetzt gleich besprechen werden:
INT 10h Fkt. AH=0 schaltet in den in AL angegebenen Bildschirmmodus um.
Der Bildschirmspeicher der VGA-Grafikmodi befindet sich im Segment A000h. Im
Modus 13h respräsentiert jedes Byte dieses Segments die Farbe eines Bildpunkts
(Pixels). Wollen wir den Punkt mit den Koordinaten x/y ansprechen, so müssen
wir 320 * x + y rechnen. Das Ergebnis ist der Offset, über den dieser Punkt im
Videosegment A000h angesprochen werden kann.
Die Prozedur PSET des Beispielprogramms dient dazu, einen Pixel in der Farbe
15 (für gewöhnlich weiß) zu setzen. Als Parameter muß der Offset des
gewünschten Pixels angegeben werden. Im Beispielprogramm ist dieser Wert 32159
- es wird also der Punkt 159/100 angesprochen.
Über den Port 60h kann man den Scancode der gerade gedrückten Taste ablesen.
Der Scancode ist eine Zahl zwischen 0 und 127 (7 Bits). Ist Bit 8 gesetzt, so
wird momentan keine Taste gedrückt. Die unteren 7 Bits enthalten dann den
Scancode der zuletzt betätigten Taste.
Der Scancode von ESC hat die Nummer 1. Eine komplette Übersicht aller
Scancodes könnt ihr u.a. in der QBasic-Hilfe unter dem Thema Tastaturcodes
finden.
Der Rest des Beispielprogramms müßte klar sein. Vergeßt niemals, am Anfang
jeder Prozedur zuerst die Rücksprungadresse und danach die Parameter in der
richtigen Reihenfolge zu POPen! Am Ende jeder Prozedur müssen Paramater und
Rücksprungadresse wieder in * u m g e k e h r t e r * Reihenfolge gePUSHt
werden! Wenn ihr diese Regeln nicht beachtet, wird an RET nicht die korrekte
Rücksprungadresse übergeben, und der Computer stürzt ab!
+++ Makros +++
Makros unterscheiden sich von PROCs in einem Punkt: Sie werden nicht geCALLt,
sondern beim Assemblieren direkt in den Programmcode eingefügt. Das belegt
zwar je nach Größe der Makros ein wenig bis sehr viel mehr Speicherplatz, ist
aber dafür um ein paar Nanosekunden schneller. Ein Makro wird mit MACRO
macroname definiert und hört mit macroname ENDM auf. Ein RET kann man sich
ersparen, und anstelle von CALL schreibt man im Programmcode beim Makro-Aufruf
nur den Namen des Makros. That's it!
+++ Arithmetikbefehle +++
Juchu, wir rechnen! Folgende vier Grundrechnungsarten gibt es: ... äh, ich
denke, das wißt ihr schon. ;-) Also, folgende Befehle hält ASM zu
Rechenzwecken bereit:
- ADD Ziel,Quelle: Addition. Ähnlich wie bei MOV sind nur bestimmte
Ziel-Quelle-Kombinationen möglich, und zwar: Reg-Wert, Mem-Wert, Reg-Reg,
Reg-Mem und Mem-Reg.
Varianten:
- ADC Ziel,Quelle: Addiert zusätzlich das Carry-Flag.
Sollte nämlich bei ADD ein Überlauf entstehen, so wird das Carry-Flag
gesetzt. Falls man z.B. 32-Bit-Zahlen addieren und ohne 386er-Befehle
auskommen will (also zuerst die unteren und danach die oberen 16 Bit
zusammenaddiert), so macht man dies also am besten, indem man zuerst die
unteren 16 Bits ADDet und danach die oberen 16 Bits ADCet.
- INC Wert: Erhöht das/die angegebe Register/Speicherstelle um 1.
Bei der Speicherstellen-Variante ist INC viel schneller als ein
vergleichbares ADD!
- SUB Ziel,Quelle: Subtraktion. Kombis: Reg-Wert, Mem-Wert, Reg-Reg, Reg-Mem,
Mem-Reg.
Varianten:
- SBB Ziel,Quelle: Äquivalent zu ADC, nur in der umgekehrten Richtung. :)
- DEC Wert: Äquivalent zu INC.
- MUL Wert: Multipliziert AL bzw. AX (je nachdem, ob der Wert ein Byte oder
ein Word ist) mit dem Wert. Das Ergebnis wird in AX bzw. DX:AX geschrieben.
- DIV Wert: Teilt AX (bei 8-Bit-Werten) bzw. DX:AX (bei 16-Bit-Werten) durch
den Wert. Das Ergebnis wird in AL bzw. AX und der Divisionsrest in AH bzw.
DX geschrieben.
Alles klar? Übrigens, wer nur mit Konstanten hantiert, kann sich all diese
Befehle sparen. Statt etwa MOV BX,OFFSET label mit anschließendem ADD BX,2
genügt es, MOV BX,OFFSET label+2 zu schreiben. Die Addition wird vom Assembler
selbst schon während der Assemblierung vorgenommen. Man spart ein wenig
Programmcode und Rechenzeit.
+++ Stringbefehle +++
Der Name ist vielleicht etwas irreführend. Stringbefehle dienen dazu, größere
Speicherblöcke zu bewegen. In Verbindung mit dem REP-Befehl ergeben sich echte
Power-Befehle! Beispielsweise reicht folgende Befehlssequenz, um im Mode 13h
den Bildschirm(speicher) zu löschen:
MOV AX,0A000h
MOV ES,AX
MOV CX,32000
XOR DI,DI
XOR AX,AX
CLD
REP STOSW
Die andere Möglichkeit wäre gewesen, den Bildschirmspeicher mit einer Schleife
und MOV zu löschen. Das wäre aber um einiges langsamer.
Wie funktioniert nun obige Befehlssequenz? Sehen wir uns die STOS-Befehls-
gruppe an. STOSB dient dazu, den Inhalt des AL-Registers an die Speicherstelle
ES:DI zu schreiben. Anschließend wird DI, je nachdem, ob das Directionflag
gesetzt ist, um 1 erhöht oder erniedrigt. In obigem Beispiel ist das DF
gelöscht, also wird DI erhöht. Allerdings verwenden wir nicht STOSB, sondern
STOSW! Diese Variante arbeitet mit Words, beschreibt also gleich zwei Bytes
des Arbeitsspeichers, entnimmt den Wert nicht aus AL, sondern aus dem
AX-Register, und verändert DI jeweils um 2.
Der zweite Teil der Arbeit wird von REP übernommen. REP dient dazu, den
nachfolgenden Befehl - in unserem Falle also STOSW - solange auszuführen, bis
CX=0 ist. Bei jedem Durchlauf wird CX um 1 erniedrigt. Alles klar?
Es gibt zwei Varianten von REP, welche zusätzlich das Zeroflag berück-
sichtigen. REPE (oder auch REPZ) wiederholt den Befehl solange, bis CX=0 oder
das Zeroflag gesetzt ist, und REPNE (REPNZ), bis CX=0 oder ZF gelöscht ist.
Diese beiden Varianten machen auf den ersten Blick wenig Sinn. Klar, denn sie
sind ja auch für andere Befehlsgruppen als für STOS gedacht! Nämlich für:
- CMPS: Vergleicht die Bytes an den Speicherstellen ES:DI und DS:SI. DI und SI
werden abhängig vom Directionflag verändert (siehe STOS).
=> Mit REPE CMPSB bzw. REPE CMPSW kann man Speicherblöcke mit einer Größe
von maximal 64 KByte, welche natürlich in CX gespeichert werden muß,
miteinander vergleichen. Ist danach ZF gesetzt oder CX ungleich 0, so
sind die Speicherblöcke nicht identisch. Am praktischsten ist es
natürlich, REPE CMPSB bzw. REPE CMPSW in Verbindung mit JCXZ zu
verwenden.
- SCAS: Ähnlich wie CMPS, jedoch wird anstelle von DS:SI AL/AX herangezogen.
Es gibt bei den Stringbefehlen neben B und W auch Doubleword-Varianten mit
einem D hinten dran, welche jedoch erst ab einem 386er funktionieren. Sie
arbeiten mit 4 Bytes gleichzeitig und benutzen die erweiterten Register (EAX,
EDI,...). Davon wird vielleicht noch in einer eigenen Kursfolge die Rede sein.
Eine weitere Befehlsgruppe ist LODS, welche AL/AX/EAX mit dem Inhalt von DS:SI
lädt. Natürlich wird SI dabei abhängig von DF verändert. Wenn man einen
Speicherbereich in einen anderen kopieren will, könnte man also so vorgehen,
daß man abwechselnd LODS und STOS aufruft. Dann ließe sich aber REP nicht mehr
verwenden, denn dieser kann nur einen Befehl ständig wiederholen. Deshalb gibt
es die Befehlsgruppe MOVS, welche beide Befehlsgruppen zusammenfaßt - die
Anweisung MOVSB führt abwechselnd zuerst LODSB und danach STOSB aus. Und damit
hätten wir alle Stringbefehlsgruppen durch.
+++ Assembler und Hochsprachen +++
So, nun zum letzten Kapitel dieser Kursfolge! Wie ihr sicherlich gesehen habt,
ist Assembler eine recht komplizierte Programmiersprache. Selbst für einfache
Aufgaben ist ein relativ großer Arbeitsaufwand erforderlich. Tatsächlich lohnt
es sich heutzutage in den meisten Fällen nicht, ein Programm komplett in ASM
zu coden. Normalerweise erstellt man nur die zeitkritischen Routinen in ASM
und macht den Rest in einer Hochsprache wie etwa C, Pascal oder Basic.
Es gibt mehrere Möglichkeiten, Assembler-Routinen in Hochsprachen-Proggies
einzubauen. Am einfachsten ist es, mit einem Inline-Assembler zu arbeiten.
Diese Möglichkeit bieten u.a. alle Compiler von Borland, die C(++)-Compiler
von Microsoft und Power Basic an. Mit einem Inline-Assembler lassen sich
ASM-Anweisungen direkt in den Quellcode des Hochsprachen-Programms einfügen.
Dazu muß man meistens einen Assembler-Block definieren.
Unter Borland (Turbo) Pascal sieht dies folgendermaßen aus:
ASM
{ hier kommen nun die Assembler-Befehle }
END;
Und so ist es in Borlands bzw. Microsofts (Quick/Visual) C:
asm { /* bei Microsoft-Compilern _asm */
/* hier kommen nun die Assembler-Befehle */
}
In Power Basic ist das Ganze leider ein wenig umständlicher. Dort muß man vor
jeden Assembler-Befehl entweder ASM oder das Rufzeichen schreiben.
Was unbedingt beachtet werden muß: Bestimmte Register dürfen nicht verändert
werden, sonst können keine Variablen mehr angesprochen werden. Es handelt sich
um die Register DS, SS, SP, BP sowie bei den Microsoft-Cs DI, SI und das
Directionflag. Falls man dennoch keine andere Möglichkeit hat, bspw. weil man
Stringbefehle benutzen muß oder ein Interrupt eines der "verbotenen" Register
verändert, muß man das betreffende Register auf dem Stack sichern und am Ende
des Assembler-Blocks wieder zurückholen. Falls auch das nicht möglich ist,
weil es sich um SS oder SP handelt, muß man eben das Register in einem anderen
Register zwischenspeichern. Irgendeine Lösung wird sich schon finden!
Schlimmstenfalls muß man auf den Inline-Assembler verzichten und auf die
zweite, etwas unbequemere Methode ausweichen: Man kann seine ASM-Module zu
OBJ-Dateien compilieren und diese zusammen mit der vom Compiler erzeugten
OBJ-Datei des Hochsprachenprogramms zu einer EXE-Datei zusammenlinken. Das hat
die Vorteile, daß man so auch in Sprachen, welche keinen Inline-Assembler
enthalten, die ASM-Routinen verwenden kann, und daß man ohne zu tricksen
Mnemonics verwenden kann, welche der Inline-Assembler nicht versteht - etwa
die der 386er-Befehle. Nähere Infos zum Einlinken von ASM-OBJs findet ihr in
euren Compiler-Handbüchern.
So, das war's für heute! Wer will, kann als Hausübung eine kleine
Mode-13h-Library zusammenbasteln, welche Prozeduren zum Setzen eines Pixels
(wobei als Parameter X-, Y-Koordinate und Farbe angegeben werden können), zum
Abfragen der Farbe eines Pixels und zum Ausfüllen des Bildschirms in einer
beliebigen Farbe enthält. Euer Adok!