| 2.2 Entwicklungswerkzeuge | 2.4 Entwicklungswerkzeuge zum Praktikumsrechner |
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:
| Transferbefehle | 39% |
| Verzweigungsbefehle | 17% |
| Unterprogrammaufrufe | 13% |
| Arithmetische Befehle | 3% |
| Vergleichsbefehle | 6% |
| restliche Befehle | 22% |
Der hohe Prozentsatz an Transferbefehlen belegt, daß allein eine geschickte Programmierung der Ein- und Ausgabe auf Assemblerebene einen Geschwindigkeitsgewinn erbringen kann.
Assemblierer müssen aus der Sicht des Entwicklers ein Mindestmaß an Anforderungen erfüllen, damit ein produktives Programmieren garantiert ist:
Bei der Entwicklung größerer Programmroutinen können zusätzlich folgende Optionen nützlich sein:
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).
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 | ||
| BEQ | NEXT | ||
| ... | ... | ||
| 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.
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. |
Befehlszeilen von Assemblerprogrammen sind überwiegend in die Felder "Marke" (Label), "Operator", "Operand" und "Kommentar" nach folgendem Schema aufgeteilt:
| Marke | Operator | Operand | Kommentar |
|---|
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.
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".
|
|
Als Beispiel für eine vollständige Syntaxbeschreibung eines Assemblers möge die Beschreibung des 6809-Assemblers, in Unterabschnitt 2.4.1, dienen.
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. |
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 10Auf Assemblerebene kann der Wert der Konstanten entgegen den normalen Konstantendeklarationen von Hochsprachen ein Datum oder eine Adresse darstellen.
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.
In der Sprache Pascal werden Verzweigungen durch die Konstrukte
IF Bedingung THEN Anweisungund durch
IF Bedingung THEN Anweisung1 ELSE Anweisung2dargestellt. 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: | . . . | ; | |||
| ; | |||||
| MAX | EQU | 10 | ;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.
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 | |||
| MAXDR | EQU | 10 | ;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.
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: | . . . | ; | |||
| MAXDR | EQU | 10 | ;maximaler Druck = 10 | ||
| MINTMP | EQU | 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 | WORK | INCA | ;Druck inkrementieren | ||
| NEXT: | . . . | ; | |||
| MAXDR | EQU | 10 | ;maximaler Druck = 10 | ||
| MINTMP | EQU | 90 | ;minimale Temperatur = 90 |
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: | ... | ; | |||
| MAXDR | EQU | 10 | ;maximaler Druck = 10 | ||
| MINTMP | EQU | 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.
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: | JSR | HALTKEY | ;Tastatureingabe abwarten | |
| c1 86 | CMPB | #$86 | ;Vergleich auf Taste "S" | ||
| 27 . . | BEQ | ENDE | ;wenn ja, Programmende | ||
| c1 84 | CMPB | #$84 | ;Vergleich auf Taste "R" | ||
| 27 0d | BEQ | ADD | ;wenn ja, Addition | ||
| c1 88 | CMPB | #$88 | ;Vergleich auf Taste "T" | ||
| 27 f1 | BEQ | NEW | ;wenn ja, neue Eingabe | ||
| c1 87 | CMPB | #$87 | ;Vergleich auf Taste "L" | ||
| 26 ed | BNE | NEW | ;wenn nein, neue Eingabe | ||
| bd . . . . | JSR | UPTST | ;Unterpr. UPTST aufrufen | ||
| 20 e8 | BRA | NEW | ;Mehrfachauswahl Ende | ||
| 8b 03 | ADD: | ADDA | #$03 | ; . . . . | |
| 20 e4 | BRA | NEW | ;zurueck zur Eingabe | ||
| . . . | . . . | ||||
| . . . | . . . | ||||
| . . . | ENDE: | . . . | ;Programmende | ||
| f1 43 | HALTKEY | EQU | $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.
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.
| ;Beginn | |||||
|---|---|---|---|---|---|
| ORG | $0400 | Programmbereich | |||
| 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 | BRA | LOOP | ;zurueck zum Zaehlanfang | ||
| 3f | END: | SWI1 | ;Programmende | ||
| N | EQU | 10 | ;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 | |||
| N | EQU | 10 | ;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.
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ß.
| vorzeichenbehaftete Zahlen | vorzeichenlose Zahlen | ||
|---|---|---|---|
| positivste Zahl | 011 | 111 | größte Zahl |
| 010 | 110 | ||
| 001 | 101 | ||
| 000 | 100 | ||
| 111 | 011 | ||
| 110 | 010 | ||
| 101 | 001 | ||
| negativste Zahl | 100 | 000 | kleinste 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.
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.
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.
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.
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.
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.
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.
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.
|
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 |