Anhand von Beispielen sollen die grundsätzlichen Arbeitsweisen und die Unterschiede zwischen einem Compiler und einem Interpreter verdeutlicht werden.
Normalerweise versteht der Computer nur seine eigene Sprache, die aus einem bestimmten Code, dem sogenannten Maschinencode, aufgebaut ist. Diese Sprache, die bei jedem Prozessor (CPU) unterschiedlich ist, besteht aus nichts weiterem als einer Folge von Nullen und Einsen. Daß ein solcher Code (z. B. 0011000000111100 bedeutet lade eine Zahl, hier 65535, in ein bestimmtes Register des Prozessors) für den Menschen sehr schwer fehlerfrei zu programmieren ist, ist wohl eindeutig. Deswegen ist man einen Schritt weiter gegangen und hat die sogenannte »Assemblersprache« entwik-kelt. Bei der Assemblerprogrammierung verschwinden nun diese umständlich zu programmierenden Nullen und Einsen, stattdessen wird jeder Befehl durch ein Kürzel, auch Mnemonik genannt, wiedergegeben. Das oben erwähnte Beispiel lautet jetzt „MOVE.W #$FFFF,D0“. Trotzdem ist das Programmieren in Assembler eine mühselige Aufgabe, da die Kommunikation mit dem Rechner immer noch stark maschinenorientiert ist. Außerdem sind Programme, die in Assembler geschrieben sind, nicht portabel, d. h. sie sind nur auf den Mikroprozessor des Rechners bezogen. Assembler stellt sozusagen die unterste Sprachebene aller Programmiersprachen dar.
Mit diesen Problemen haben sich verschiedene Softwareentwickler in den 50er Jahren beschäftigt. Seitdem gibt es das Konzept der höheren Programmiersprache. Anders als in Assembler werden hier die Programme in einer „menschnahen“ Sprache entwickelt. Ein Programm, das in einer höheren Sprache geschrieben ist, ist dadurch
maschinenunabhängig und läßt sich deshalb leichter auf andere Computersysteme übertragen. Bekannte Vertreter einer Hochsprache sind Basic, Pascal, Fortran, Cobol, ’C’,..., um nur ein paar zu nennen. Allerdings ergibt sich durch die Verwendung einer Hochsprache ein Problem, daß nämlich der Prozessor eine Sprache, die der menschlichen Denkweise entgegenkommt, nicht verarbeiten kann. Um dennoch in einer Hochsprache programmieren zu können, ist eine Übersetzung des Programmtextes in den maschinenverständlichen Code notwendig. Ein solches Übersetzungsprogramm hat man in zwei Varianten realisiert. Zum einen gibt es sogenannte Compiler, andererseits die Interpreter. Um den Unterschied zwischen beiden zu verstehen, muß man die Arbeitsweise beider Programme verstehen.
Basic ist ohne Zweifel die am meisten verbreitete Hochsprache im Home-und Personal-Computer-Bereich. Seine einfache Struktur, die Flexibilität beim Programmieren (im Unterschied zu Pascal, bei dem sich der Anwender über die Struktur seines Programmes von vornherein im Klaren sein muß) und die leichte Erlernbarkeit machen Basic so beliebt. Basic wird in der Regel als Interpreter geliefert, und manche Rechner werden schon vom Werk aus mit dieser Sprache versehen. Es existieren aber auch einige Basic-Compiler, die das schon fertige und geprüfte Basic-Programm zu compi-lierlen ermöglichen. Daß Basic größtenteils als Interpreter vorliegt, hat für die Verbreitung dieser Sprache beigetragen.
Interpretersprachen haben den Vorteil, daß sich Programme damit besonders schnell entwickeln lassen und eventuelle Fehler leicht auszubessern
sind. Dem gegenüber steht die relativ langsame Ausführungsgeschwindigkeit solcher Programme. Warum das so ist, können wir nur erfahren, wenn wir einiges mehr über die Arbeitsweise eines Interpreters wissen.
Ein Interpreter ist im wesentlichen eine Sammlung von Unterroutinen, die in Maschinensprache (Assembler) geschrieben sind. Zu diesen Routinen gehört eine Tabelle, welche die verschiedenen Befehle des Interpreters und die entsprechenden (Sprung-) Adressen der dazu gehörenden Unterroutinen beinhaltet. Die Variablen werden auch gesondert verwaltet, und ihre Organisation ist einer der kompliziertesten Teile solcher Interpreter. Schließlich existiert noch eine sogenannte Interpreterschleife.
Beim Editieren (Eingeben) in Basic geschieht schon eine ganze Menge. Der Editor eines Basic-Interpreters ist sozusagen intelligent. Bestimmte Befehle werden sofort nach Beendigung der Eingabe durch „RETURN“ ausgeführt (ohne RUN etc. eingeben zu müssen), daher nennt man diese Betriebsart den „Direkt-Modus“. Im Gegensatz dazu wird jede Zeile, die mit einer Zahl beginnt, als Programm angesehen (“Programm-Modus“) und nicht sofort ausgeführt. Manche Editoren prüfen direkt nach der Eingabe die Syntax („Rechtschreibung“ der Befehle) und geben gegebenenfalls eine Fehlermeldung aus. Andere wandeln während des Editierens die eingegebenen Befehle in einen speziellen Zwischen-Code um. Dieser aus einem Byte bestehende Code (auch Tokens genannt) erleichtert die Arbeit des Interpreters. Wir wollen hier jedoch nicht auf „Feinheiten“ einzelner Interpreter eingehen, sondern nun vielmehr die grundsätzliche Arbeitsweise anhand eines kleinen Beispiels erklären. Nachdem der Programmtext (siehe unten) erstellt worden ist, tritt die schon erwähnte Interpreterschleife in Kraft. Der gesamte Programmtext wird nun nach jedem einzelnen Begriff analysiert. Diese Begriffe werden mit der oben genannten Tabelle verglichen, und wenn es sich um einen Befehl handelt, springt die Interpreterschleife in die dazugehörige Unterroutine und führt sie aus. Sehen wir nun ein Beispiel an:
10 FOR x=1 TO 100
20 PRINT ”ST Computer”
30 NEXT x
Dieses Programm veranlaßt, daß „ST-Computer“ 100 mal auf dem Bildschirm dargestellt wird. Der Interpreter liest bei der ersten Programmzeile (10) das Wort FOR, da es sich zwischen zwei Blancs (Leerzeichen) befindet, und vergleicht es nun mit der Tabelle, in der sich die Befehle befinden. In dieser Tabelle findet er die Adresse der entsprechenden Maschinensprache Unterroutine für „FOR“. Die Interpreterschleife springt zu dieser Adresse und läßt die Unterroutine ablaufen. In der Routine selbst werden verschiedene Parameter (z. B. Anfang-und Endwert der Variablen x) in einem reservierten Speicherplatz abgelegt. Sodann liest die Interpreterschleife in der Zeile 20 das Wort PRINT, wobei es wieder mit der oben genannten Tabelle verglichen und ausgeführt wird. In der Zeile 30 wird die Variable x um eins erhöht und mit dem Endwert (hier 100) verglichen, nachdem zuvor das Wort NEXT als Basic-Befehl erkannt und interpretiert worden ist. Die Variable x, die schon am Anfang in einer separaten Tabelle gespeichert wurde, wird mit dem erneuten Wert aktualisiert. Solange der Endwert noch nicht erreicht ist, springt der Interpreter in die Zeile 10 zurück und fängt wieder von vorne an.
Wie man sieht, besteht die Arbeit eines Interpreters hauptsächlich darin, Begriffe zu lesen, zu vergleichen und Unterroutinen auszuführen. Dies alles läuft - sozuagen im Hintergrund -während eines Programmablaufes ab, so daß viel „Prozessorzeit“ verlorengeht und das eigentliche Programm langsam wird. Als Vorteil von Interpretersprachen kann man die leichte Programmerstellung, das bequeme Ändern eines Programmes oder das schnelle Finden eines Fehlers erwähnen.
Ein Interpreter, wie schon oben erwähnt wurde, führt jeden Befehl sofort aus. Ein Compiler übersetzt zuerst das gesamte Programm in Maschinensprache, und daraufhin wird der generierte Code ausgeführt.
Der größte Vorteil einer solchen Methode ist der Gewinn an Geschwindigkeit, den ein compiliertes Programm gegenüber einem interpretierenden Programm aufweisen kann. Der Geschwindigkeitsgewinn bei compilierten Programmen beträgt das ca. 100fache.
Bis ein lauffähiges Programm in einer Compiliersprache erzeugt ist, sind mehrere, teilweise längere, Vorgänge nötig. Der Text muß gelesen, analysiert und auf Syntax geprüft werden, bis der Compiler in der Lage ist, ein in Maschinencode geschriebenes Programm zu generieren. Dies verschiedenen Arbeitsphasen werden je nach Compiler auf unterschiedliche Art und Weise ausgeführt. Die Anzahl der insgesamt notwendigen Vorgänge hängt teilweise von der Größe des internen Speichers sowie von der Struktur der Programmiersprache ab. Ein großer Arbeitsspeicher erlaubt ein Auskommen mit wenigen „Passes“, da mehrere Vorgänge sowie Zwischenergebnisse in dem internen Speicher Platz finden. Es gibt Programmiersprachen, die aufgrund ihrer Struktur unbedingt zwei Passes erfordern. Andere, wie z. B. PASCAL, kommen mit einem Pass aus.
Der Scanner übernimmt den ersten Teil der Compilierung. Der Scanner liest den Programmtext auf der Diskette Zeichen für Zeichen. Dabei versucht er, diesen Quellentext in Grundeinheiten zu zerlegen. Diese Grundeinheiten sind beispielsweise Befehle, Variabein, Operatoren etc. Diese Einheiten werden umgewandelt in eine für den nächsten Vorgang verständliche Form. Auch hier werden sämtliche bedeutungslose Leerzeichen eliminiert. Der Scanner hat also die Aufgabe der Überprüfung auf die Richtigkeit des Textes (in grammatikalischer Hinsicht). Dabei hilft sich der Compiler mit Tabellen, in denen sich die „Worte“ der Programmiersprache befinden. Trifft der Scanner auf ein Wort, wird es zuerst in der Tabelle gesucht und auf eine Übereinstimmung geprüft. Trifft das zu, wird es in eine bedeutungstragende Einheit („Tokens“) geformt.
Der zweite Vorgang, einer der aufwendigsten, ist der Parser. Der Scanner liest den vorliegenden Programmtext und wandelt ihn in die obengenannte Token um. Aber eine Sprache wird erst verständlich, wenn man diese einzelnen Worte zu bedeutungsvollen Sätzen zusammenfügt. Man kann ohne weiteres behaupten, daß der folgende Satz ein in deutscher Sprache geschriebener Satz ist: „Wie lang, wie unerschöpflich lang ist ein Frühling vorzeiten gewesen, als ich noch ein Knabe war!.,,
Anders ist dies bei dem nächsten Satz, der nicht gerade in einem korrekten Deutsch geschrieben wurde: „Kommt zu mir ich lerne Dich Deutsch.“
Eine Programmiersprache hat eine bestimmte Syntax, die berücksichtigt werden muß. Dafür sorgt der Parser. Er liest die schon von dem Scanner vorbereiteten Einheiten und analysiert sie auf die richtige Syntax. Erkennt er bei der Durchsuchung einen Syntaxfehler, so weist der Parser sofort darauf hin. Ist im Gegenteil das Programm frei von Syntaxfehlern, kann der Parser seine Analyseergebnisse weitergeben.
Der nächste Teil eines Compilers ist die sogenannte Codegenerierung. Das Ergebnis des Parsers wird von dieser Instanz in Maschinenbefehle umgesetzt. Das Objektprogramm (so heißt das lauffähige compilierte Programm) ist nicht mehr als eine Zusammenfassung dieser Befehle. Hier können immer noch Probleme anderer Art auf-treten. Das Programm kann von grammatikalischen und syntaktischen Fehlern frei sein, was aber nicht bedeutet, daß es von logischen Fehlern frei ist.
Es gibt Compiler, die keinen direkt ausführbaren Maschinencode, sondern eine Art Zwischensprache, die für keinen wirklichen Mikroprozessor gedacht ist, generiert.
Diese Zwischensprache wird über einen Interpreter zur Ausführung gebracht. Ein Beispiel solcher Pseudo-Compiler ist das UCSD-Pascal.
Es gibt ein zur Zeit sehr populäres Wort in der Computerwelt: „Modularität“. Unter Modularität versteht man das Schreiben eines umfangreichen Programms in kleinen Teilen und das spätere Zusammenfugen in eine einzige Einheit. Genauso ist es denkbar, bestimmte Prozeduren, die öfter Vorkommen, in verschiedene Programme einzusetzen, ohne sie erneut zu schreiben. Das ist die Aufgabe des Linkers (zu Deutsch: Binder). Er fügt alle einzelnen Teile zu einem gesamten lauffähigen Programm zusammen.
Wie schon am Anfang gesagt wurde, verhält sich jede Sprache etwas anders. Eine gründliche und generelle Beschreibung, wie eine Compilersprache zu handhaben wäre, ist nicht möglich. Die nötige Information muß dem dazugehörigen Handbuch entnommen werden.
Wir werden anhand eines Beispiels des C-Compilers von Digital Research zeigen, wie man ein Programm in „C“ compiliert.
Sie brauchen, um ein Programm zu schreiben (Quellcode zu erzeugen), einen Editor. In dem Entwicklungspaket, das von Atari vertrieben wird, ist der Editor von Matacomco enthalten. Außerdem finden Sie in dem oben genannten Entwicklungspaket:
Ebenso gehört zu dem Entwicklungspaket ein Programm mit dem Namen COMMAND.PRG: Es bildet eine Art shell zwischen Anwender und Rechner, so daß Sie nicht mehr von der bedienungsfreundlichen GEM-Umgebung profitieren können. Dieses shell hat aber den Vorteil, daß man direkte Befehle im Interaktiv-Modus angeben kann.
Hier die verschiedenen Schritte, bis ein Programm in C compiliert wird:
(UB/MM)