2.1 Einleitung 2.3 Einführung in die Assemblerprogrammierung

2.2/1

2.2 Entwicklungswerkzeuge

2.2.1 Assembler, Cross-Assembler, Macro-Assembler, algebraische Assembler, Disassembler

1. Assembler

Der Begriff "Assembler" ist im technischen Sprachgebrauch zweideutig. Zum einen wird mit "Assembler" eine maschinenorientierte Programmiersprache bezeichnet. Zum anderen wird dem Begriff "Assembler" das Übersetzungsprogramm, das ein Programm, welches in der Syntax einer maschinenorientierten Programmiersprache verfaßt ist, in ein Maschinen- (Prozessor-) lesbares Programm übersetzt, zugeordnet. Synonym werden diese Übersetzungsprogramme auch "Assemblierer" genannt.

Eine Assemblersprache ist, wie bereits erwähnt, eine symbolische, maschinen- (prozessor-) orientierte Programmiersprache. Für jede Prozessorfamilie existieren spezielle, auf den Befehlssatz des Prozessors oder der Prozessorfamilie zugeschnittene Assemblersprachen. Die Syntax der Assemblersprachen kann sich von Prozessorhersteller zu Prozessorhersteller, aber auch von Assembler zu Assembler für den gleichen Prozessor ändern. Instruktionen werden in Assemblersprachen in Form mnemonischer Befehle, kurz Mnemonics (Mnemotechnik: Kunst, das Einprägen von Gedächtnisstoff durch besondere Lernhilfen zu erleichtern) notiert. Jedem Operationscode (OP-Code) auf Maschinensprachebene wird eine an die Funktion des Befehls erinnernde Abkürzung zugewiesen. Beipielsweise steht das Mnemonic "STA" für "STORE A", d.h. speichere den Inhalt des Registers A in die Speicherzelle, die nach dem Mnemonic notiert ist. Nach dem Mnemonic folgen kein, ein, zwei oder drei Operanden, die die Adressierungsart sowie Quelle und Ziel des Befehls angeben.

Anhand der Mnemonics setzt das Übersetzungsprogramm, der Assembler, den Programmtext Zeile für Zeile in entsprechende Maschineninstruktionen um. Bei der Umsetzung der Befehle handelt es sich um eine 1:1-Umsetzung, d.h. der Assembler generiert selbständig keinen neuen zusätzlichen Maschinencode. Der erzeugte Code ist eine Folge von Bitmustern, die der jeweilige Prozessor (für den das Programm geschrieben wurde) direkt lesen, d.h. dekodieren, kann. Ein weiterer Bestandteil eines Assemblerprogramms sind sogenannte Assembler-Direktiven, die zur Steuerung des Übersetzungsvorganges sowie des Assemblers benötigt werden. Die Mächtigkeit der Assembler-Direktiven ist von Assembler zu Assembler verschieden. Sie kann sehr rudimentär ausgelegt sein, aber auch z. B. bis zu einem Conditional-Assembly durch die Verwendung von speziellen "IF-THEN-ELSE"-Konstrukten reichen.

2.2/2

2. Cross-Assembler

Cross-Assembler, wie z.B. der Motorola Assembler "AS9", erlauben es, Maschinenprogramme, welche auf einem Computer (host) geschrieben wurden, für einen vom Host-Prozessor typverschiedenen Prozessor (target) zu generieren. Diese ausführbaren Maschinenprogramme können dann auf die Zielarchitektur geladen (download) und gestartet werden. Das Übertragen der erzeugten Programme erfolgt überwiegend über die serielle Schnittstelle oder via Netzwerk. Beispielsweise stellt im Falle des Cross-Assemblers "AS9" Ihr PC den host dar, während der Praktikumsrechner das target repräsentiert.

3. Makro-Assembler

Macro-Assembler ermöglichen es, eine bestimmte Folge von Assemblerbefehlen mit einem einzigen, vom Programmierer zu definierenden "Macro-Befehl" aufzurufen. Die Zuordnung einer Befehlssequenz zu einem definierten Namen wird als Definition des Macros bezeichnet. Diese "benutzerdefinierten" Befehle sind eine gewisse Art von Unterprogrammen, die während der Übersetzung anstelle des aufrufenden Befehls als substituierendes Codefragment an dessen Stelle im Maschinenprogramm eingesetzt werden.

Macros können in der Regel in Bibliotheken zusammengefaßt werden und über eine "INCLUDE"-Direktive im Quellprogramm dem Assembler bekannt gemacht werden. Macro-Bibliotheken sind hierbei Dateien, in denen einzelne Makroroutinen abgespeichert werden. Im Gegensatz zu Programmbibliotheken in höheren Programmiersprachen werden Makrobibliotheken nicht übersetzt (assembliert). Der Assembler fügt ab der Stelle, an der die Direktive "INCLUDE" auftritt, die Bibliothek in das Originalprogramm ein. Bei Auftreten eines Macros durchsucht der Assembler die eingefügten Routinen und setzt das Codefragment des Makros anstelle des Aufrufes in das Programm ein. Dieses (unter Umständen mehrmalige) Einsetzen von Code in das Programm unterscheidet ein Makro von einem Unterprogramm.

4. Algebraische Assembler

Algebraische Assembler werden im Bereich der Digitalen Signalprozessoren verwendet. Diese Assembler unterstützen die hohe Parallelität, die Digitale Signalprozessoren auszeichnet. Sie sind in der Regel Macro-Assembler mit sehr mächtigen Assembler-Direktiven. Ihre Notation ist in den meisten Fällen der Sprache C angelehnt. Als Beispiel verdeutliche folgende Sequenz die benutzte Notation:
F13=F0*F4,F9=F9+F12,F0=DM(I0,M1),F5=PM(I9,M8);
F12=F0*F5,F8=F8-F13,F4=PM(I8,M8); 
Jede Anweisung in einer Zeile wird im DSP zur gleichen Zeit parallel zu den anderen Anweisungen einer Zeile ausgeführt! Die einzelnen Anweisungen werden durch Kommata voneinander getrennt. Die Zeile muß mit einem Strichpunkt abgeschlossen werden.

5. Disassembler

Ein Disassembler ist ein Programm, welches ein Maschinenprogramm in ein Assemblerprogramm (Quellcodeprogramm) zurück übersetzt. Im Idealfall erhält man als disassembliertes Programm das korrekte Assemblerprogramm, reduziert auf die Mnemonics und den zugehörigen Operanden der unterstützten Assemblersprache. Als Marken und Symbole sind bei disassemblierten Programmen in der Regel die hexadezimalen Werte der Marken und Symbole des Maschinenprogramms eingesetzt und müssen in der Regel "von Hand" nachgearbeitet werden. Der OP-Code des disassemblierten Programms wird in den meisten Fällen zur Orientierung mit ausgegeben.

2.2/3

2.2.2 Binder, Linker, Lader, Boot-Lader, Boot-PROM

1. Binder, Linker

Die Begriffe "Binder" und "Linker" werden in der Informatik synonym benutzt. Da ein komplexeres Programm in der Regel aus mehreren Einzelprogrammen und bereits übersetzten Standardroutinen besteht, müssen die einzelnen Maschinenprogramme zu einem lauffähigen Programm zusammengesetzt werden. Diese Aufgabe übernehmen sogenannte Binder (Linker). Die zu bindenden Programme liegen in der Regel als Objekt-Dateien vor. Objekt-Dateien sind hierbei Maschinenprogramme, in denen einzelne Adressen durch den Assembler noch nicht aufgelöst (zugeordnet) werden konnten, da die zugehörige Referenz in anderen Programmen oder Bibliotheken liegt. Der Linker löst diese Zuordnungsprobleme auf, und verschiebt gegebenenfalls bei Doppelbelegung von Speicheradressen einzelne Code-/Datenbereiche.

2. Lader

Der Lader ist eine Routine des Betriebssystems oder des Monitors. Er hat die Aufgabe, das auszuführende Maschinenprogramm in den Speicher des Rechners zu "laden". Hierzu erhält das Programm vom Betriebssystem die Speicheradresse mitgeteilt, ab der das Programm abgelegt werden soll. Der Lader übernimmt hierbei auch etwaige Adreßumrechnungen zwischen den im Maschinenprogramm vorkommenden absoluten Adressen und den für den Speicherbereich gültigen Adressen.

3. Boot-Lader

Ein Boot-Lader (Ur-Lader) ist ein Hilfsprogramm des Entwicklungssystems, mit dem ein entwickeltes Programm ohne Betriebssystem oder Monitor auf einem Target ablauffähig ist. Dieses Hilfsprogramm installiert und startet das eigentliche Programm. Bei Assembler-Programmen kann diese Aufgabe, abhängig von dem Prozessortyp und dem Programmodus, auf ein Minimum reduziert werden oder völlig fehlen. Bei einem Hochsprachenprogramm, das in der Regel eine gewisse Betriebssystemunterstützung erwartet, hat der Boot-Lader diese Unterstützung zu leisten. Beispielsweise müssen verschiedene Controller und der Stack initialisiert werden, Speicherverwaltungstabellen angelegt und mit Zugriffspointern versehen werden, Interruptvektortabellen mit den zugehörigen Routinen zur Verfügung gestellt werden sowie unter Umständen zusätzliche Schnittstellen, wie Netzwerkkarten, initialisiert werden.

4. Boot-PROM

Ein Boot-PROM ist ein EPROM, in dem ein Startup-Code, Monitorroutinen oder sogar ein vollständiges Betriebssystem gespeichert ist. Beispielsweise enthält Ihr PC ein EPROM, in dem die grundlegenden Routinen zum Initialisieren des PCs untergebracht sind. Nach dem Einschalten des PCs werden diese Routinen (BIOS: Basic Input Output System) ausgeführt. Hierdurch werden alle Controller und weiteren Elemente des System korrekt initialisiert und können für das weitere "Hochfahren" des Systems genutzt werden. Hierzu wird nach Abarbeitung der BIOS-Routinen auf den Boot-Sektor Ihrer Festplatte (oder Diskette) verzweigt, um das eigentliche Betriebssystem in den Hauptspeicher zu laden und zu starten.

2.2/4

2.2.3 Compiler, Cross-Compiler, Interpreter, Debugger

1. Compiler, Cross-Compiler

Ein Compiler übersetzt Programme, welche in einer höheren Programmiersprache geschrieben worden sind, in Maschinenprogramme, die unter einem bestimmten Betriebssystem auf bestimmten Prozessoren ablauffähig sind. Das ablaufende Programm benutzt verschiedene Dienste des Betriebssystems, die dem Compiler bekannt sein müssen. Hochsprachenprogramme bestehen in der Regel aus mehreren Teilprogrammen, die nach erfolgter Übersetzung zusammen gebunden werden müssen. Da Hochsprachenprogramme komfortable Kontrollstrukturen, wie z.B. Schleifenkonstrukte, unterstützen, kann der Compiler diese Strukturen nicht zeilenweise auf Maschinensprachebene übersetzten. Er berücksichtigt vielmehr die "Umgebung" einer Zeile bei der Übersetzung mit, um ein sinnvolles Maschinencodefragment zu erzeugen. Hierdurch kann der Compiler z. B. auch erkennen, daß Zeilen eines Quellcodeprogrammes nie ausgeführt werden (dead code), und damit auch nicht in Maschinencode übersetzt werden müssen.

Cross-Compiler werden bei der Programmierung von Mikrocontrollern eingesetzt. Das Quellprogramm wird auf einem leistungsstarken Rechensystem, z.B. einem PC, für eine bestimmte Zielarchitektur entwickelt und übersetzt. Wie bei Cross-Assemblern kann das Programm danach zum Testen auf die Zielarchitektur geladen werden.

2. Interpreter

Ein Interpreter ist ein Programm, welches ein Quellcodeprogramm Zeile für Zeile einliest und unmittelbar nach der syntaktischen Analyse ausführt. Eine Übersetzung und Abspeicherung in ein vollständig vorliegendes Maschinenprogramm findet hierbei nicht statt. Der Interpreter bearbeitet das Quellprogramm sequentiell, analysiert jede Instruktion und führt diese sofort aus. Syntaktische Fehler werden hierbei erst bei der Befehlsinterpretation entdeckt. Der Vorteil von Interpretern liegt in einer schnelleren Testmöglichkeit von Programmen, da der eigentliche Compilierungsvorgang entfällt. Nachteilig wirkt sich die wesentlich längere Rechenzeit bei der Ausführung von Programmen aus, da die Befehlszeilen interpretiert und Variablenwerte über Tabellen ausgetauscht werden müssen.

3. Debugger

Mittels eines Debuggers können semantische Fehler zur Laufzeit auf Assembler- oder Hochsprachenebene gezielt untersucht werden. Dazu wird das zu untersuchende Programm mit zusätzlichem "Debug-Code" compiliert und unter der Kontrolle des Debuggers gestartet. Zur Fehlersuche bieten Debugger mehrere Kontrolloptionen. Beispielsweise kann ein Programm "befehlsweise" ausgeführt werden (Trace), in dem entsprechend dem Programmfluß ein Befehl ausgeführt wird und dann das Programm angehalten wird. Danach ist es möglich, Register-, Variablen- oder Speicherinhalte zu kontrollieren oder für den nächsten Trace-Schritt zu verändern. Ein weitere Möglichkeit, in einen Haltepunkt zu gelangen, ist die Verwendung von Breakpoints. Hierbei wird nach Definition eines Breakpoints für einen Befehl in einem bestimmten Programmsegment, das Programm gestartet und wie zur normalen Laufzeit bedient.

2.2/5

Läuft das Programm während seiner Abarbeitung auf einen Breakpoint, so wird das Programm an dieser Stelle unterbrochen und man hat die Möglichkeit, wie im Trace-Modus, Variablenwerte zu kontrollieren. Ein nachfolgender Trace oder das Setzen eines anderen Breakpoints ist in der Regel möglich. Mit diesen Optionen ist es möglich, gezielt das Verhalten von als fehlerhaft vermuteten Routinen zu untersuchen, Parameterübergaben zwischen Unterprogrammen zu kontrollieren, Feldüberläufe oder falsche Wertzuweisungen zu entdecken.

2.2.4 Simulatoren, Emulatoren

Ein Simulatorprogramm bildet das Verhalten eines Systems auf der Basis von Modellen dieser Zielarchitektur nach. Die Abarbeitung eines Programms der Zielarchitektur erfolgt in der Art eines Interpreters. In der Regel wird bei Simulatoren nur der Programmfluß nachgebildet. Das zeitliche und physikalische Verhalten der Zielarchitektur wird dabei nicht berücksichtigt.

Ein Emulator bildet dagegen die Eigenschaften einer Zielarchitektur derart nach, daß ablauffähige Programme dieser Zielarchitektur auf einem Host-System mittels Emulator direkt abgearbeitet werden können. Eine Interpretation findet nicht statt. Stattdessen werden die benutzten Hardwarekomponenten (Prozessoren, Speicher, Controller, ...) vom Emulator derart nachgebildet, daß sie die Maschineninstruktionen der Zielarchitektur direkt verarbeiten und in entsprechende Aktionen umsetzen können. Das zeitliche und/oder physikalische Verhalten der einzelnen Komponenten kann hierbei berücksichtigt werden. Im wesentlichen existieren heutzutage zwei Ausprägungen von Emulatoren: hardwareorientierte Emulatoren und softwareorientierte Emulatoren.

Unter hardwareorientierten Emulatoren versteht man im allgemeinen komplexere FPGA 1) - oder Prozessor-gesteuerte Geräte , die in der Zielarchitektur anstelle des Prozessors / Controllers über einen Stecker mit Kabel eingesteckt werden. Diese Emulatoren bilden das zeitliche Verhalten der emulierten Komponenten in idealer Weise nach, ohne das Zielsystem zeitlich zusätzlich zu belasten. Es sind allerdings für diese Emulatoren hohe Investitionskosten notwendig. Softwareorientierte Emulatoren bilden die Hardware des Zielsystems nur durch die Anwendung von Software nach. Die Kosten für diese Emulatoren sind geringer. Es ist allerdings eine sehr genaue zeitliche Abbildung des Verhaltens, insbesondere des Echtzeitverhaltens, im Vergleich zu den hardwareorientierten Emulatoren nicht möglich.

2.2/6

2.2.5 Entwicklungsumgebungen

Bis Anfang der neunziger Jahre bestand die klassische Entwicklungsumgebung aus einem Editor, einem Compiler, einem Assembler und einem Debugger. Bei Mikrocontrollern wurde für eingebettete Anwendungen (embedded control) zusätzlich ein hardwareorientierter Emulator (In-Curcuit Emulator) und ein Hostsystem mit Cross-Compiler verwendet. Der Host war in der Regel eine UNIX-Workstation. Echtzeitbetriebssysteme wurden selten eingesetzt. Heutzutage führt der Trend zu immer weniger Hardware und immer mehr Software, um die für ein System immer kürzer werdenden Entwicklungszyklen (time-to-market) auffangen zu können. In Zeiten harter Konkurrenz können Applikationen durch Simulatoren und Software-Emulatoren bereits entwickelt werden, obwohl die darunter liegende Hardware noch nicht verfügbar ist.

Eine heutige Entwicklungsumgebung präsentiert sich dem Entwickler mit einer graphischen Benutzeroberfläche, in der alle Entwicklungswerkzeuge integriert sind. Als Host-System wird eine Workstation oder ein PC benutzt. Echtzeitbetriebssysteme sind bei embedded control-Anwendungen die Regel. An modernen Entwicklungswerkzeugen werden neben C-Compilern, C++-Compilern, Assemblern, mächtigen Bibliotheken, Linkern und Debuggern auch Simulatoren und softwareorientierte Emulatoren angeboten. Die Umgebungen werden durch Dienstleistungsprogramme, wie Projektmanager, Browser, Versionsverwaltung, Revisionskontrolle und leistungsfähige Online-Hilfe ergänzt. Für das Hardware-Debugging wird ein universeller Logikanalysator eingesetzt. Nur in speziellen Problemfällen bzw. zum abschließenden Test des Projektes wird auf hardwareorientierte Emulatoren und Hardware-Debugger zurückgegriffen.


2.1 Einleitung 2.3 Einführung in die Assemblerprogrammierung