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!