Assembler-Programmierung (1): Direkter Kurs auf den Prozessor

Assembler - keine andere Sprache gestattet dem Programmierer so direkten Zugriff auf den Computer, in keiner anderen entstehen so schnelle Programme.

Viele Computer-Besitzer, die eine Programmiersprache erlernen wollen, denken sich: »Warum Assembler" Es gibt viele andere Sprachen, die nahezu gleich schnell, aber viel übersichtlicher und leichter zu erlernen sind.« Das stimmt: Assembler ist verworren und oft sehr unübersichtlich. Aber: Assembler ist die Basis aller höheren Programmiersprachen und jeder, der Assembler kann, tut sich leichter, den Computer, das Betriebssystem und Hochsprachen zu verstehen. Für hochoptimierte (z. B. Sound-Sampling) und besonders hardware-orientierte (z. B. Laufwerkszugriffe) Funktionen ist Assembler ein Muß. Nicht umsonst sind viele Spiele in Assembler geschrieben.

Dieser Kurs führt Sie nicht nur in Assembler ein, sondern zeigt auch den praktischen Einsatz. Dabei lernen Sie die gängigen Fehlersuchmethoden, die Makroprogrammierung und die Einbindung von Assembler in Hochsprachen kennen.

Der Prozessor

Das Herz des Computers ist sein Prozessor. Das einzige, was der Prozessor tut, ist das Ausführen von Befehlen, z. B. »Schreibe den Wert 32 an die Speicheradresse 5000«. Die Befehle sind sehr elementar und in Werten codiert. So sagt beispielsweise die Bytefolge 34,42,0,32,0,0,17,88 dem Prozessor, daß die Speicheradresse 5000 mit dem Wert 32 zu füllen ist. In dieser Art gibt es eine nahezu unendliche Zahl an Prozessor befehlen - für die Speicheradresse 5001 gilt eine andere Bytefolge.

Da aber kein Mensch alle Befehle in dieser Form im Kopf behalten kann, entwickelten die Prozessorhersteller die Assembler-Sprache. Diese ist auf jedem Prozessor unterschiedlich - einfach deshalb, weil jeder Prozessor andere Befehle besitzt und auch gleichen Befehlen andere Werte zugeordnet sind. Ein Intel-Prozessor besitzt etwa eine völlig unterschiedliche Maschinensprache als ein Motorola-Prozessor.Das Herz des ST ist der Motorola-Prozessor mit dem Namen 68000 - dieser wurde vor über zehn Jahren entwickelt. Der Atari TT beispielsweise besitzt den Prozessor 68030 - ein Nachfolgemodell des 68000, das jedoch 68000-kompatibel ist. Dies ist möglich, da die Firma Motorola bei der Entwicklung von Nachfolgemodellen des 68000 (68010/20/30/40) darauf bedacht war, aufwärtskompatibel zu bleiben. Mit anderen Worten: Die alten Befehle bleiben gleich, es sind lediglich neue hinzugekommen.

In Assembler-Sprache sieht der obige Befehl folgendermaßen aus:

move.b#32,5000

Dieser Assembler-Befehl ist in vier Teile gegliedert: die Anweisung (»move«), die Operandenbreite (».b«), der erste (#32) und der zweite Operand (5000). Doch dazu später mehr. Um Assembler-Befehle in die maschinenlesbare Form zu bringen, ist der Assembler-übersetzer da - im Computerjargon der Einfachheit halber als »Assembler« bezeichnet. Um aus Assembler-Befehlen ein lauffähiges Programm zu machen, geht man normalerweise folgendermaßenvor:

Das Programm wird zunächst mit einem Texteditor in Assembler-Sprache entwickelt und in einer Quelltextdatei gesichert. Diese Dateien besitzen häufig die Endung ».s« oder ».src«. Daraufhin übersetzt der Assembler diese Datei in eine Objektdatei (».o«). Ein Linker erzeugt daraus schließlich das startfertige Programm. Viele Assembler erzeugen auch direkt das fertige Programm, ohne den Umweg über die Objektdateien zu gehen, so z. B. »Devpac«. Borlands » MAS«, »AS68« und andere erzeugen dagegen ausschließlich Objektdateien. Im »GFA Assembler« und »Turbo Ass« ist beides vorgesehen. Bild 1 zeigt Ihnen das Zusammenspiel von Quelltext, Programm und Prozessor.

Um die Assembler-Sprache zu verstehen, müssen wir erst einmal den Aufbau des Prozessors verstehen denn beides ist untrennbar miteinander verbunden. Jeder Prozessor besitzt Register. Das sind interne Speicherbereiche. Sie speichern Werte, die zu einem späteren Zeitpunkt wieder benütigt werden - oder die mit einem anderen Wert oder anderem Register mittels einer mathematischen Operation verknüpft werden.

Die Prozessorregister

Der 68000 kennt drei Arten von Registern: Daten- und Adressregister und spezielle Register. Von den Daten und Adressregistern gibt es jeweils acht Stück. In Assembler heißen sie D0 (DatenregisterNummer 0) bis D7 und A0 (Adressregister Nummer 0) bis A7. Alle Registersind 32 Bit breit. Das bedeutet, daß sie 32 Bit-Werte (4 Byte) aufnehmen können. An speziellen Registern besitzt der 68000 das Statusregister (SR), den Programmzähler (PC, englisch, Program Counter) und das Stackregister (SP, englisch Stack Pointer). Eine übersicht aller Register sehen Sie in Bild 2.

Die Datenregister sind, wie ihr Name bereits andeutet, dazu da, Daten zu speichern. Datenregisterkönnen 1 Byte, 1 Wort (2 Byte) oder ein Langwort (4 Byte) auf einmal aufnehmen. Dazu versieht man den Assembler-Befehl mit einem Zusatz; ».b« für Byte, ».w« für Wort und ».l« fürLangwort. Dazu einige Beispiele:

move.b   #10,D0          * den Bytewert 10 in D0 
move.w   #10,D0          * den Wortwert 10 in D0
move.l   #10,D7          * den Langwortwert 10 in D7
move.l   #$ffffeeee,D0   * hexadezimal
move.b   #%110111,D0     * binär

Die Anweisung lautet in allen Beispielen »move« und besagt, daß der erste Operand in den zweiten kopiert wird. Der erste Operand ist ein absoluter Wert - das wird durch das »#« deutlich. Assembler verarbeiten drei Zahlennotationen: dezimal, hexadezimal und binär. Dezimalzahlen sind der Normalfall und enthalten die Ziffern »0« bis »9«; hexadezimale Zahlen werden durch ein »$« eingeführt und enthalten zusätzlich noch die Ziffern »a« bis »f«. Binäre Zahlen kennzeichnen Sie durch »%«; sie dürfen nur »0« und »1« enthalten. Der zweite Operand ist ein Register - in obigen Beispielen Datenregister. Kommentare werden durch »« eingeführt und gelten bis zum Ende der Zeile. Viele Assembler verwenden neben dem »« auch »;« als Kommentarstart.

Im ersten Beispiel kopieren wir den Bytewert 10 nach D0, im zweiten den Wortwert 10. Was ist in diesem Beispiel der Unterschied zwischen Byte- und Wortwert? Zum einen sind Befehle mit Bytewerten kürzer, denn 10 wird zu $a codiert anstatt wie beim Wortwert zu $00a. Zum anderen läßt der Bytewert die übrigen 24 Bit des Registers unberührt. Angenommen D0 enthält $ffeeddcc. Nach Ausführung von »move.b #10,D0« würde es

$ffeedd0a enthalten, nach »move.w #10,D0« dagegen $ffee000a.

Die zweite Art von Registern, die Adressregister, dienen primär zum Speichern von Adressen. Mit ihnen läßt sich rechnen und das ist das wichtigste auf Speicheradressen zugreifen. Im Gegensatz zu Datenregistern dürfen Sie Adressregistern keine Bytewerte (».b«) zuordnen, sondern lediglich Würter und Langwürter. Betrachten wir folgende Beispiele:

(1) move.b  #10,5000 
(2) move.l  #5000,A0
    move.b  #10,(A0)

Der erste Befehl kopiert (»move.b«) den Bytewerte 10 in die Speicheradresse 5000. Dasselbe macht auch das zweite Beispiel - nur nicht direkt, sondern über den Umweg eines Adressregisters. Mit dem ersten Befehl kopieren wir den Langwortwert 5000 in das Adressregister 0. Der zweite Befehl kopiert den Byte wert an die Adresse, auf die das Adressregister 0 zeigt - in diesem Fall 5000. Im Fachjargon bezeichnet man diesen Zugriff als indirekte Adressierung, da die Adresse nicht direkt - wie im ersten Beispiel - angegeben wird, sondern sich über das Adressregister ergibt.

Neben der indirekten Adressierung kennt der 68000-Prozessor noch 17 weitere Adressierungsarten. Davon ergeben viele erst Sinn, wenn wir weitere Assembier-Befehle kennen. Aus diesem Grund gehen wir darauf erst später genauer ein.

Elementare Assembler-Befehle

Um ein Programm zu schreiben, reicht der »move«-Befehlnoch lange nicht aus. Deswegen machen wir uns nun mit weiteren elementaren Assembler-Befehlen vertraut. Um Werte zu addieren, gibt es den »add«-, zum Subtrahieren den »sub«-Befehl. Beide sind äusserst flexibel und verarbeiten Byte- (nur bei Datenregistern), Wort- und Langwortoperanden, wobei die Operanden aus absoluten Werten, Registern oder sonstigen Adressierungsarten aufgebaut sein dürfen. Ein Beispiel:

move.w     #10,D0    
add.w    #20,D0    *10+20=30
sub.w    #10,D0    *30-10=20

In diesem Beispiel addieren und subtrahieren wir Werte zum Datenregister D0. Das Ergebnis dieser Operationen legt der Prozessor immer im Zieloperanden - hier D0 - ab. Aus diesem Grund ist beispielsweise der Befehl »add.w D0,10« nicht zulässig, da »10« ein absoluter Wert ist und keinen Ort beschreibt, an dem der Prozessor das Ergebnis speichern könnte.


**Bild 1. Zunächst übersetzt der Assembler (und ggf. der Linker) den Duelltext in ein fertiges Programm. Der Prozessor führt die Maschinenbefehle des Programms über den PC (Program Counter) aus. **Um ein Register oder eine Adresse auf einen bestimmten Wert hin zu überprüfen, kennt der Prozessor den Befehl »cmp«. Er vergleicht die beiden Operanden miteinander. Das Ergebnis legt er im Statusregister ab. Dieses spezielle Register ist in zwei Teile untergliedert, das Systembyte und das CCR (Condition Code Register, zu deutsch Zustandscode-Register). Das Systembyte interessiert uns an dieser Stelle noch nicht; der Vergleich verändert lediglich das CCR (Bild 3). Das CCR besteht aus fünf Bit-Flags, die als Extension-, Negativ-, Zero-, Overflow- und Carry-Flag bezeichnet sind.

Sind beispielsweise bei einem Vergleich beide Werte identisch, so wird das Zero-Flag gelöscht. Das gleiche passiert, wenn bei einer Subtraktion bzw. Addition das Ergebnis Null ist. Ist das Ergebnis negativ, d. h. hat es den Wert 0 unterschritten, so wird das Negativ-Flag gesetzt. Um auf den Zustand der CCR-Flags zu reagieren, gibt es einen zustandsabhängigen Sprungbefehl.

Dieser verzweigt zum angegebenen Ziel, wenn der verlangte Flag-Zustand wahr ist. Ansonsten macht der Prozessor mit dem nächsten Befehl weiter.

move.w    #10,D0 
add.w     #20, D0   *10+20= 30
cmp.w     #30,D0    * ist D0 = 30 wahr?
beq       labell     *ja, dann springe zu labell
move.w    #30,D0    * nein,dann hier weiter...
label1...

In diesem Beispiel testen wir mit »beq« (Branch if equal), ob das Zero-Flag gelöscht ist. Wenn ja, dann soll die Programmausführung ab der mit dem Label »label1« markierten Zeile weitermachen. Labels sind Sprungmarken; sie stehen am Anfang der Zeile. Ihnen folgt ein Doppelpunkt. In den Befehlen ersetzen sie absolute Adressen, wie etwa 5000. Der Assembler ermittelt die zum Label gehörende Speicheradresse bei der übersetzung und setzt diese anstatt des Labels in die Befehle ein.

»beq« gibt es noch viele weitere bedingte Sprunganweisungen - alle in der Form »bcc« (siehe Tabelle 2). Mit ihnen lassen sich auch Schleifen programmieren. Das folgende Beispiel addiert solange den Wert 10 zu D0, bis der Wert 100 erreicht ist. Dazu benutzen wir »bne« (Branch if Not

Equal). Dieser Befehl verzweigt, wenn das Zero-Flag nicht gelüscht ist - oder mit anderen Worten: wenn die verglichenen Werte nicht übereinstimmen.

    move.w    #0,D0      *Initialisieren
loop:
    add.w     #10,D0     *DO = D0+#10
    cmp.w     #100,D0    *100 erreicht?
    bne       loop

Die bedingten Sprungbefehle sind auf eine Sprungdistanz von 32 KByte beschränkt, benötigen dafür aber auch nur 4 Byte Speicherplatz. Ist die Sprungdistanz geringer als 128 Byte, so können Sie zusätzlich die »kurzen« Sprünge verwenden - diese sind erstens schneller und zweitens kürzer(2 Byte). Dazu hängen Sie einfach dem Sprungbefehl die Endung ».s« (Short) an.


**Bild 2. **Der 68000-Prozessor besitzt acht Datenregister (D0 bis D7/, sieben Adressregister (A0 bis A6), zwei Stackregister (USP,SSP), einen Programmzähler (PC) und ein Statusregister (SR/. Dieses ist in das Systembyte und dem CCR untergliedert.**Mit unseren bisherigen Kenntnissen und einigen neuen Sprachelementen können wir bereits ein sinnvolles Programm schreiben:

1:    clr.w  D0                    *entspr.    mowe.w #0,D0  
2:    move.l     #$f8000,A0
3:loop:
4:    move.w     D0,(A0)+
5:    emp.l      #$f9000,A0   *$f9000 erreicht?
6:    bne.s      loop         *kurzer Sprung
7:    rts

In Zeile 1 verwenden wir den neuen Befehl »clr« (Clear). Er ist ein schnellerer und übersichtlicherer Ersatz für »move.w #0«. Wollen wir das Beispiel optimieren, so entfernen wir die erste Zeile und ersetzen die vierte Zeile durch »clr.w (A0)+«.

Die zweite Zeile des Beispiels initialisiert das Adressregister A0 mit dem hexadezimalen Wert $f8000 (bei STs mit 1 MByte die Bildschirmadresse). Die dritte Zeile ist eine Sprungmarke; das Label heißt »loop«.

Die nächste Zeile führt eine neue Adressierungsart ein, die »indirekte Adressierung mit Post-Inkrement«. Dies bedeutet, daß es sich um eine indirekte Adressierung handelt, wobei nach erfolgtem Zugriff das Adressregister erhüht wird. Dabei handelt es sich um eine Langwort-Operation, die etwa mit »add.l #X,A0« zu vergleichen ist. In unserem Fall erhüht sich das Register um ein Wort (2 Byte), da der Zugriff auf Wortbasis erfolgt. Würden wir »move.l D0,(A0)+« schreiben, so würde A0 um ein Langwort, bei »move.b D0,(A0)+« lediglich um 1 Byte erhöht. Angenommen A0 zeigt auf 10000, so hat es nach Ausführung dieser Zeile den Wert 10002.

In der fünften Zeile vergleichen wir A0 mit dem absoluten Wert $f9000. Solange dieser noch nicht erreicht ist (Zero-Flag gesetzt), verzweigt der folgende Befehl »bne« an die Sprungmarke »loop«. Der Befehl in der letzten Zeile, »rts«, soll uns an dieser Stelle noch nicht interessieren und dient in diesem Beispiel lediglich dazu, das Programm zu beenden.

Dies ist der geeignete Zeitpunkt, um auf das letzte noch nicht vorgestellte Prozessorregister einzugehen, das PC-Register. Dieses Register ist - bildlich dargestellt - das Lesezeichen des Prozessors, Es enthält stets die Adresse des nächsten vom Prozessor abzuarbei tenden Befehls. Will der Prozessor einen Befehl verarbeiten, so liest er die Befehlsadresse indirekt über den PC. Dabei erhöht sich der PC automatisch, so daß er auf den nächsten Befehl zeigt. Diese Distanz beträgt beim 68000-Prozessor je nach Befehl zwischen 2 und 10 Byte. Stößt der Prozessor auf einen auszuführen den Sprungbefehl, so schreibt er zunächst die neue Befehlsadresse in den PC und greift anschließend in üblicher Weise darauf zu.

Sie sollten wissen, daß der Prozessor nur auf gerade Adressen zugreifen kann (z. B. 0, 1000, 66666). Zeigt der PC auf eine ungerade Adresse, so verursacht der Prozessor einen Adress-Fehler. Dieser wird wiederum vom Betriebssystem abgefangen, das drei Bomben auf dem Bildschirm darstellt.


**Bild 3. Das Statusregister enthält das Systembyte und das CCR (Condition Code Register/. Dieses besteht aus fünf Flags. Mit den Assembler-Befehlen »bcc« und »dbcc« reagieren Sie auf bestimmte Zustände (siehe auch Tabelle 1).**Wir haben bisher nur bedingte Sprungbefehle kennengelernt. Oft ist es jedoch nötig, ohne Bedingung zu verzweigen. Dafür kennt der 68000 zwei Befehle: »bra« (Branch Always) und »jmp«(Jump). Ersterer gehört zu der Befehlskategorie »bcc«(wie etwa auch »beq«) und ist wie alle anderen Sprüngedieser Kategorie auf eine Sprungdistanz von 32 KByte begrenzt. Bei »jmp« ist die Sprungdistanz unbegrenzt. Der Vorteil von

»jmp« liegt aber vielmehr darin, daß sich die Zieladresse aus nahezu jeder beliebigenAdressierungsart ergibt.

 1:  loop:
2:    add.1      D1,D0.      * D0=D0+Dl
3:    tst.1     D0          * ist D0 gleich 0 "
4:    beq       istnull
5:    move.1    D0,A0       * D0 nach A0 kopieren
6:    jmp(A0)               * indirekt über A0 springen
7: istnull:
8:    add.1      #10,D0      * D0=D0+10
9:    braloop               * das ganze von vorne

Das obige Beispiel ist zugegebenermaßen nicht besonders sinnvoll, zeigt aber sehr schön den Unterschied zwischen »bra« und »jmp«. Im Gegensatz zu »bra« ist die Zieladresse bei »jmp« nicht erkennbar und ergibt sich erst während der Programmausführung. In der dritten Zeile finden Sie außerdem einen neuen Befehl, »tst«. Er entspricht »cmp #0«, d. h. er vergleicht seinen Operanden mit Null. In dem Beispiel könnten wir diesen Befehl entfernen, und das Programm würde trotzdem genauso funktionieren. Dies liegt daran, daß bereits durch den vorausgehenden »add.1 D1,D0« die Zustandsflags gesetzt werden. Ist das Ergebnis der Addition Null, so wird u. a. das Zero-Flag gelöscht. Dies gilt nicht nur für »add«, sondern für nahezu alle Befehle (außer für »move«), die den Zieloperanden verändern.

Tabelle.

Alle Endungen für die Befehlskategorien »Bcc« und »DBcc«, wobei »cc« der aufgeführten Endung entspricht. Bei den Flagkombinationen entspricht »&« einem logischen Und, »I« einem Oder und »!« einem Nicht.

Für die Schleifenprogrammierung gibt es beim 68000Prozessor eine sehr praktische Befehlskategorie, die sich an »bcc« anlehnt: » dbcc« . Genaugenommen sind diese Befehle lediglich eine Kombination aus zwei anderen uns bereits bekannten Befehlen: »sub« und »bcc«. »dbcc« verlangt zwei Operanden, ein Datenregister und ein Label. Stößt der Prozessor auf einen dieser Befehle, so verringert er zunächst das angegebene Datenregister um 1. Dies ist stets eine Wortoperation (»sub.w #1,Dx«). Ist das Ergebnis größer oder gleich Null, so verzweigt der Prozessor an die Sprungmarke. Ist das Ergebnis kleiner Null (negativ), so führt der Prozessor den folgenden Befehl aus. Eine Warteschleife sieht so aus:

      move.w     #10000,D0 
loop:
      nop
      dbra      D0,loop
      rts

Dieses Beispiel verwendet einen neuen Befehl, »nop«. »nop« ist ein Abkürzung für »No Operation« und sagt bereits alles aus, was der Befehl macht: nichts. Eine Anmerkung noch zu »dbcc«: Da das Datenregister in einer Wortoperation um 1 erniedrigt wird und somit die oberen 16 Bit des Registers unberührt bleiben, kann die Schleife maximal 65536 mal (_ $ffff) durchlaufen werden.

Der letzte Befehl, den wir diesmal kennenlernen, heißt »lea«. Er ordnet einem Adressregister eine Adresse zu. Diese ergibt sich aus einer (fast) beliebigen Adressierungsart.

1: lea     40(A0),A1   
2: jmp     (A1)

In Zeile 1 weisen wir A1 die Adresse zu, auf die A0 zeigt plus den Wert 40. Zeigt A0 z.B. auf 10000, so würde A1 10040 enthalten. In Zeile 2 springt das Programm an diese Adresse. Zum Abschluß dieses Kursteils finden Sie ein Programm, das den Speicher des ST löscht und anschliessend einen Reset verursacht. Die mit »e« versehenen Zeilen enthalten Befehle, die wir erst in den folgenden Kursteilen kennenlernen.

start: 
clr.l        -(sp)
move.w      #$20,-(sp)
trap        #1
move.w      #$2700,sr
move.1           $42e,A2      * $42e ist eine System-
                          *  variable und zeigt auf 
        nbsp;                 *  das Ende des Speichers 
move.1           $4f2,A3       * Anfang des Betriebssystems
                            * bzw. Anfang der  Reset 
                             * routine 
    lea           $ A0          * A0 zeigt  auf   Adresse $8 
    lea          start,Al     * A1 zeigt auf Programmstart
clear1:
    clr.w           (A0)+        * Speicher löschen
     cmp.1         A0,A1        * Programmstart erreicht "
    bne         clear1       * nein, noch nicht 
    lea            ende,A0       * A0 zeigt auf Programmende
 
clear2   :
    clr.w        (A0)+          * Speicher lüschen 
    cmp.1          A0,A2         * Speicherende erreicht?
    bne            clear2        * nein, noch nicht
    jmp              (A3)          * Resetroutine aufrufen
 
ende: 

Kursübersicht


Martin Backschat
Aus: TOS 12 / 1990, Seite 72

Links

Copyright-Bestimmungen: siehe Über diese Seite