Adok's Way to Assembler
Folge 6

Hallo Freunde! Fr 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 +++

Fr  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 knnen Parameter bergeben werden,
was  meistens  mit  Hilfe des Stacks  getan  wird. Funktionen sind hnlich wie
Prozeduren,  knnen aber auch einen Wert zurckgeben. In Hochsprachen wird der
Rckgabewert  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 Rcksprungsoffset auf  den  Stack und fhrt einen
JMP-Befehl aus. Am Ende jeder Prozedur mu RETN (bzw. bei Interrupt-Prozeduren
IRET)  stehen.  Sobald  der Prozessor auf RETN  stt,  holt er den Offset der
Rcksprungsadresse  vom Stack und fhrt 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 fr 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 Rcksprungadresse 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 abhngig 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 Rcksprungsoffsets
 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         ;Rcksprungsoffset auf Stack legen
 RET             ;Zurck 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 resprsentiert jedes Byte dieses Segments die Farbe eines Bildpunkts
(Pixels).  Wollen wir den Punkt mit  den Koordinaten x/y ansprechen, so mssen
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  (fr  gewhnlich  wei)  zu  setzen.  Als  Parameter  mu  der  Offset des
gewnschten 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 gedrckten Taste ablesen.
Der  Scancode ist eine Zahl zwischen 0 und 127 (7 Bits). Ist Bit 8 gesetzt, so
wird  momentan  keine  Taste gedrckt. Die  unteren  7 Bits enthalten dann den
Scancode der zuletzt bettigten Taste.

Der  Scancode  von  ESC  hat  die  Nummer  1.  Eine  komplette bersicht aller
Scancodes  knnt  ihr u.a. in der  QBasic-Hilfe  unter dem Thema Tastaturcodes
finden.

Der  Rest  des Beispielprogramms mte klar  sein.  Verget niemals, am Anfang
jeder  Prozedur  zuerst die Rcksprungadresse und  danach die Parameter in der
richtigen  Reihenfolge  zu POPen! Am Ende  jeder Prozedur mssen Paramater und
Rcksprungadresse  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
Rcksprungadresse bergeben, und der Computer strzt ab!

+++ Makros +++

Makros  unterscheiden sich von PROCs in einem Punkt: Sie werden nicht geCALLt,
sondern  beim  Assemblieren direkt in  den  Programmcode eingefgt. Das belegt
zwar  je nach Gre der Makros ein wenig bis sehr viel mehr Speicherplatz, ist
aber  dafr  um  ein  paar Nanosekunden  schneller.  Ein  Makro wird mit MACRO
macroname  definiert  und hrt 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  wit  ihr  schon.  ;-)   Also,  folgende  Befehle  hlt  ASM  zu
Rechenzwecken bereit:

- ADD Ziel,Quelle:   Addition.   hnlich  wie   bei  MOV  sind  nur  bestimmte
  Ziel-Quelle-Kombinationen  mglich,  und zwar:  Reg-Wert, Mem-Wert, Reg-Reg,
  Reg-Mem und Mem-Reg.

  Varianten:
  - ADC Ziel,Quelle: Addiert zustzlich das Carry-Flag.
    Sollte  nmlich  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: Erhht 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 anschlieendem ADD BX,2
gengt es, MOV BX,OFFSET label+2 zu schreiben. Die Addition wird vom Assembler
selbst  schon  whrend  der  Assemblierung  vorgenommen.  Man  spart ein wenig
Programmcode und Rechenzeit.

+++ Stringbefehle +++

Der  Name ist vielleicht etwas irrefhrend. Stringbefehle dienen dazu, grere
Speicherblcke 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 lschen:

MOV AX,0A000h
MOV ES,AX
MOV CX,32000
XOR DI,DI
XOR AX,AX
CLD
REP STOSW

Die andere Mglichkeit wre gewesen, den Bildschirmspeicher mit einer Schleife
und MOV zu lschen. Das wre 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. Anschlieend wird DI,  je  nachdem, ob das Directionflag
gesetzt  ist,  um  1  erhht oder erniedrigt.  In  obigem  Beispiel ist das DF
gelscht,  also wird DI erhht. 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 verndert 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 auszufhren, bis
CX=0 ist. Bei jedem Durchlauf wird CX um 1 erniedrigt. Alles klar?

Es  gibt  zwei  Varianten  von REP,  welche  zustzlich  das  Zeroflag berck-
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 gelscht ist.

Diese  beiden Varianten machen auf den ersten Blick wenig Sinn. Klar, denn sie
sind ja auch fr andere Befehlsgruppen als fr STOS gedacht! Nmlich fr:

- CMPS: Vergleicht die Bytes an den Speicherstellen ES:DI und DS:SI. DI und SI
  werden abhngig vom Directionflag verndert (siehe STOS).
  => Mit REPE CMPSB bzw.  REPE CMPSW kann man  Speicherblcke  mit einer Gre
     von  maximal  64 KByte,  welche natrlich in CX gespeichert  werden  mu,
     miteinander  vergleichen.  Ist danach ZF gesetzt  oder CX ungleich 0,  so
     sind  die  Speicherblcke  nicht  identisch.   Am  praktischsten  ist  es
     natrlich,  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
ldt.  Natrlich  wird  SI  dabei abhngig  von  DF  verndert. Wenn man einen
Speicherbereich  in einen anderen kopieren will,  knnte man also so vorgehen,
da man abwechselnd LODS und STOS aufruft. Dann liee sich aber REP nicht mehr
verwenden, denn dieser kann nur einen Befehl stndig wiederholen. Deshalb gibt
es  die  Befehlsgruppe  MOVS, welche  beide  Befehlsgruppen zusammenfat - die
Anweisung MOVSB fhrt abwechselnd zuerst LODSB und danach STOSB aus. Und damit
htten 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 fr einfache
Aufgaben ist ein relativ groer Arbeitsaufwand erforderlich. Tatschlich lohnt
es  sich heutzutage in den meisten Fllen  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 Mglichkeiten,  Assembler-Routinen  in Hochsprachen-Proggies
einzubauen.  Am  einfachsten ist es,  mit  einem Inline-Assembler zu arbeiten.
Diese  Mglichkeit  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 einfgen.
Dazu mu man meistens einen Assembler-Block definieren.

Unter Borland (Turbo) Pascal sieht dies folgendermaen 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 umstndlicher. Dort mu man vor
jeden Assembler-Befehl entweder ASM oder das Rufzeichen schreiben.

Was  unbedingt beachtet werden mu:  Bestimmte Register drfen nicht verndert
werden, sonst knnen 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 Mglichkeit hat, bspw. weil man
Stringbefehle  benutzen mu oder ein Interrupt eines der "verbotenen" Register
verndert,  mu man das betreffende Register auf dem Stack sichern und am Ende
des  Assembler-Blocks  wieder zurckholen. Falls  auch  das nicht mglich ist,
weil es sich um SS oder SP handelt, mu man eben das Register in einem anderen
Register zwischenspeichern. Irgendeine Lsung 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. Nhere Infos zum  Einlinken von ASM-OBJs findet ihr in
euren Compiler-Handbchern.

So,   das  war's  fr  heute!  Wer   will,  kann  als  Hausbung  eine  kleine
Mode-13h-Library  zusammenbasteln,  welche Prozeduren  zum Setzen eines Pixels
(wobei  als Parameter X-, Y-Koordinate und Farbe angegeben werden knnen), zum
Abfragen  der  Farbe eines Pixels und  zum  Ausfllen des Bildschirms in einer
beliebigen Farbe enthlt. Euer Adok!
