2.2 Entwicklungswerkzeuge 2.4 Entwicklungswerkzeuge zum Praktikumsrechner

2.3/1

2.3 Einführung in die Assemblerprogrammierung

2.3.1 Grundlegendes zur Assemblerprogrammierung

Maschinenorientierte Sprachen versus problemorientierte Sprachen

Assemblersprachen stehen in unmittelbarem Zusammenhang mit der zugrundeliegenden Maschinensprache und sind somit stark prozessorabhängig. Bei der Entwicklung von Assemblerprogrammen ist es daher nicht möglich, sich ausschließlich auf die Umsetzung des Algorithmus in das Programm zu konzentrieren, so wie man es von höheren Programmiersprachen gewöhnt ist. Assemblersprachen orientieren sich in ihrer Semantik an den implementierten Befehlen des Prozessors und nicht an einer problemorientierten und den Programmierer unterstützenden Darstellung der zu modellierenden Algorithmen. Dieser Umstand behindert zum einen eine zügige Umsetzung des verwendeten Algorithmus in das Assemblerprogramm, eröffnet zum anderen aber die Möglichkeit einer effizienteren Lösung des Problems als in einer Hochsprache. Nach einer Studie von Motorola treten Maschinenbefehle in 8-bit Prozessoren in Standard-Anwendungen mit folgender Häufigkeit auf:

  
Transferbefehle39%
Verzweigungsbefehle17%
Unterprogrammaufrufe13%
Arithmetische Befehle3%
Vergleichsbefehle6%
restliche Befehle22%

Der hohe Prozentsatz an Transferbefehlen belegt, daß allein eine geschickte Programmierung der Ein- und Ausgabe auf Assemblerebene einen Geschwindigkeitsgewinn erbringen kann.

Anforderungen an Assemblierer

Assemblierer müssen aus der Sicht des Entwicklers ein Mindestmaß an Anforderungen erfüllen, damit ein produktives Programmieren garantiert ist:

2.3/2

Bei der Entwicklung größerer Programmroutinen können zusätzlich folgende Optionen nützlich sein:

2.3.2 Funktionsweise von Assemblierern

Um die grundlegenden Anforderungen, die an einen Assembler gestellt werden, zu erfüllen, benutzt ein Standard-Assembler als Datenstrukturen mindestens zwei Tabellen und einen Zähler.

Informationen über den zugrundeliegenden Befehlssatz sind in der sogenannten Zuordnungstabelle (Befehlstabelle) statisch gespeichert. Sie ermöglicht eine Abbildung zwischen der symbolischen Darstellung der Prozessorbefehle (und ihren Adressierungsarten) und den zugehörigen binären Bitmustern der Maschineninstruktionen. Darüber hinaus ist in dieser Tabelle für jeden Befehl die Anzahl der im Speicher zu reservierenden Bytes und die Anzahl der benötigten Prozessortaktzyklen abgelegt. Die Zuordnungstabelle entspricht in ihrem grundlegendem Aufbau den ersten Feldern der Tabellen B-1, B-2 und B-3 des Anhangs B der ersten Kurseinheit.

Zur Verwaltung der vom Programmierer definierten Symbole baut der Assembler während des Übersetzungsvorganges die Symboltabelle auf. In dieser Tabelle werden alle Symbole mit ihren Werten (Konstanten, Sprungziele) abgelegt. Auf Anforderung durch den Benutzer wird diese Tabelle vom Assembler ausgegeben.

Zur Vergabe von eineindeutigen Speicheradressen für die einzelnen Befehle des zu generierenden Maschinenprogramms ist ein Zähler, der Adreßzähler, notwendig. Er wird im Laufe der Abarbeitung um die jeweils benötigte Anzahl von Bytes (Zuordnungstabelle) für den aktuell zu verarbeitenden Befehl inkrementiert. Der Adreßzähler modelliert den Programmzähler des Prozessors.

Neben den obigen Datenstrukturen ist für einen Assembler im wesentlichen nur noch ein Parser (to parse: Satz grammatikalisch zergliedern), ein Modul zur Fehlerbehandlung (exception handling), ein Modul zur Ein-/Ausgabebehandlung und ein Modul zur Steuerung des Übersetzungsvorganges notwendig. Ein Parser ist eine Programmroutine, die eine eingelesene Programmzeile des Quellprogrammes (String) syntaktisch korrekt nach token (hier: Symbole, Mnemonics, Operanden) durchsucht und die Befehlszeile entsprechend der Syntax in Felder zerschneidet (softwaretechnisch wird ein Ableitungsbaum mittels einer kontextfreien Grammatik erzeugt).

2.3/3

Der Übersetzungsvorgang ist ein mehrmaliges sequentielles Lesen und Verarbeiten des Quellprogramms. In der Regel werden für die Generierung eines Maschinenprogramms zwei Lesezyklen (two pass assembler) benötigt, um alle Referenzen auflösen zu können. Bei Assemblern mit sehr mächtigen Assembler-Direktiven ist noch ein dritter Durchlauf nötig. Das Quellprogramm wird von dem Assembler Zeile für Zeile abgearbeitet. Als Ergebnis ist das erzeugte Maschinenprogramm eine 1:1-Übersetzung des eingegebenen Quellprogramms.

Bei einem two pass assembler wird im ersten Durchlauf die Symboltabelle aufgebaut und versucht, allen Symbolen während dieses Durchlaufes ihre zugewiesenen bzw. zu berechnenden Werte zuzuordnen. Im zweiten Durchlauf werden Assembler-Direktiven nochmals berechnet und das Maschinenprogramm generiert. Im allgemeinen ist es nicht möglich, die Übersetzung in einem Durchlauf durchzuführen, da Vorwärts-Referenzen (forward reference) nicht sofort aufgelöst werden können. Beispielsweise hat das folgende Codefragment eine Vorwärts-Referenz, d.h. der Marke "NEXT" wird erst in einer der folgenden Programmzeilen ein Wert (Adresse) zugewiesen.

     
  CMPA #$0100
   BEQNEXT
   ......
 NEXT:EXG X,Y

Da die Zuweisung später erfolgt, kann im ersten Durchlauf dem Befehl "BEQ" nicht sofort die relative/absolute Adresse des Sprungziels als Operand mitgegeben werden. Aus diesem Grund wird im ersten Durchlauf eine vollständige Symboltabelle aufgebaut, durch die im zweiten Durchlauf die Generierung des Maschinenprogrammes ermöglicht wird. Wird ein Sprungziel zuerst definiert und später darauf Bezug genommen (backward reference), so kann eine Zuordnung sofort erfolgen. Für Interessierte ist im folgenden der genaue Programmablauf einer Assemblierung beschrieben.

Pass 1

Zu Beginn des Übersetzungsvorganges wird der Adreßzähler mit Null initialisiert. Nach Einlesen der ersten Programmzeile und fehlerfreiem Zerlegen in einzelne token, wird überprüft, ob das Programmende erreicht ist und gegebenenfalls zu pass 2 verzweigt. Ist dies nicht der Fall, so wird eine eventuell vorliegende Assembler-Direktive verarbeitet und danach ein eventuell vorhandendes Symbol (Sprungziel) in die Symboltabelle (bei backward reference mit Speicheradresse entsprechend dem Inhalt des Adreßzählers) aufgenommen. Danach wird der Adreßzähler entsprechend der benötigten Bytes des vorliegenden Befehls erhöht und die nächste Programmzeile eingelesen. Wird ein syntaktischer Fehler bei der Zerlegung erkannt bzw. wird ein Symbol im Quellcodeprogramm zweimal definiert, so wird eine Fehlermeldung für diese Programmzeile ausgegeben und der Übersetzungsvorgang gestoppt.

2.3/4

Pass 2

Zu Beginn des zweiten Durchlaufs wird die Symboltabelle überprüft und bei nicht- oder falschdefinierten Symbolen eine Fehlermeldung ausgegeben. Danach wird die erste Programmzeile eingelesen und zerteilt. Im Anschluß wird überprüft, ob das Programmende erreicht ist. Ist dies der Fall, so wird das generierte Maschinenprogramm abgespeichert, gewünschte Tabellen für Benutzer ausgegeben und das Programm beendet. Ist das Programmende nicht erreicht, so wird der Operand einer eventuell vorhandenen Assembler-Direktive berechnet und der Adreßzähler gegebenenfalls erhöht und die nächste Programmzeile eingelesen. Bei Vorliegen eines Befehls wird der Operand (mit Hilfe der Symboltabelle) berechnet und der Maschinencode für den Befehl erzeugt. Danach wird der dreßzähler entsprechend der benötigten Bytes des verarbeiteten Befehls erhöht und die nächste Programmzeile eingelesen.

Praktische Übung P2.3-1:
Entwerfen Sie je ein Flußdiagramm für den ersten Durchlauf sowie für den zweiten Durchlauf des Übersetzungsvorganges bei einem two pass assembler.

2.3.3 Syntax von Assemblerprogrammen

Assemblerprogramme bestehen aus einer Folge von Befehlszeilen. Jede Befehlszeile besteht aus einer Sequenz von ASCII-Zeichen, die in der Regel mit einem Zeilenende-Zeichen (line feed) abgeschlossen ist. Der gültige Zeichensatz von Assemblern unterscheidet sich in den meisten Fällen nur geringfügig, allerdings kann sich die Semantik bei der Anwendung der Sonderzeichen stark unterscheiden.

Befehlszeilen von Assemblerprogrammen sind überwiegend in die Felder "Marke" (Label), "Operator", "Operand" und "Kommentar" nach folgendem Schema aufgeteilt:

MarkeOperatorOperandKommentar

Die ersten drei Felder werden meistens durch mindestens ein Leerzeichen voneinander getrennt. Das Kommentarfeld wird in der Regel durch einen führenden Strichpunkt eingeleitet. Das Operatorfeld muß, falls das Markenfeld leer ist, durch mindestens ein Leerzeichen oder ein Tabulator-Zeichen vom Zeilenanfang getrennt sein.

Im Markenfeld werden Symbole als Sprungziele oder als Konstanten definiert. Diese Symbole erhalten den aktuellen Inhalt des Adreßzählers oder eine Konstante als Wert zugewiesen. Die Definition dieser Symbole ist nur einmal möglich, d.h. der Wert bleibt während der ganzen Laufzeit des Programmes konstant. Werden Symbole als Sprungziele definiert, so fordern manche Assembler, die Symbole mit einem Doppelpunkt abzuschließen.

Im Operatorfeld stehen die Mnemonics (symbolische Darstellung der Prozessorbefehle) oder Assembler-Direktiven. In einigen Syntaxbeschreibungen von Assemblern wird die Unterscheidung zwischen Mnemonics und Assembler-Direktiven durch einen vorgestellten Punkt (.) bei den Direktiven vereinfacht.

Das Operandenfeld hinter einem Prozessorbefehl kann, in Abhängigkeit von der verwendeten Adressierungsart, leer sein oder bis zu drei Operanden enthalten. Wird die implizite Adressierung verwendet, bleibt das Operandenfeld leer. Unterstützt der Prozessor (und damit der Assembler) Drei-Adreß-Befehle, so können drei Operanden, durch Kommata getrennt, im Operandenfeld stehen. Bei einer Assembler-Direktive im Operatorfeld kann die Anzahl der Operanden stark varieren.

Ein Operand kann eine Konstante, ein Symbol oder ein Ausdruck sein. Ein Ausdruck kann bei den meisten Assemblern aus mehreren Symbolen und Konstanten bestehen, die sich über eine Rechenvorschrift zu einem konstanten Wert zusammenfassen lassen. Beispielsweise wäre ein gültiger Ausdruck "$F0 & WIDTH + 25 ", bei dem das Symbol "WIDTH" eine Konstante (Symbol) darstellt.

2.3/5

Das Kommentarfeld dient der Dokumentierung und der Strukturierung des Programms. Es wird in der Regel durch einen führenden Strichpunkt (;) oder ein Doppelkreuz (#) eingeleitet. Der Kommentar kann auch alleine in der Programmzeile stehen und am Anfang der Zeile beginnen.

Zur Steuerung des Übersetzungsvorganges werden Assembler-Direktiven benutzt. Die beiden wichtigsten Direktiven, "ORG" und "EQU", sind bei fast allen Assemblern mit einer identischen Syntax und Semantik zu finden.

Mittels der Direktive "ORG" kann auf den Adreßzähler des Assemblers direkter Einfluß genommen werden. Beispielsweise weist die Programmzeile " ORG $0400" dem Adreßzähler die hexadezimale Adresse "0400" zu. Das Dollarzeichen kennzeichnet bei den meisten Assemblern die nachfolgende Zahl als hexadezimal.

Durch die Direktive "EQU" wird einem Symbol im Markenfeld ein Wert, im Sinne einer Konstantendefinition, zugewiesen. Dieser Wert ist für das Symbol während der ganzen Laufzeit des Programms konstant. Zum Beispiel wird durch die Programmzeile "CLRDSP EQU $F110" dem Symbol "CLRDSP" der hexadezimale Wert "F110" zugewiesen, der der Einsprungadresse der Monitorroutine "CLRDSP" zum Löschen der Anzeige des Praktikumsrechners entspricht.

Zum Abschluß werden zwei Codefragmente als Beispiele für verschiedene Darstellungsweisen von Assemblerprogrammen vorgestellt. Das erste Beispiel zeigt einige Zeilen eines Assemblerprogramms für einen MIPS-Prozessor, der Drei-Adreß-Befehle unterstützt. Das zweite Beispiel zeigt die Syntax des im Praktikum verwendeten 6809-Assemblers "AS9".

Tabelle 2.3-1: Codefragment eines MIPS-Assemblers
 .text   
 .globlmain  
     
main:addu$sp,$sp,32 # Restore last Stack frame
 sw$ra,20($sp) # Save return address
 sw$fp,16($sp) # Save old frame pointer
 move $a1,$v0# Move result to $a1

Tabelle 2.3-2: Codefragment des 6809-Praktikumsassemblers "AS9"
 ORG$0400 ; Beginn Programmbereich
     
  LDA# $3E ;CB2 als Steuerleitung (High),
 STACRB ;Flanke an CB1, disable
NEWLINECLRDRB ;Port B loeschen
  . . .. . . 
 BRANEWLINE ;Ruecksprung neue Eingabe
     
DLY1MSEQU$F160  
HALTKEY EQU$F143 

Als Beispiel für eine vollständige Syntaxbeschreibung eines Assemblers möge die Beschreibung des 6809-Assemblers, in Unterabschnitt 2.4.1, dienen.

2.3/6

2.3.4 Kontrollstrukturen in Assemblerprogrammen

Programmiererinnen und Programmierer, die es gewohnt sind, in einer problemorientierten Sprache zu denken, fällt der Übergang zu einer maschinenorientierten Programmiersprache meistens schwer, da die gewohnten Kontrollstrukturen nicht unterstützt werden. Um den Übergang zu erleichtern, werden in dieser Einführung in die Assemblerprogrammierung problemorientierte Kontrollstrukturen, wie sie aus Pascal, Modula oder C bekannt sind, den entsprechenden äquivalenten Assemblerroutinen gegenübergestellt. Anhand der Beispiele sollen Sie erkennen, welche Befehle an welchen Stellen des Assemblerprogramms notwendig sind (dunkel unterlegt), um den gewünschten Programmfluß zu erreichen.

In einigen Unterabschnitten finden Sie Hinweise und Beispiele für eine sinnvolle, prozessor-angepaßte Verwendung von Registern und Befehlen. Die Beispiele sind in der Ihnen bekannten Syntax des 6809-Prozessors notiert. Der Befehlssatz und die Adressierungsarten des 6809 sind der KE1 als rote "Glossarseiten" beigefügt und sollten von Ihnen bei der Durcharbeitung der folgenden Beispiele genutzt werden.

Praktische Übung P2.3-2:
Führen Sie die folgenden Beispiele der folgendenUnterabschnitte mit Ihrem Praktikumsrechner durch. Benutzen Sie die Trace-Funktion (Taste F1 des Praktikumsrechners) zur Einzelschrittausführung der Befehle.
Zur Erleichterung sind bei den Beispielen, entspr. Kurseinheit 1, neben den Mnemonics und den Operanden auch der OP-Code des 6809-Prozessors leicht unterlegt mit aufgeführt.

1. Wertzuweisungen

Wertzuweisungen an Konstanten werden in der Programmiersprache Pascal zu Beginn des Programms durch

const  MAXDR    10;
deklariert. Hierbei ist der an die symbolische Konstante "MAXDR" zugewiesene Wert "10" während des gesamten Programmlaufes nicht mehr änderbar. Auf Assemblerebene steht die Direktive EQU für eine Konstantendeklaration zur Verfügung. Entsprechend dem Hochsprachenkonstrukt wird einer symbolischen Konstanten ebenfalls ein Wert zugewiesen, der nicht mehr veränderbar ist.
MAXDR    EQU    10
Auf Assemblerebene kann der Wert der Konstanten entgegen den normalen Konstantendeklarationen von Hochsprachen ein Datum oder eine Adresse darstellen.

2.3/7

Wertzuweisungen an Variablen werden auf Assemblerebene neben üblichen Operationsbefehlen durch Transfer- und Speicherbefehle durchgeführt. Variablenwerte werden in Prozessorregistern oder im Speicher gehalten. Zur Modifikation werden diese in ein Prozessorregister geladen oder, falls schon in einem Register vorhanden, sofort verändert. Werden Variablenwerte bei der aktuellen Programmbearbeitung nicht benötigt, können diese in nicht benutzten Registern des Prozessors (schneller Zugriff) oder auf dem Stack zwischengespeichert werden. Ein Zurückschreiben des Variablenwertes in den Speicher ist natürlich auch möglich.

Die folgenden Beispiele verwenden die Konstanten "MAXDR" und "MINTMP" sowie die Variablen "Druck" und "Temp" 1). In den Codefragmenten der Assemblerbeispiele wird die Variable "Druck" im Register A, die Variable "Temp" im Register B gehalten.

2. Einfache Verzweigungen

In der Sprache Pascal werden Verzweigungen durch die Konstrukte

IF  Bedingung  THEN  Anweisung
und durch
IF  Bedingung  THEN  Anweisung1  ELSE Anweisung2
dargestellt. Der Programmfluß wird durch die Abfrage einer Bedingung in seinem sequentiellen Ablauf unterbrochen und in alternative Programmabläufe aufgespaltet. Ist die Bedingung im ersten Konstrukt erfüllt, so wird die Anweisung ausgeführt, ansonsten nicht. Im zweiten Konstrukt wird bei einer erfüllten Bedingung Anweisung1 ausgeführt, ansonsten Anweisung2. Das Programm wird in beiden Fällen bei einer nach dem IF-Konstrukt folgenden Anweisung fortgesetzt.

Eine Bedingung in Pascal ist dann erfüllt, wenn die Boolesche Bedingung true ist. Auf Prozessorebene wird ein Verzweigungsbefehl ausgeführt, wenn die zu dem Befehl korrespondierenden Flags im CC-Register (Condional Code Register) gesetzt sind. Eine Bedingung kann in höheren Programmiersprachen eine "einfache" Vergleichsoperation, wie z. B. "A < B" sein, oder auch mehrere verknüpfte Bedingungen, wie z. B. "(A < B) AND (C > D)", enthalten. Auf Prozessorebene stehen für "einfache" Vergleichsoperationen entsprechende Befehle zur Verfügung. Komplexere Bedingungen müssen aufgelöst werden (Unterabschnitt 3).

In Assemblersprachen wird die Auswahl von Alternativen durch "Überspringen" der nicht zu durchlaufenden Alternativen gelöst.

Beispielsweise ist zu der PASCAL-Struktur

const  MAXDR = 10;
. . .
IF  Druck < MAXDR  THEN Druck := Druck + 1;
. . .
das Assemblercodefragment

. . .
81 0a CMPA #MAXDR ;Vergleich auf max Druck
24 01 BHS NEXT ;Wenn >=, springen
4c INCA ;Variable Druck erhoehen
NEXT: . . .;
;
MAXEQU10 ;maximaler Druck = 10

äquivalent. Soll der "INCA"-Befehl nicht ausgeführt werden, so wird er mittels des Befehls "BHS" übersprungen. Zu beachten ist, daß die Bedingung im Assemblercode gegenüber der Pascal-Struktur negiert ist.

2.3/8

Da in der Struktur

const  MAXDR = 10;
. . .
IF  Druck < MAXDR  THEN  Druck := Druck + 1
                   ELSE  Druck := Druck - 2;
. . .
eine Auswahl zwischen zwei Alternativen zu treffen ist, sind auf Assemblerebene ein bedingter und ein unbedingter Sprung notwendig.

. . .
81 0a CMPA #MAXDR ;Vergleich auf max Druck
24 03 BHS ELSE ;Wenn >=, springen
4c INCA ;Druck um Eins erhoehen
20 02 BRA NEXT ;Alternative ueberspringen
80 02 ELSE: SUBA#02 ;ELSE-Zweig: Druck - 2
NEXT:. . . ;naechste Anweisung
MAXDREQU10 ;maximaler Druck = 10

Durch den unbedingten Sprung "BRA" (branch always) wird nach Abarbeitung der ersten Alternative die zweite Alternative übersprungen, um damit zu der nächsten Anweisung nach den Alternativen zu gelangen.

Hinweis:

Bevor ein Verzweigungsbefehl verwendet werden kann, ist ein Vergleichsbefehl, z. B. "CMPA", zu benutzen, um die einzelnen Flags im CC-Register für den Vergleichsbefehl vorzubereiten. Es kann nur dann auf den Vergleichsbefehl verzichtet werden, wenn der vorhergehende Befehl die Flags für den nachfolgenden Verzweigungsbefehl korrekt setzt. Wird dagegen zwischen Vergleichsbefehl und Verzweigungsbefehl ein Befehl gelegt, der die Flags für den Verzweigungsbefehl anderweitig ändert, ergeben sich falsche Verzweigungsentscheidungen!

Aus den Assembler-Befehlstabellen (siehe Anhang KE1) können Sie ersehen, welche Befehle welche Flags des CC-Registers setzen und entsprechend entscheiden, ob ein Vergleichsbefehl notwendig ist oder nicht.

3. Verzweigungen mit mehreren Bedingungen

Für den Vergleich zwischen Operanden stehen auf Prozessorebene in der Regel nur Vergleichsbefehle mit zwei Operanden zur Verfügung. Hat die für eine Verzweigung zu verwendende Bedingung mehr als zwei Operanden, z. B. "(A < B) AND (C > D)", so ist sie in mehrere Einzelvergleiche aufzulösen. Für die Zusammenfassung der Einzelvergleiche zu einem Booleschen Gesamtergebnis sind mehrere Lösungen möglich.

Denkbar ist der Einzelvergleich mit jeweiligem nachfolgendem Abspeichern des Vergleichsergebnisses. Die Ergebnisse werden danach wieder paarweise verglichen, um in mehreren Schritten zu einem Gesamtergebnis zu gelangen und um die verlangte Aktion auszuführen oder nicht.

2.3/9

Eine anderer Lösungsweg nutzt für die UND-Verknüpfung zwischen Einzelvergleichen, die implizite UND-Verknüpfung der einzelnen Befehle in einem sequentiellen Programmfluß aus. Bei einer ODER-Verknüpfung zwischen Einzelvergleichen wird der Umstand ausgenutzt, daß wenn der erste Vergleich wahr ist, der zweite (oder weitere) Vergleich nicht mehr überprüft werden muß. Die beiden folgenden Beispiele zeigen jeweils einen Implementierungsvorschlag. In den Assemblerbeispielen wurde für die Variable "N" das A-Register, für die Variable "X" das B-Register verwendet. Die Konstanten "MAX" und "TEMP" wurden über die Assembler-Direktive "EQU" eingebunden.

Für die PASCAL-Struktur

const  MAXDR  = 10;
const  MINTMP = 90;
. . .
IF  (Druck < MAXDR)  AND  (Temp > MINTMP)
    THEN  Druck := Druck + 1;
. . .
ergibt sich als Assemblerimplementierung:

. . .
81 0a CMPA #MAXDR ;Vergleich auf max Druck
24 05 BHS NEXT ;Wenn >=, springen
c1 51 CMPB #MINTMP ;Vergleich auf min Temp.
23 01 BLS NEXT ;Wenn <=, springen
4c INCA ;Druck inkrementieren
NEXT: . . .;
MAXDREQU10 ;maximaler Druck = 10
MINTMPEQU 90;minimale Temperatur = 90

Nur wenn beide Sprungbedingungen nicht erfüllt sind, also die dazu komplementären Bedingungen des PASCAL-Fragments erfüllt sind, wird die geforderte Aktion ausgeführt. Ist die erste Sprungbedingung erfüllt (bzw. wird der komplementäre erste Vergleich in PASCAL-Notation nicht erfüllt), wird sofort mit der Ausführung der folgenden Anweisung nach der Verzweigung begonnen.

Bei einer ODER-Verknüpfung

const  MAXDR   = 10;
const  MINTEMP = 90;
. . .
IF  (Druck < MAXDR)  OR  (Temp > MINTMP)
    THEN  Druck := Druck + 1;
. . .
ergibt sich als Assemblerimplementierung:

. . .
81 0a CMPA #MAXDR ;Vergleich auf max Druck
24 05 BLO WORK ;Wenn kleiner, addieren
c1 51 CMPB #MINTMP ;Vergleich auf min Temp.
23 01 BLS NEXT ;Wenn <=, springen
4c WORKINCA ;Druck inkrementieren
NEXT: . . .;
MAXDR EQU10 ;maximaler Druck = 10
MINTMPEQU 90;minimale Temperatur = 90

2.3/10

Ist die erste Bedingung erfüllt, kann sofort zu der auszuführenden Anweisung gesprungen werden. Ist die erste Bedingung nicht erfüllt, aber die zweite Bedingung, so wird ebenfalls die Aweisung ausgeführt. Werden beide Bedingungen nicht erfüllt, so wird durch die zweite Verzweigung die auszuführende Anweisung übersprungen und mit der nachfolgenden Anweisung begonnen.

Müssen in einer Verzweigung mehrere ELSE-Zweige berücksichtigt werden, so ist ein Vergleich mit mehreren Bedingungen nicht möglich. Eine Verschachtelung des "IF-THEN-ELSE"-Konstrukts ist die Folge. Zur Verdeutlichung möge das folgende Beispiel dienen.

const  MAXDR   = 10;
const  MINTEMP = 90;
. . .
IF  Druck < MAXDR 
    THEN  IF  Temp > MINTMP  THEN  Druck := Druck + 1
                             ELSE  Temp  := Temp + 4
    ELSE  Temp := Temp + 25;
. . .
Aus der PASCAL-Struktur ergibt sich als beispielhafte Implementierung:
. . .
81 0a CMPA #MAXDR ;Vergleich auf max Druck
24 0b BHS ELSE1 ;Wenn >=, nach ELSE1
c1 5a CMPB #MINTMP ;Vergleich auf min Temp.
23 03 BLS ELSE2 ;Wenn <=, nach ELSE1
4c INCA ;2. Ebene, THEN-Zweig
20 06 BRA NEXT ;zur naechsten Anweisung
cb 04 ELSE2:ADDB #04;2. Ebene, ELSE-Zweig
20 02 BRA NEXT ;zur naechsten Anweisung
8b 19 ELSE1:ADDA #25;1. Ebene, ELSE Zweig
NEXT: ...;
MAXDREQU 10;maximaler Druck = 10
MINTMPEQU 90;minimale Temperatur = 90

Es sind in der Implementierung neben den beiden bereits bekannten bedingten Sprüngen zwei unbedingte Sprünge notwendig, um die einzelnen Anweisungen voneinander zu trennen.

2.3/11

4. Mehrfachauswahl

Immer wieder stellt sich das Problem, eine Auswahl aus mehreren Möglichkeiten zu treffen.

Beispielsweise könnte in Abhängigkeit von einer Tastatureingabe bei Drücken der Taste

'S' : das Programm zu beenden sein, bei
'R' : eine Addition danach eine neue Eingabe durchzuführen sein, bei
'T' : sofort eine weitere Eingabe möglich sein, bei
'L' : die Verzweigung in ein Unterprogramm mit nachfolgender neuer Eingabe möglich sein.

Trifft keine der Möglichkeiten zu, könnte die Eingabe zu wiederholen sein.

In PASCAL würde diese Abfrage u.a. mit einer CASE-Struktur modelliert werden. In einem 6809-Assemblerprogramm des Praktikumsrechners würde obige Abfrage z.B. wie folgt implementiert 2):

. . . . . .
bd f1 43 NEW:JSRHALTKEY ;Tastatureingabe abwarten
c1 86 CMPB#$86 ;Vergleich auf Taste "S"
27 . . BEQENDE ;wenn ja, Programmende
c1 84 CMPB#$84 ;Vergleich auf Taste "R"
27 0d BEQADD ;wenn ja, Addition
c1 88 CMPB#$88 ;Vergleich auf Taste "T"
27 f1 BEQNEW ;wenn ja, neue Eingabe
c1 87 CMPB#$87 ;Vergleich auf Taste "L"
26 ed BNENEW ;wenn nein, neue Eingabe
bd . . . . JSRUPTST ;Unterpr. UPTST aufrufen
20 e8 BRANEW ;Mehrfachauswahl Ende
8b 03 ADD:ADDA #$03; . . . .
20 e4 BRANEW ;zurueck zur Eingabe
. . . . . .
        
. . . . . .
. . . ENDE:. . . ;Programmende
        
f1 43 HALTKEYEQU$F143 ;Einsprungadresse UP


Hinweis (für Fortgeschrittene):

Bei einer Mehrfachauswahl können unter Umständen durch geschickte Auswahl und Kombination einzelner Abfragen Vergleichsbefehle eingespart werden. Soll z.B. bei einer positiven Zahl nach Adresse $0600 gesprungen werden und bei einer negativen Zahl nach $0700 verzweigt werden, so können die beiden Verzweigungsbefehle "BPL" and "BMI" direkt hintereinander liegen. Der Befehl vor den beiden Vergleichsbefehlen muß natürlich die Flags des CC-Registers korrekt setzen.

2.3/12

5. Zyklische Programme

In einem Programm werden in der Regel mehrere Schleifen benötigt. Abhängig von der Anzahl der minimalen Durchläufe durch eine Schleife werden verschiedene Schleifenkonstrukte benutzt.

Bei der WHILE-Schleife steht der Test für die Endebedingung der Schleife vor dem Schleifenkörper, in dem alle Anweisungen, die in einer Schleife durchgeführt werden sollen, stehen. Somit ist es bei der WHILE-Schleife möglich, daß der Schleifenkörper bei Aufruf der Schleife nicht ein einziges mal durchlaufen wird, da die Endebedingung bereits erfüllt ist. Entsprechend ist bei einer Assemblerimplementierung die Abfrage der Endebedingung vor den Schleifenkörper zu stellen. Bei einer REPEAT-Schleife liegt die Endebedingung hinter dem Schleifenkörper, so daß die Schleife auf jeden Fall einmal durchlaufen wird.

Innerhalb des Schleifenkörpers einer WHILE-Schleife als auch einer REPEAT-Schleife muß es eine Anweisung geben, die Einfluß auf den Wahrheitswert der Endebedingung der Schleife nimmt, damit die Schleife terminiert.

In den folgenden beiden Beispielen werden Implementierungen der WHILE- und der REPEAT-Schleife vorgestellt. Die FOR-Schleife wird nicht diskutiert, da sie einen Spezialfall der WHILE-Schleife darstellt.

Als jeweilige Anwendung wird die n-fache Aufsummierung, mit
X = 1 + 2 + 3 + ... + N und N = 10,
gewählt. Für X ergibt sich der Wert 55.

In Pascal-Notation ergibt sich für die WHILE-Schleife:

const N = 10;
var   I:  char;
var   X:  integer;

X := 0;
I := 1;
WHILE  I <= N  DO
  BEGIN
    X := X + I;
    I := I + 1
  END ;

Bei der Umsetzung der WHILE-Schleife in ein Assemblerprogramm für den 6809-Prozessor sollten die Befehle und die möglichen Adressierungsarten des Prozessors berücksichtigt werden.

Möchte man für die Addition die Register des Prozessors ausnützen, ohne über den Speicher des Rechners arbeiten zu müssen, so bietet sich der "ABX"-Befehl an, der eine Addition zwischen dem B-Register und dem X-Register ermöglicht. Da der Befehl das Ergebnis der Addition (Variable X) im X-Register ablegt, ist entsprechend der Aufgabenstellung das B-Register für die Zählvariable "I" zu benutzen. Dies "harmoniert" mit dem "INCB"-Befehl, so daß die Variable "I" in einem Taktzyklus inkrementiert werden kann. Die Konstante "N" wird über die Assembler-Direktive "EQU" in das Programm eingebunden. Damit Sie das Programm auf Ihrem Praktikumsrechner vollständig austesten können, sollte es ab der Speicheradresse $0400 beginnen.

2.3/13

;Beginn
ORG $0400Programmbereich
8e 00 00 LDX#$0000 ;X mit 0 initialisieren
c601 LDB#$01 ;B := 1
c1 0a LOOP: CMPB #N ;Zaehlende erreicht ?
22 04 BHI END ;Wenn ja, nach END
3a ABX ;Nein, Addition
5c INCB ;Zählvariable erhöhen
20 f8 BRALOOP ;zurueck zum Zaehlanfang
3f END:SWI1 ;Programmende
        
  NEQU10 ;Endebedingung = 10

Die Abfrage für das Beenden der Schleife liegt bei der WHILE-Schleife vor dem Schleifenkörper, der in einem Assemblerprogramm zwischen dem bedingten Sprungbefehl und der zugehörigen Sprungadresse "geklammert" ist. Am Ende des Schleifenkörpers ist bei der WHILE-Schleife mit einem unbedingten Sprung zum Schleifenanfang zurückzukehren.

Die REPEAT-Schleife in Pascal-Notation lautet:

const N = 10;
var   I:  char;
var   X:  integer;

X := 0;
I := 1;
REPEAT
    X := X + I;
    I := I + 1
UNTIL  I = N;
Als Assemblerprogramm ergibt sich bei den gleichen Registerbelegungen wie für die WHILE-Schleife:

ORG $0400;Beginn Programmbereich
8e 00 00 LDX#$0000 ;X mit 0 initialisieren
c6 01 LDB#$01;B := 1
3a LOOP:ABX ;Addition
5c INCB ;Zählvariable erhöhen
c1 0a CMPB #N ;Zaehlende erreicht ?
23 fa BLS LOOP ;Wenn nein, weitere ADD
3f SWI1;Programmende
        
  NEQU10 ;Endebedingung = 10

Bei jeder Schleife ist die Reihenfolge der Anordnung der Zählvariable zu anderen Anweisungen des Schleifenkörpers wichtig, wenn diese Anweisungen die Zählvariable verwenden. Beispielsweise müßte in der REPEAT-Schleife das B-Register mit dem Wert Null initialisiert werden, wenn zuerst der "INCB"-Befehl und danach der "ABX"-Befehl im Schleifenkörper folgen würde. Die Möglichkeit solcher Variationen kann ausgenutzt werden, um einfachere Abfragebedingungen für die Beendigung der Schleife zu erreichen.

2.3/14

6. Vorzeichenlose und vorzeichenbehaftete Verzweigungsbefehle

Vorsicht ist bei der Benutzung von vorzeichenlosen und vorzeichenbehafteten Verzweigungsbefehlen geboten. Abhängig vom Datentyp der Variablen, die für den Vergleichstest für die Verzweigungsbedingung herangezogen wird, ist der passende Verzweigungsbefehl auszuwählen.

Da die Wertigkeit von vorzeichenlosen und vorzeichenbehafteten Zahlen verschieden ist, wie Tabelle 2.3-1 für 3-bit Zahlen zeigt, führt ein falsch gewählter Verzweigungsbefehl zu einem nicht korrekt ablaufenden Programmfluß.

Tabelle 2.3-3: Vorzeichenbehaftete und vorzeichenlose 3-bit Zahlen
vorzeichenbehaftete Zahlen vorzeichenlose Zahlen
positivste Zahl011 111 größte Zahl
 010110  
 001 101 
 000 100 
 111 011 
 110 010 
 101 001 
negativste Zahl100 000kleinste Zahl

Beispielsweise sei die Variable A = 111, die Variable B = 011. Sind die beiden Zahlen vorzeichenlos, so ist A > B. Sind die beiden Zahlen vorzeichenbehaftet, so gilt A < B.

7. Zusammenfassung

Mittels bedingten und unbedingten Verzweigungsbefehlen ist es möglich, alle Kontrollstrukturen einer problemorientierten Programmiersprache auf Assemblerebene abzubilden. Ein Assemblerbefehl, der die Flags des CC-Registers korrekt setzt, wie z.B. "CMPA", und der Verzweigungsbefehl bilden in der Regel eine Einheit.

Bei der Implementierung einer Schleife ist auf eine einfache Abfrage des Schleifenendekriteriums zu achten. Gegebenenfalls ist die Initialisierung der in der Endebedingung abgefragten Variablen zu verändern. Damit die Schleife terminiert, muß diese Variable im Sinne der Endebedingung verändert werden.

Programmierfehler können durch falsche Verwendung von vorzeichenlosen und vorzeichenbehafteten Verzweigungsbefehlen entstehen.

2.3/15

2.3.5 Unterprogramme und Stackoperationen

Unterprogramme sind auf Assemblerebene, wie auch in jeder höheren Programmiersprache, ein Mittel zur Strukturierung und Vereinfachung von Programmen. Zum Verständnis komplizierter Vorgänge und Berechnungen ist es meistens nicht notwendig zu wissen, wie die einzelnen Teilergebnisse berechnet wurden, wichtig ist nur die Grobstruktur. Genau dies wird durch Unterprogrammaufrufe erfüllt. Indem die Berechnungen der Teilergebnisse in Unterprogramme ausgelagert werden, wird das "aufrufende" Programm kleiner, übersichtlicher und durchschaubarer.

Ein Assemblerunterprogramm ist ähnlich strukturiert wie ein Unterprogramm einer höheren Programmiersprache. Es ist in sich abgeschlossen, d.h. es besitzt einen definierten Anfang und ein definiertes Programmende. Einem Unterprogramm können Parameter (Variablenwerte) zur Berechnung übergeben werden. Ergebnisse können von dem Unterprogramm an das "aufrufende" Programm zurückgegeben werden. Ein Unterprogramm kann wiederholt aufgerufen werden und es kann sich, je nach Konstruktion des Unterprogramms, selbst aufrufen. Unterprogramme können andere Unterprogramme aufrufen (Verschachtelung). Nachteilig wirkt sich bei der Benutzung von Unterprogrammen die etwas längere Ausführungszeit aus, da zusätzliche Befehle zur Steuerung des Programmflußes verwendet werden müssen. Zur Zwischenspeicherung von Registerinhalten wird in der Regel der Stack verwendet.

1. Kontrollfluß

Der Aufruf eines Unterprogramms auf Assemblerebene erfolgt durch spezielle Prozessorbefehle (subroutine calls). Gelangt eine Routine im Laufe ihrer Abarbeitung auf einen Unterprogrammaufruf, so wird in der Regel der Inhalt des Programmzählers, der die Speicheradresse des dem Aufruf nachfolgenden Befehls enthält, auf dem Stack abgelegt. Die Abspeicherung des Programmzählers auf dem Stack wird bei fast allen modernen Prozessoren automatisch durch den subroutine call erzwungen 3). Danach wird an die Speicheradresse verzweigt, die als Operand dem Befehl zum Aufruf des Unterprogramms folgt. Nach Abarbeitung des Unterprogramms wird wiederum mittels eines speziellen Prozessorbefehls ("RTS": return from subroutine) zu dem aufrufenden Programm zurückgekehrt. Hierzu wird durch den "RTS"-Befehl die Speicheradresse, die beim Sprung in das Unterprogramm auf den Stack gelegt wurde, geholt und in den Programmzähler übertragen. Der Programmfluß wird im "aufrufenden" Programm mit dem Befehl nach dem Unterprogrammaufruf fortgesetzt.

Eine Verschachtelung von Unterprogrammen ist möglich (ein Unterprogramm ruft ein weiteres Unterprogramm auf), da der Stack nach dem LIFO-Prinzip (last in, first out) arbeitet. Hierdurch können die einzelnen Rückkehradressen der "aufrufenden" Programme ordnungsgemäß und in der richtigen Reihenfolge vom Stack geholt werden.

2.3/16

Die Übergabe des Kontrollflußes zwischen mehreren Aufrufen von Unterprogrammen zeigt beispielhaft Bild 2.3-1. Der Kontrollfluß wird vom "aufrufenden" Programm an das Unterprogramm abgegeben. Nach Beendigung des Unterprogramms wird die Kontrolle an das "aufrufende" Programm wieder zurückgegeben. Wird im Unterprogramm eine weitere Unterroutine aufgerufen, so wird die Kontrolle entsprechend weitergegeben.


Bild 2.3-1:Kontrollflußübergabe bei Unterprogrammen

2. Sichern von Registerinhalten

Zur korrekten Abarbeitung des aufgerufenen Unterprogramms werden Prozessorregister benötigt. Bei kleinen Programmen kann unter Umständen die Benutzung der Register zwischen Hauptprogramm und Unterprogramm so geschickt aufgeteilt werden, daß keine Registerinhalte während des Unterprogrammaufrufes zwischengespeichert werden müssen. Bei komplexeren Programmen ist dies kaum möglich, so daß der Registersatz des Prozessors teilweise oder ganz im Speicher abgelegt werden muß. Als Zwischenspeicher wird in der Regel der Systemstack benutzt. Die Zwischenspeicherung der Registerinhalte kann zu zwei Zeitpunkten stattfinden:

Welche Variante gewählt wird, ist von Fall zu Fall zu entscheiden und liegt im Ermessen des Programmierers. Bei beiden Varianten ist zu beachten, daß die einzelnen Registerinhalte in umgekehrter Reihenfolge vom Stack geholt werden müssen, wie sie auf den Stack gelegt worden sind. Das heißt, die Register, die zuletzt auf den Stack geschrieben wurden, müssen als erste wieder gelesen werden.

3. Parameterübergabe

Die Art der Übergabe von Parametern (Daten, Adressen, Adreßzeiger) an das aufgerufene Unterprogramm hängt von der Anzahl der übergebenen Parameter, der zur Verfügung stehenden "Übergabezeit" und bei größeren Programmen (z.B. von Compilern erzeugte Programme) vom Typ der Parameter ab.

Für die Übergabe werden verschiedene Methoden benutzt, von denen einige ausgewählte im folgenden kurz vorgestellt werden.

2.3/17

4. Lageunabhängigkeit von Programmen und Unterprogrammen

Kann ein Maschinenprogramm an einer beliebigen Stelle des Speichers geladen und ausgeführt werden, so wird diese Eigenschaft als "Lageunabhängigkeit" bezeichnet. Hierfür müssen alle Sprungziele und Speicheradressen von (Unter-) Programmen und Datenbereichen relativ zur Speicheradresse des ersten Maschinenbefehls des Programms adressiert werden. Lageunabhängige Programme enthalten somit keine absoluten Speicheradressen.

Lageunabhängige Programme werden durch ausschließliche Benutzung von Befehlen mit relativer Adressierung erreicht. Beispielsweise sind bei der Programmierung von Unterprogrammen des 6809-Prozessors statt des "JSR"-Befehls die Befehle "BSR" und "LBSR" zu verwenden. Für die relative Adressierung von Datenbereichen sind bei dem 6809-Prozessor von Motorola die "LEA "-Befehle zu benutzen, z.B. "LEAX OFFSET,PCR".

Hinweis: Vertiefende Übungsbeispiele zur Programmierung von Unterprogrammen finden Sie in Abschnitt 2.5.

2.3/18

5. Rekursive Programmierung

Ein rekursiver Programmablauf liegt vor, wenn sich Programmroutinen wechselseitig oder selbst aufrufen. Als Abbruchbedingung muß eine nichtrekursive Abbruchbedingung in den Programmroutinen enthalten sein. Das heißt, es ist ein Zustand (eine Variable) so zu verändern, daß nach endlich vielen Schritten ein eindeutig definierter Endzustand erreicht ist.

Rekursion eignet sich besonders dann, wenn das zugrunde liegende Problem oder die zu behandelnden Daten rekursiv definiert sind. Bei der rekursiven Programmierung wird im allgemeinen Programmcode eingespart und die Lesbarkeit von Routinen unter Umständen erhöht. Rekursive Lösungen benötigen allerdings mehr Speicher und eine längere Programmlaufzeit, da bei jedem Aufruf der rekursiven Routine Daten auf den Stack abgelegt werden müssen. Rekursive Strukturen lassen sich in Schleifenkonstrukte überführen.

Tabelle 2.3-3 zeigt als Beispiel für die Programmierung von Unterprogrammen und speziell für die rekursive Programmierung die rekursive Implementierung des Beispiels von Unterabschnitt 2.3.4 zur n-fachen Aufsummierung von Zahlen, mit
X = 1 + 2 + 3 + ... + N und N = 10.
Für X ergibt sich der Wert 55.

Das Beispiel ist für eine Rekursion nicht charakteristisch, zeigt aber die wichtigsten Techniken. Die Zeilen $0400 - $0408 stellen das Hauptprogramm, die Zeilen $0409 - $0416 das Unterprogramm dar. Im B-Register wird die Zählvariable Rekursionsebene für Rekursionsebene dekrementiert und der Inhalt auf den Stack abgelegt. Die Zeilen $040C und $040E realisieren die Abbruchbedingung der Rekursion. In Zeile $0413 wird das B-Register restauriert und in der nächsten Zeile zu dem Inhalt des X-Registers addiert. Das X-Register stellt somit eine Art globale Variable dar. Nach dem Durchlauf durch alle zehn Rekursionsebenen terminiert das Programm in der Hauptroutine in Zeile $0408.

Der Vergleichsbefehl in Zeile $040C ist nicht unbedingt notwendig, da durch den DECB-Befehl bereits das Zero-Flag im CC-Register korreht gesetzt wird. Der Übersichtlichkeit halber wurde der Befehl in das Listing mit aufgenommen.

Tabelle 2.3-4: Rekursives Beispielprogramm zur n-fachen Aufsummierung
0400   ORG$0400 ;Beginn Programmbereich
        
04008E 00 00  LDX #$0000;X initialisieren
0403C60A  LDB#10 ;B auf 10 Durchläufe
0405BD 04 09  JSRUPRADD :rekursives UP aufrufen
04083F  SWI1;Programmende
        
04093404 UPRADD:PSHSB ;Inhalt von B auf Stack
040B5A  DECB ;Zähler dekrementieren
040CC1 00  CMPB#0 ;Vergleich auf Rek-Ende
040E27 03  BEQNEXT ;wenn ja, UP überspr.
0410BD 04 09  JSRUPRADD ;rekursiver UP-Aufruf
041335 04NEXT: PULSB ;Zähler vom Stack holen
04153A  ABX;X := X + B
041639  RTS  ;Ende Unterprogramm

2.3/19

2.3.6 Programmdokumentation

Assemblerprogramme können sehr kompakt und undurchschaubar geschrieben werden, so daß nur eine gute Strukturierung und Kommentar-Disziplin zu lesbaren und wartbaren Programmen führt.

Verschiedene Programmier-Teams haben Richtlinien und Regeln für eine gute Dokumentierung von Programmen herausgegeben. Die Praxis zeigt, daß nur durch konsequentes Anwenden dieser Hilfen ein Programm auch nach einem halben Jahr noch verständlich bleibt. Im folgenden einige Auszüge:


Beispielsweise könnte ein Programmkopf folgende Struktur besitzen:

;*************************************************************
; <Name des Programms>
;
; Funktion:  ...............
;
; Input:   Par 1:  .............
;          Par 2:  .............
;          Par n:  .............
; Output:  Par 1:  .............
;          Par 2:  .............
;
; Aufger. von:  .........     Globale Var.:  ....................
;
; Autor:  ...............     Version:  ......   Datum:  ........
;

2.2 Entwicklungswerkzeuge 2.4 Entwicklungswerkzeuge zum Praktikumsrechner