Westfälische Wilhelms-Universität Münster Ausarbeitung Zwischencode-Erzeugung im Rahmen des Seminars Übersetzung von künstlichen Sprachen“ ” Sebastian Hanneken Themensteller: Prof. Dr. Herbert Kuchen Betreuer: (MScIS Tim A. Majchrzak) Institut für Wirtschaftsinformatik Praktische Informatik in der Wirtschaft Inhaltsverzeichnis 1 Einleitung 1 2 Grundlagen 2 2.1 Einordnung in den Compile-Prozess . . . . . . . . . . . . . . . . . . . 2 2.2 Funktion und Arten des Zwischencodes . . . . . . . . . . . . . . . . . 2 2.3 Definition und Repräsentation von 3-Adresscode . . . . . . . . . . . . 4 3 Syntaxgesteuerte Erzeugung von Zwischencode 6 3.1 Handhabung von Deklarationen . . . . . . . . . . . . . . . . . . . . . 6 3.2 Übersetzung von Ausdrücken . . . . . . . . . . . . . . . . . . . . . . 7 3.2.1 Zuweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 3.2.2 Boolesche Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . 8 3.2.3 Arrayzugriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 3.3 Kontrollstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 3.3.1 Übersetzung von Schleifen mittels ererbter Attribute . . . . . 12 3.3.2 Reduktion von goto-Instruktionen . . . . . . . . . . . . . . . . 13 3.3.3 Backpatching . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 3.3.4 Switch-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . 18 4 Zwischencode am Beispiel des Java Bytecode 19 5 Zusammenfassung und Ausblick 22 Literaturverzeichnis 24 i Kapitel 1: Einleitung 1 Einleitung Today, compilers and high level languages are the foundation of the ” complex and ubiquitous software infrastructure that undergirds the global economy.“ [HPP09, S. 60] Dieses Zitat beschreibt bereits die große Bedeutung, welche Compiler aktuell aufweisen. Die effiziente Übersetzung einer Hochsprache in eine maschinenverwertbare Form stellt den Compiler-Entwickler vor eine Herausforderung. Die im Compilerbau angewendeten Technologien und Techniken finden ihre Anwendung darüber hinaus auch in vielen weiteren Gebieten [ALSU07, S. 17]. Eine Auseinandersetzung mit den verschiedenen Bereichen eines Compilers ist daher besonders interessant. Vor der eigentlichen Übersetzung des Quellprogramms in Maschinencode wird in vielen Fällen zunächst ein Zwischencode erstellt. Ziel dieser Arbeit ist es, die Bedeutung dieser Phase anhand der Übersetzung verschiedener Programmstrukturen zu erläutern. Die Ausarbeitung wird hierbei wie folgt untergliedert: Zunächst wird im Grundlagenkapitel 2 die Erzeugung des Zwischencodes in den übergeordneten Compile-Prozess eingeordnet. Im Folgenden wird sowohl die Funktion des Zwischencodes beschrieben als auch verschiedene Arten von Zwischencode (insbesondere der dieser Arbeit zugrunde liegende 3-Adresscode) vorgestellt. In Kapitel 3 steht die syntaxgesteuerte Erzeugung des 3-Adresscode für Beispielkonstrukte einer Programmiersprache im Mittelpunkt der Betrachtung. Nach der Darstellung der Handhabung von Deklarationen wird die Übersetzung von Ausdrücken erläutert, wobei im Einzelnen Zuweisungen, boolesche Ausdrücke und Arrayzugriffe behandelt werden. Im Rahmen der Übersetzung von Kontrollstrukturen werden neben der einfachen Übersetzung zwei Erweiterungen (Reduktion von gotoInstruktionen und Backpatching) vorgestellt, welche die Effizienz der Ausführung erhöhen. Abschließend wird die Umsetzung von Switch-Anweisungen demonstriert. In Kapitel 4 wird das Augenmerk auf die reale Programmiersprache Java gelegt, wobei deren Zwischencode skizziert und anhand des zuvor Vorgestellten eingeordnet wird. Die Ausarbeitung endet mit einer Zusammenfassung und einem kurzen Ausblick. 1 Kapitel 2: Grundlagen 2 Grundlagen n 2.1 + m n Quellsprache 1 HW-Plattform 1 Quellsprache 1 HW-Plattform 1 Quellsprache 2 HW-Plattform 2 Quellsprache 2 HW-Plattform 2 * m Einordnung in den Compile-Prozess Die Aufgabe eines Compilers besteht darin, ein Programm in einer bestimmten ZC Quellsprache 3 in ein äquivalentes HW-Plattform 3 Quellsprache 3 3 sog. Quellsprache, Programm einer HW-Plattform Zielsprache zu übersetzen [ALSU07, S. 1]. Um die effiziente Übersetzung sicherzustellen, wird diese in verQuellsprache 4 HW-Plattform 4 Quellsprache 4 HW-Plattform 4 schiedene Phasen unterteilt, welche je nach Anwendungsgebiet unterschiedlich ausgestaltet werden können. Die nachfolgende Abbildung 1 visualisiert die Phasen der Compilierung, wie sie im Rahmen dieses Seminars behandelt werden. Nach der lexiQuellProgramm Lexer (Lexikalische Analyse) Parser (Syntaktische Analyse) Statische Überprüfung Zwischencodeerzeugung Zwischencode front end Codeerzeugung back end Abbildung 1: Compile-Prozess (in Anlehnung an [ALSU07, S. 357]) Offset kalischen Analyse [ALSU07, Kap. 3], die dasBezeichner Programm derTypQuellsprache für die arr array(3, integer) 0 record 12 weitere Bearbeitung analysiert und in sinnvollerechteck Sequenzen zerlegt, erstellen die verhöhe float 20 schiedenen Methoden der syntaktischen Analyse eine baumartige Darstellung (SynBezeichner Typ Offset breite integer 4 taxbaum), welche die grammatikalische Struktur widerlänge der vorherigen integer Zerlegung 0 spiegelt [ALSU07, Kap. 4]. Hieran schließt sich die statische Überprüfung an, welche unter anderem die Typüberprüfung beinhaltet [ALSU07, Kap. 6.5]. Neben den If - Schleife If-Else - Schleife While - Schleife impliziten Zwischendarstellungen wie dem Syntaxbaum, wird in vielen Fällen zum Abschluss des front ends“ eine explizite Zwischendarstellung (im Folgenden Zwi” schencode) erzeugt, welche die Schnittstelle zwischen front end“ und back end“ ” ” beschreibt und als ein Programm für eine abstrakte Maschine angesehen werden ... kann [ALSU07, S. 9]. ... Je nach Compiler kann der Zwischencode verschiedene Formen annehmen, er ... spiegelt jedoch auf jeden Fall das Quellprogramm korrekt wieder. Guter Zwischen- code zeichnet sich dadurch aus, dass er auf einfache Weise erstellt, aber auch gut in den jeweiligen Maschinencode transformiert werden kann [ALSU07, S. 9]. Zudem sollte jede hierin verwendete Operation eine einfache und klare Bedeutung haben, um spätere Optimierungen zu erleichtern [Ap02, S. 137]. Im folgenden Kapitel wird auf die Funktion und die Arten des Zwischencodes näher eingegangen. 2.2 Funktion und Arten des Zwischencodes Die Idee des Zwischencodes leitet sich direkt aus der großen Menge verschiedener Quellsprachen bzw. Hardware-Plattformen ab. Ein spezieller Compiler übersetzt ge2 Kapitel 2: Grundlagen nau eine Quellsprache in eine Zielsprache (z. B. in einen von der Hardware abhängigen Maschinencode)[ALSU07, S. 357]. Ändert sich entweder die Quell- oder Zielsprache, ist der gesamte Compiler zu modifizieren. Um jede Quellsprache (n) in jeden Maschinencode (m) übersetzen zu können, benötigt man n ∗ m verschiedene Compiler. Eine Reduktion der benötigten Beziehungen kann durch eine Trennung von front ” end“ und back end“ erreicht werden. Der jeweilige Zwischencode fungiert als Schalt” werk zwischen den verschiedenen front ends“ und back ends“, welche auf diesem ” ” aufsetzen und macht jede der n Quellsprachen in Bezug auf die existierenden back ” ends“ plattformunabhängig (vgl. auch UNCOL als Idee eines idealisierten Zwischencodes [SOWMTS58]). Bei der Entwicklung einer neuen Quellsprache muss lediglich durch die Bildung eines auf die Besonderheiten dieser Sprache abgestimmten front ends“ die effiziente Übersetzung in den Zwischencode sichergestellt werden, ” um alle bestehenden, von der neuen Quellsprache unabhängigen und nur auf dem gemeinsamen Zwischencode aufsetzenden, back ends“ ohne zusätzlichen Aufwand ” für die Codeerzeugung nutzen zu können. Dieses führt zu einer Reduktion der Anzahl der benötigten Compiler auf n + m (vgl. Abbildung 2), um die gleiche Anzahl an Quellsprachen bzw. Hardware-Plattformen abdecken zu können. Die Auswahl bzw. n + m n Quellsprache 1 HW-Plattform 1 Quellsprache 1 HW-Plattform 1 Quellsprache 2 HW-Plattform 2 Quellsprache 2 HW-Plattform 2 * m ZC Quellsprache 3 HW-Plattform 3 Quellsprache 3 HW-Plattform 3 Quellsprache 4 HW-Plattform 4 Quellsprache 4 HW-Plattform 4 Abbildung 2: Reduktion der Beziehungen (vgl. [Ap02, S. 137]) das Design des zu verwendenden Zwischencodes hängt vom jeweiligen Compiler ab [ALSU07, S. 358 ff.]. Auf der einen Seite können andere Sprachen wie z. B. C als Zwischencode verwendet werden, auf der anderen Seite kann ein reiner Zwischencode zur Anwendung kommen, wie er in den folgenden zwei Beispielen kurz vorgestellt wird. Abstrakte Syntaxbäume können direkt als Input für das jeweilige back end“ ” fungieren. Hierbei handelt es sich um einen Zwischencode auf einem hohen Level, d. h. dessen Struktur weist starke Analogien zur Quellsprache auf. Operatoren und 3 Kapitel 2: Grundlagen atomare Operanden werden durch innere Knoten respektive Blattknoten abgebildet. Eine spezielle Art der Syntaxbäume sind die sog. gerichteten azyklischen Graphen (DAG), bei denen, sobald sich ein Konstrukt der Quellsprache wiederholt, im Gegensatz zu den einfachen Syntaxbäumen, ein gemeinsamer Knoten modelliert wird. Die Postfix-Notation steht für eine bestimmte Anordnung der Operatoren und Operanden [Gu99, S. 180 f.]. Hierbei werden zunächst die Operanden und danach der Operator erzeugt. Durch dieses Vorgehen lässt sich eine abstrakte Stack-Maschine modellieren. Die Operation a + b kann z. B. durch die Postfix-Notation a b + dargestellt werden. In der vorliegenden Seminararbeit soll jedoch die Erzeugung des Zwischencodes anhand der Generierung von 3-Adresscode (3AC) erläutert werden, welcher im folgenden Kapitel definiert wird. 2.3 Definition und Repräsentation von 3-Adresscode 3-Adresscode ist eine linearisierte Form eines Syntaxbaumes (bzw. DAG) und besteht aus den beiden Konzepten Adressen und Instruktionen [ALSU07, S. 363 ff.]. Auf der rechten Seite einer Instruktion darf jedoch höchstens eine arithmetische bzw. logische Operation op“ oder eine relationale Operation relop“ verwendet ” ” werden. Die Instruktionen beziehen sich auf eine oder mehrere Adressen, welche entweder Bezeichner des Quellprogramms, Konstanten oder vom Compiler generierte temporäre Bezeichner sein können. Bezeichner des Quellprogramms stellen hierbei Zeiger auf Einträge der aktiven Symboltabelle dar, worauf im Zuge der Handhabung von Deklarationen näher eingegangen werden soll. Tabelle 1 bietet einen Überblick über die in dieser Ausarbeitung verwendeten 3-Adressinstruktionen. Bestimmte 3-Adressinstruktionen können hierbei mit einer symbolischen Textmarke L eindeutig identifiziert werden, was unter anderem im Rahmen von bedingten und unbedingten Sprüngen von Bedeutung ist. Zur Repräsentation des 3AC in einer Datenstruktur bieten sich verschiedene Darstellungsmöglichkeiten an [ALSU07, S. 366 ff.]. Bei der Darstellungsform Quadru” pel“ werden die Operatoren und Adressen der Instruktionen durch <op, arg1 , arg2 , Ergebnis> auf 4 Felder aufgeteilt (vgl. Tabelle 2). Nicht benötigte Felder (z. B. bei unären Operatoren wie minus) werden nicht ausgefüllt. Ebenso wird bei bedingten und unbedingten Sprüngen lediglich das Ergebnis-Feld für die Textmarke benötigt. Die Felder beider Argumente bleiben in diesem Fall unausgefüllt. Tripel“ haben im ” Gegensatz hierzu lediglich drei Felder <op, arg1 , arg2 > (vgl. Tabelle 3). Auf die konkrete Bezeichnung mithilfe einer expliziten temporären Variable wird in diesem Fall verzichtet. Möchte man dennoch auf das Ergebnis eines vorherigen Befehls zugreifen, 4 Kapitel 2: Grundlagen x = y op z Zuweisungsinstruktionen, wobei x, y und z Adressen sind. x = op y Zuweisung, wobei op unär ist (z. B. minus). x=y Kopierinstruktionen. goto L Unbedingter Sprung. Instruktion mit der Marke L wird als Nächstes ausgeführt. if x goto L bzw. Bedingte Sprünge, die in Abhängigkeit von der ifFalse x goto L Bedingung x zur Instruktion L springen. if x relop y goto L Bedingte Sprünge, die den Sprung von der Auswertung der Bedingung x relop y abhängig machen. x = y[i] bzw. Indizierte Kopierinstruktionen. Mit x[i] wird der i-te x[i] = y Wert im Speicher nach Position x angesprochen. Tabelle 1: Instruktionen des 3-Adresscodes (vgl. [ALSU07, S. 364 f.]) geschieht dieses über die sog. Wertnummer, welche die Instruktion kennzeichnet, die das Ergebnis berechnet. Zur eindeutigen Abgrenzung einer Wertnummer von einer Konstante wird diese in Klammern gesetzt. Im Folgenden soll dargestellt werden, 0 minus breite 1 + 5 2 = t2 t1 t1 0 minus breite t2 1 + 5 (0) x 2 = x (1) ··· ··· Tabelle 2: Quadrupel Tabelle 3: Tripel wie die verschiedenen Konstrukte des Quellprogramms in 3AC umgesetzt werden können. Die Erzeugung des Zwischencodes wird hierbei mithilfe von zwei Arten syntaxgerichteter Formulierungen verdeutlicht. Bei einer syntaxgerichteten Definition (SGD) werden der kontextfreien Grammatik Attribute und semantische Regeln hinzugefügt [ALSU07, S. 304]. Syntaxgerichtete Übersetzungsschemata (SGÜ) bilden eine verwandte Notation, bei der die semantischen Regeln in die Produktionen eingebettet sind. Zur besseren Lesbarkeit werden diese hierfür in geschweifte Klammern eingefasst [ALSU07, S. 324]. Für eine genauere Definition sei jedoch auf das ebenfalls während des Seminars behandelte Thema Syntaxgerichtete Übersetzung ” und Typüberprüfung“ verwiesen. 5 Kapitel 3: Syntaxgesteuerte Erzeugung von Zwischencode 3 Syntaxgesteuerte Erzeugung von Zwischencode 3.1 Handhabung von Deklarationen Obwohl Deklarationen nicht direkt in Zwischencode übersetzt werden, ist ihre Behandlung von großer Bedeutung. Die eigentliche Zuweisung des Speicherplatzes für die verschiedenen Deklarationen erfolgt erst zur Laufzeit, da diese unter anderem stark von der jeweiligen Zielmaschine abhängen [ALSU07, S. 373 ff.]. Das Speicherlayout für die in den Deklarationen verwendeten Bezeichner kann jedoch unter Zuhilfenahme sog. relativer Adressen bereits während der Kompilierung spezifiziert werden, um hierdurch nachgelagerte Prozesse zu unterstützen. Als Resultat enthält die Menge der Symboltabellen für jeden verwendeten Bezeichner dessen Typ und dessen relative Adresse, d. h. dessen relative Position im Speicher, ausgehend von einem fiktiven Startpunkt. Eine Ausnahme sind Datentypen mit dynamischer Größe, die auf diese Art und Weise nicht behandelt werden können und somit nicht Bestandteil dieser Ausarbeitung sind. Der eigentliche Speicherplatzbedarf ist abhängig vom jeweiligen Typ des Bezeichners und den Eigenschaften der Zielmaschine. Datentypen die mehr als ein Byte benötigen werden in aufeinander folgenden Bytes abgespeichert, wobei deren relative Adresse durch die Position des ersten Bytes bestimmt wird. Die Größe der Basistypen (Integer, Float, etc.) sind feste Werte und können direkt bei der Berechnung eingesetzt werden. Handelt es sich um den Datentyp Array, lässt sich dessen Größe durch Multiplikation der Größe eines Elements mit der Anzahl der Elemente bestimmen. Ist die Größe eines Integers mit 4 Byte angegeben, lässt sich die gesamte, durch die Deklaration int[3] zahl;“ benötigte ” Speichermenge durch 3 · [Größe Integer(4)] = 12“ bestimmen. Die nachfolgende ” Deklaration bekäme in diesem Fall eine um 12 erhöhte relative Adresse im Vergleich zur vorherigen Deklaration. Einen Sonderfall stellt der Record-Datentyp dar [ALSU07, S. 376 ff.]. Mit dessen Hilfe können komplexe Strukturen definiert werden, welche wiederum Deklarationen enthalten können. Aus diesem Grund wird für jeden Record-Datentyp eine neue Symboltabelle angelegt. Die im Record-Datentyp vorhandenen Deklarationen werden äquivalent behandelt, die berechneten relativen Adressen beziehen sich jedoch auf die soeben angelegte neue Symboltabelle. Der benötigte Speicherbedarf des Record-Datentyps berechnet sich aus der Summe des Speicherbedarfs der in ihr enthaltenen Deklarationen. Im Beispiel (Abbildung 3) enthält der Record mit dem Namen rechteck“ zwei Integerwerte mit je einer Größe von 4 Byte, sodass sich für ” die Größe des Record-Datentyps 8 Byte ergeben. Nach der Behandlung des Record- 6 Kapitel 3: Syntaxgesteuerte Erzeugung von Zwischencode int[3] arr; record { int länge; int breite;} rechteck; float höhe; Bezeichner arr rechteck höhe Typ array(3, integer) record float Offset 0 12 20 Bezeichner länge breite Typ integer integer Offset 0 4 Abbildung 3: Handhabung verschiedener Deklarationen Datentyps wird die alte Symboltabelle wieder hergestellt und die noch folgenden Deklarationen beziehen sich wieder auf den Anfang der ursprünglichen Symboltabelle. Zuletzt wird ebenfalls in dieser Symboltabelle ein Eintrag für den Bezeichner, den Typ und den Speicherbedarf des Record-Datentyps erstellt. Dessen Typ wird durch das Schlüsselwort record“ gekennzeichnet und enthält eine Referenz auf die ” ihm zugehörige Symboltabelle (vgl. auch Abbildung 3). Problematisch sind die als fix angenommenen Größen der jeweiligen Basistypen, die sich auf verschiedenen Zielmaschinen unterscheiden können. Um dieses Problem zu umgehen lassen sich symbolische Typgrößen verwenden, welche bei der späteren Codeerzeugung an die Gegebenheiten der Zielmaschine angepasst werden können [ALSU07, S. 386]. 3.2 Übersetzung von Ausdrücken 3.2.1 Zuweisungen Die nachfolgende syntaxgerichtete Übersetzung 1 beschreibt die Erzeugung von 3AC aus einer Anweisung (S) [ALSU07, S. 378 ff.]. Die internen Elemente der Anweisungen (hier: Zuweisungen) stellen in diesem Fall Ausdrücke (E) dar, welche durch Operationen verbunden sein können (hier: + bzw. −). Die Übersetzung folgt S → id = E; E → E1 + E2 { gen(top.get(id.lexeme) =“ E.addr ); } ” { E.addr = new Temp(); − E1 gen(E.addr =“ E1 .addr +“ E2 .addr ); } ” ” { E.addr = new Temp(); id gen(E.addr =“ minus“ E1 .addr ); } ” ” { E.addr = top.get(id.lexeme); } | | Syntaxgerichtete Formulierung 1: Zuweisungen dem schrittweisen Vorgehen, sodass die durch die Hilfsfunktion gen()“ gebildete 3” Adressinstruktion automatisch an die zuvor Erstellten angehängt wird. Die Adresse 7 Kapitel 3: Syntaxgesteuerte Erzeugung von Zwischencode (Bezeichner, Konstanten oder temporäre Variablen) der einzelnen Ausdrücke wird in einem Attribut addr“ gespeichert. Temporäre Variablen können mithilfe der ” Funktion new Temp()“ generiert werden. Namen lassen sich mit Hilfe des Bezeich” ners id“ und der Funktion top.get(id.lexeme)“ für die jeweilige Instanz in der ” ” Symboltabelle nachschlagen. Beispiel 1 verdeutlicht die Erzeugung von 3AC für die Zuweisung x = länge + − breite. x = länge + − breite t1 = minus breite 3AC =⇒ t2 = länge + t1 x = t2 Beispiel 1: Erzeugung von 3AC bei Zuweisungen Die Auswertung beider Ausdrücke erfolgt äquivalent. Zunächst wird eine neue temporäre Variable erstellt, in der das Resultat der Operation gespeichert werden soll. In einem zweiten Schritt kann der benötigte 3AC generiert und an den schon bestehenden angehängt werden. Schlussendlich kann die Generierung der eigentlichen Zuweisung erfolgen, wobei die Bezeichner breite“, x“ und länge“ zuvor in ” ” ” der Symboltabelle nachgeschlagen werden müssen. 3.2.2 Boolesche Ausdrücke Eine weitere Form von Ausdrücken stellen die booleschen Ausdrücke (B) dar. Hierunter wird die Anwendung von booleschen Operatoren (&& (UND), k (ODER), ! (NICHT)) auf boolesche Variablen oder relationale Ausdrücke der Form E1 rel E2“ ” verstanden, wobei rel.op einen der sechs Vergleichsoperatoren (<, ≤, =, !=, ≥, >) bezeichnet [ALSU07, S. 399 f.]. Boolesche Ausdrücke können durch die folgende Grammatik beschrieben werden. B → B k B | B && B | !B | (B) | E rel E | true | false Die Funktion dieser Ausdrücke kann auf der einen Seite die Steuerung des Kontrollflusses sein oder auf der anderen Seite die Ermittlung eines expliziten Wertes (true bzw. false). Letzterer kann in Analogie an die bereits vorgestellten Zuweisungen ausgewertet werden. In der vorliegenden Ausarbeitung soll jedoch nur die Übersetzung zur Steuerung des Kontrollflusses dargestellt werden. In Abhängigkeit des Wahrheitsgehalts des booleschen Ausdrucks wird die als nächstes auszuführende Anweisung bestimmt, welche mit den Textmarken B.true 8 Kapitel 3: Syntaxgesteuerte Erzeugung von Zwischencode bzw. B.false gekennzeichnet ist [ALSU07, S. 403 ff.]. Für einen relationalen Ausdruck der Form E1 rel E2“ kann der 3AC, welcher im Attribut code“ gespeichert ” ” wird, direkt erstellt werden. Gefolgt vom 3AC zur Auswertung der beiden OpeProduktion B → E1 rel E2 Semantische Regeln B.code = E1 .code k E2 .code k gen( if“ E1 .addr rel.op E2 .addr goto“ B.true) ” ” k gen( goto“ B.false) ” Syntaxgerichtete Formulierung 2: Boolsche Ausdrücke (1/2) randen E1 und E2 werden zwei Sprungbefehle erzeugt (vgl. auch Syntaxgerichtete Formulierung 2). Beim Ersten handelt es sich um einen bedingten Sprung, welcher zur Anweisung mit der Textmarke B.true springt, wenn der relationale Ausdruck erfüllt ist. Im anderen Fall sorgt ein zweiter, unbedingter Sprung für die Ausführung der Anweisung mit der Textmarke B.false. Weitaus interessanter ist jedoch die Behandlung von booleschen Ausdrücken, welche durch boolesche Operatoren zusammengesetzt sind. Im Folgenden soll dieses anhand der SGD 3 des booleschen UND“ ” illustriert werden. Produktion B → B1 &&B2 Semantische Regeln B1 .true = nL() B1 .false = B.false B2 .true = B.true B2 .false = B.false B.code = B1 .code k label (B1 .true) k B2 .code Syntaxgerichtete Formulierung 3: Boolsche Ausdrücke (2/2) Das Ziel der Übersetzung ist wiederum, die auf den Ausdruck folgende Anweisung zu bestimmen, welche durch die entsprechenden Textmarken benannt ist. Zu beachten ist jedoch, dass unter Umständen nicht der gesamte Ausdruck ausgewertet werden muss. Das boolesche UND“ ist nur in dem Fall erfüllt, in dem beide ” Unterausdrücke B1 und B2 erfüllt sind. Nach der Auswertung von B1 ergeben sich somit zwei Möglichkeiten. Sollte bereits B1 nicht erfüllt sein, ist gleichzeitig der Gesamtausdruck nicht erfüllt und die Textmarke kann dementsprechend gesetzt werden (B1 .false = B.false). Wird dieser Ausdruck jedoch erfüllt, muss mit der Auswertung 9 Kapitel 3: Syntaxgesteuerte Erzeugung von Zwischencode des zweiten Teilausdrucks B2 fortgefahren werden. Die erste Anweisung von B2 besitzt zu diesem Zeitpunkt noch keine explizite Textmarke, sodass eine neue mithilfe der Funktion nL()“ erstellt werden muss (B1 .true = nL()). Der Wahrheitsgehalt des ” Teilausdrucks B2 ist in diesem Fall identisch zum Wahrheitsgehalt des Gesamtausdrucks, und die Sprungziele B2 .true und B2 .false werden dementsprechend gesetzt. Der durch die letzte semantische Regel generierte 3AC spiegelt dieses Vorgehen exakt wieder. Durch die Hilfsfunktion label () wird aus dem jeweiligen Attribut eine Textmarke erzeugt. 3.2.3 Arrayzugriffe In Ausdrücken enthaltende Arrayzugriffe können ebenfalls effizient durch 3AC ausgedrückt werden [ALSU07, S. 381 ff.]. Die Elemente des Arrays werden zeilenweise in aufeinander folgenden Speicherbereichen abgelegt, d. h. A[1, 1] A[1, 2] · · · A[n, m] für ein 2-dimensionales Array. Hierdurch ist es möglich, die relative Adresse eines bestimmten Elements A[i1 ][i2 ] · · · [ik ] mithilfe der folgenden Formel zu berechnen. base + i1 · w1 + i2 · w2 + · · · + ik · wk Wie bereits im vorherigen Kapitel 3.1 beschrieben, ist die Position des ersten Elements die relative Adresse des Arrays (hier: base). Mithilfe der Positionen i1 · · · ik und den Größen der jeweiligen Dimensionen w1 · · · wk lässt sich die relative Position des Elements innerhalb des Speicherbereichs des Arrays feststellen (Der Index des ersten Elements des Arrays ist hierbei per Definition = 0). Abbildung 4 zeigt die Berechnung der relativen Position eines Arrayzugriffs A[1, 2] mithilfe der soeben vorgestellten Formel, bezogen auf ein Array der Größe 3 · 4. Zu bestimmen Array: 30 34 38 42 46 50 54 58 62 Zugriff auf 66 70 74 . Abbildung 4: Bestimmung der relativen Position im Speicher sind die jeweiligen Größen der Dimensionen w1 und w2 . Im Gegensatz zu w2 , welche der Größe eines einzelnen Elements entspricht, besteht w1 aus einem Block von 4 Elementen, welche in diesem Fall jeweils einen Integerwert enthalten, sodass 10 Kapitel 3: Syntaxgesteuerte Erzeugung von Zwischencode w1 = 4 · 4(Größe Integer) = 16. Für die Umsetzung der Berechnungen in 3AC gemäß obiger Formel muss die bereits vorgestellte SGÜ 1 angepasst werden. Um sowohl Zuweisungen an oder von Arraypositionen zu ermöglichen, lassen sich die folgenden zwei Produktionen hinzufügen, wobei das Nichtterminal L einen Arrayzugriff beschreibt. Die Arrayzugriffe werden hierbei in indizierte Kopierinstruktionen S → L = E; E → L { gen(L.array.base [“ L.addr ]“ =“ E.addr ); } ” ” ” { E.addr = new Temp(); gen(E.addr =“ L.array.base [“ L.addr ]“); } ” ” ” Syntaxgerichtete Formulierung 4: Arrayzurgriffe (1/2) übersetzt, deren Basis die Adresse des ersten Elements des Arrays ist (L.array.base). Der zweite Parameter L.addr beschreibt die relative Position des benötigten Wertes innerhalb des Speicherblocks, der durch das Array belegt ist. Die Berechnung dieser relativen Position erfolgt während der Auswertung der Produktionen für L (vgl. Syntaxgerichtete Formulierung 5). Während der Übersetzung werden die Terme ik · wk L → id[E] { L.array = top.get(id.lexeme); L.type = L.array.type.elem; L.addr = new Temp(); L → L1 [E] gen(L.addr =“ E.addr ·“ L.type.width); } ” ” { L.array = L1 .array; L.type = L1 .type.elem; t = new Temp(); L.addr = new Temp(); gen(t =“ E.addr ·“ L.type.width); ” ” gen(L.addr =“ L1 .addr +“ t); } ” ” Syntaxgerichtete Formulierung 5: Arrayzugriffe (2/2) beginnend mit i1 · w1 sukzessive ausgewertet (vgl. auch Beispiel 2). Die benötigte Größe wi der jeweiligen Dimension wird durch die Untersuchung des Typs des Elements/Teilarrays (L.type.elem bzw. L.array.type.elem) berechnet (hier: 4 bzw. 16). Nachdem zwei benachbarte Summanden gemäß der obigen Formel berechnet wurden, werden diese aufsummiert. Da im 3AC nie mehr als ein Operator pro Instruktion enthalten sein darf, werden mehrere temporäre Variablen benötigt. Als Ergebnis der Auswertung enthält die Adresse L.addr“ (vgl. t3 ) die benötigte relative Adresse des ” 11 Kapitel 3: Syntaxgesteuerte Erzeugung von Zwischencode arr[i][j] Quellsprache 1 HW-Plattform 1 Quellsprache 1 Quellsprache 2 HW-Plattform 2 Quellsprache 2 =⇒ HW-Plattform 3 Quellsprache 3 t = i · 16 HW-Plattform 1 1 t2 = j · 4 3AC w1 = 16 Quellsprache 3 HW-Plattform 2 ZC w2 = 4 Quellsprache 4 t3 = t1 + t2 HW-Plattform 3 t4 = arr[t3 ] HW-Plattform 4 Quellsprache 4 HW-Plattform 4 Beispiel 2: Erzeugung von 3AC beim Arrayzugriff gewünschten Elements und kann für den Zugriff, wie oben beschrieben, verwendet Quellwerden. Programm 3.3 3.3.1 Lexer (Lexikalische Analyse) Parser (Syntaktische Anlayse) Statische Überprüfung Zwischencodeerzeugung front end Zwischencode Codeerzeugung back end Kontrollstrukturen Übersetzung von Schleifen mittels ererbter Attribute Die in Kapitel 3.2.2 vorgestellte Übersetzung von booleschen Ausdrücken kann zur Handhabung von Kontrollstrukturen verwendet werden [ALSU07, S. 401 ff.] Eine Realisation kann durch geschickte Positionierung der beiden bereits erläuterten Textmarken B.true und B.false, wie in Abbildung 5 für die wichtigsten Schleifentypen dargestellt ist, erreicht werden. Exemplarisch soll im Folgenden die Übersetzung ... ... ... Abbildung 5: Struktur der Erzeugung von Zwischencode für Schleifen der If-Else-Schleife veranschaulicht werden. Abhängig von der Auswertung des booleschen Ausdrucks (B) soll hierbei entweder die Anweisung S1 (Sprungziel B.true) oder die Anweisung S2 (Sprungziel B.false) ausgeführt werden. Um zu verhindern, dass nach der Ausführung des Zwischencodes der Anweisung S1 mit der Ausführung des Zwischencodes für S2 fortgefahren wird, muss ein unbedingter Sprung an das Ende des Zwischencodes von S2 am Ende des Codeblocks von S1 generiert werden. Die nachfolgende syntaxgerichtete Definition 6 zeigt die zur Übersetzung benötigten 12 Kapitel 3: Syntaxgesteuerte Erzeugung von Zwischencode semantischen Regeln. Ein Programm (P ) besteht aus einer oder mehreren AnweiProduktion P → Semantische Regeln S S.next = nL() P.code = S.code k label (S.next) S → if (B) S1 else S2 B.true = nL() B.false = nL() S1 .next = S2 .next = S.next S.code = B.code k label (B.true) k S1 .code k gen ( goto“ S.next) ” k label (B.false) k S2 .code Syntaxgerichtete Formulierung 6: Schleifen sungen (S). Die auf den Zwischencode für S folgende Anweisung wird durch eine neu erzeugte Textmarke S.next gekennzeichnet. Hierfür wird diese an das Ende des Zwischencodes gehängt. Die Übersetzung der eigentlichen Schleife startet mit der Generierung der vom booleschen Ausdruck benötigten Sprungmarken B.true und B.false. Es wird in jedem Fall mit dem nächsten Befehl der übergeordneten Anweisung fortgefahren, unabhängig davon, ob die Anweisung S1 oder S2 in Abhängigkeit des Wahrheitsgehalts von B ausgeführt wird. Aus diesem Grund werden sowohl die Sprungmarke S1 .next als auch die Sprungmarke S2 .next auf S.next gesetzt. Der am Ende generierte, in S.code gespeicherte Zwischencode, spiegelt die in Abbildung 5 abgebildete Struktur exakt wieder. Der Vollständigkeit halber soll an dieser Stelle erwähnt werden, dass auch die Verkettung mehrerer Anweisungen durch eine Produktion S → S1 S2 in vergleichbarer Weise möglich ist. 3.3.2 Reduktion von goto-Instruktionen Instruktionen des 3AC werden sequenziell abgearbeitet [ALSU07, S. 405 ff.]. Diese Eigenschaft lässt sich zur Reduktion von unnötigen goto-Instruktionen verwenden, was zugleich ein Ansatz zur Minimierung des Speicherplatzes und zur Optimierung des Laufzeitverhaltens durch die Elimination unnötiger Verzweigungen darstellt. Das folgende Beispiel 3 demonstriert die Übersetzung des Programmauschnitts if(länge > 5 && breite < 3) höhe = 6 else höhe = 3“. Die linke Seite zeigt ” die erzeugten Instruktionen gemäß der in den vorherigen Kapiteln genannten Vorschriften, während die rechte Seite eine um 2 goto-Instruktionen reduzierte Variante 13 Kapitel 3: Syntaxgesteuerte Erzeugung von Zwischencode if länge > 5 goto L1 ifFalse länge > 5 goto L2 goto L3 L1 : if breite < 3 goto L2 ifFalse breite < 3 goto L2 höhe = 6 goto L3 L2 : höhe = 6 goto L1 L2 : höhe = 3 goto L4 L3 : höhe = 3 L1 : L4 : Beispiel 3: Reduktion von goto-Instruktionen zeigt. Wenn der erste relationale Ausdruck länge > 5“ erfüllt ist, muss mit den ” Instruktionen zur Auswertung des relationalen Ausdrucks breite < 3“ fortgefahren ” werden, um den Wahrheitsgehalt des Gesamtausdrucks zu bestimmen. Da dieser direkt auf den ersten Ausdruck folgt, ist ein Sprung nur dann notwendig, wenn der erste relationale Ausdruck nicht erfüllt ist. Der Befehl ifFalse“ besitzt genau die ” benötigte Eigenschaft, was zu der gewünschten Befehlsreduktion führt. Durch geschickte Wahl der Instruktion (if bzw. ifFalse) lassen sich daher goto-Instruktionen einsparen. Die Umsetzung kann mithilfe einer imaginären Sprungmarke fall erfolgen, welche keinen Sprung erzeugt (vgl. Syntaxgerichtete Formulierung 7). Produktion B → B1 &&B2 Semantische Regeln B1 .true = fall B1 .false = if B.false 6= fall then B.false else nL() B2 .true = B.true B2 .false = B.false B.code = if B.false 6= fall then B1 .code k B2 .code else B1 .code k B2 .code k label (B1 .false) Syntaxgerichtete Formulierung 7: Reduktion von goto-Instruktionen (1/2) Zur korrekten Abbildung werden einige Änderungen im Vergleich zur vorherigen Übersetzung benötigt. Da der Zwischencode für B2 , wie oben bereits erläutert, 14 Kapitel 3: Syntaxgesteuerte Erzeugung von Zwischencode auf den Zwischencode von B1 folgt, kann der natürliche Fluss ausgenutzt werden, indem B1 .true auf fall gesetzt wird. B1 .false kann jedoch nicht mehr ohne vorherige Prüfung auf B.false gesetzt werden. Im Falle, dass B.false die Sprungmarke fall enthält, würde durch einfaches Kopieren dieser Sprungmarke auch im Falle dass B1 nicht erfüllt ist fälschlicherweise mit der Auswertung von B2 fortgefahren werden, da es sich bei fall um keine absolute Sprungmarke handelt. In diesem Fall muss der 3AC für B2 durch die Generierung einer neuen Sprungmarke explizit übersprungen werden. Konsequenterweise ist auch die Generierung des 3AC abhängig von der Marke B.false die den 3AC entweder mit oder ohne die zusätzliche explizite Sprungmarke erzeugt. Auch bei der Übersetzung des relationalen Ausdrucks ergeben sich Änderungen, wie im Folgenden gezeigt wird. Die Art des generierten ZwischencoProduktion B → E1 rel E2 Semantische Regeln test = E1 .addr rel.op E2 .addr s = if B.true 6= fall and B.false 6= fall then gen( if“ test goto“ B.true) ” ” k gen( goto“ B.false) ” else if B.true 6= fall then gen( if“ test goto“ B.true) ” ” else if B.false 6= fall then gen( ifFalse“ test goto“ B.false) ” ” B.code = E1 .code k E2 .code k s Syntaxgerichtete Formulierung 8: Reduktion von goto-Instruktionen (2/2) des hängt in diesem Fall von der Beschaffenheit der beiden Attribute B.false und B.true ab. Sollte keine der beiden Sprungmarken auf fall stehen, lässt sich der natürliche Fluss nicht ausnutzen und beim produzierten Zwischencode ergeben sich keine Änderungen. Für den Fall, dass eine der beiden Sprungmarken auf fall gesetzt ist, wird genau die Instruktion if x goto L“ bzw. ifFalse x goto L“ gewählt, ” ” welche den natürlichen Fluss ausnutzt und so eine unnötige goto-Instruktion vermeidet (vgl. Beispiel 3). Letztlich bleibt anzumerken, dass bei der SGÜ der if-elseSchleife lediglich B.true auf fall gesetzt wird und die veränderte SGÜ hier nicht abgebildet ist. 15 Kapitel 3: Syntaxgesteuerte Erzeugung von Zwischencode 3.3.3 Backpatching Beide in den letzten beiden Kapiteln vorgestellten Möglichkeiten den Zwischencode von Schleifen zu erzeugen weisen ein bedeutendes Problem auf. Unter Umständen müssen die Ziele der Sprunganweisungen zu einem Zeitpunkt eingetragen werden, an dem diese noch nicht bekannt sind [ALSU07, S. 410 ff.]. Als Resultat ist es nicht möglich, den vorgestellten Zwischencode in einem Durchgang zu erstellen. Im Folgenden soll das sog. Backpatching“ am Beispiel der Übersetzung des für die Kon” trollstrukturen benötigten booleschen Ausdrucks illustriert werden, welches durch eine veränderte Behandlung von Sprungmarken die Übersetzung in einem Durchgang ermöglicht. Der hierbei erstellte Zwischencode gleicht dem in Kapitel 3.3.1 mit dem Unterschied einer veränderten Behandlung der Sprungziele. Jeder boolesche Ausdruck besitzt nun nicht mehr die vom Wahrheitsgehalt abhängigen Sprungmarken B.true und B.false, sondern synthetisierte Listen B.truelist bzw. B.falselist der Indizes von goto-Instruktionen, in denen das Ziel B.true oder B.false eingetragen werden muss. In einem ersten Schritt werden unvollständige Sprungbefehle erstellt, welche erst nachdem das endgültige Sprungziel bekannt ist, vervollständigt werden. SGÜ 9 zeigt die hierfür benötigten Änderungen. Für die Umsetzung werden drei weitere B → B1 && M B2 { backpatch(B1 .truelist, M.instr ); B.truelist = B2 .truelist; B.falselist = merge(B1 .falselist, B2 .falselist; } B → E1 rel E2 { B.truelist = makelist(nextinstr ); B.falselist = makelist(nextinstr +1); M → gen( if“ E1 .addr rel.op E2 .addr goto “); ” ” gen( goto “); } ” { M.instr = nextinstr ; } Syntaxgerichtete Formulierung 9: Backpatching Funktionen benötigt. Während makelist(x) eine neue, das Element x enthaltene Liste erstellt, konkateniert merge(l1 , l2 ) die Listen l1 und l2 . Die namensgebende Funktion ist backpatch(l1 , y), durch dessen Ausführung in allen in der Liste l1 enthaltenen bisher noch unvollständigen Sprüngen das Ziel y eingetragen wird. Die Variable nextinstr“ liefert darüber hinaus den Index der nächsten Anweisung. Bei der ” Übersetzung des relationalen Ausdrucks E1 rel E2 wird der gleiche Zwischencode generiert wie zuvor, jedoch werden unvollständige Sprünge goto “ generiert, welche ” einen Platzhalter für das nachträglich einzufügende Sprungziel bereitstellen. Um die16 Kapitel 3: Syntaxgesteuerte Erzeugung von Zwischencode se Sprünge im weiteren Verlauf ausfindig zu machen, werden deren Indizes (nextinstr für true bzw. nextinstr + 1 für false) in den jeweiligen Listen gespeichert. Bei der Auswertung der Produktion B1 &&M B2 werden wiederum die Attribute truelist und falselist gesetzt. Da der Gesamtausdruck nur erfüllt ist, wenn sowohl B1 als auch B2 erfüllt sind, muss B.truelist auf B2 .truelist gesetzt werden. Äquivalent ist der Gesamtausdruck nicht erfüllt sobald einer der beiden booleschen Ausdrücke nicht erfüllt ist, was dazu führt, dass die beiden entsprechenden Listen zu B.falselist konkateniert werden. Für das erfolgreiche Backpatching muss das Marker-Nichtterminal M eingeführt werden, dessen Aufgabe darin besteht, den Index der ersten Anweisung des Blockes B2 zu speichern. Hierdurch wird es möglich, nach der Auswertung von B2 alle in B1 .truelist enthaltenden unvollständigen Sprünge mit diesem Index zu füllen. Im Folgenden soll das Vorgehen anhand der Auswertung des schon bekannten booleschen Ausdrucks länge > 5 && breite < 3“ demonstriert werden (vgl. Beispiel 4). ” Auf der linken Seite ist der Zwischencode abgebildet, welcher nach der Auswertung 00 : if laenge > 5 goto 01 : goto 00 : if laenge > 5 goto 02 backpatch({00}, 02) =⇒ 01 : goto 02 : if breite < 3 goto 02 : if breite < 3 goto 03 : goto 03 : goto Beispiel 4: Backpatching der beiden booleschen Ausdrücke B1 und B2 erzeugt worden ist. Auffällig ist, dass vier unvollständige Sprünge generiert wurden, deren Sprungziele zum Zeitpunkt der Erzeugung noch nicht bekannt waren. Erst durch die Auswertung der semantischen Aktionen der Produktion B → B1 &&M B2 wird die Funktion backpatch(B1 .truelist, M.instr ) ausgeführt, wobei M.instr zuvor auf den Index 02 der ersten Anweisung von B2 gesetzt wurde. Die truelist enthält alle unvollständigen Sprünge, wo dieser Index eingesetzt werden muss (hier: 00). Das Resultat ist die erste vollständig generierte 3-Adress-Instruktion. Die Übersetzung der eigentlichen Schleife lässt sich auf ähnliche Art und Weise mithilfe der veränderten Produktion S → if (B) M1 S1 N else M2 S2 durch Backpatching umsetzen [ALSU07, Kap. 6.7.3]. Wiederum werden drei Markernichtterminale benötigt, wobei N den Zweck hat, den else-Zweig zu überspringen. Abschließend bleibt zu erwähnen, dass die in Kapitel 3.3.2 angewandte Technik zur Reduktion von unnötigen goto-Instruktionen zusätzlich zur Anwendung kommen kann. 17 Kapitel 3: Syntaxgesteuerte Erzeugung von Zwischencode 3.3.4 Switch-Anweisung Eine weitere, in vielen Programmiersprachen gebräuchliche Kontrollstruktur ist die Switch-Anweisung [ALSU07, S. 418 ff.]. Abhängig von der Übereinstimmung eines bestimmten Ausdrucks mit einem von n verschiedenen Werten, werden die mit diesem Wert korrespondierenden Anweisungen ausgeführt. Stimmt keiner der Werte mit dem Ausdruck überein, kann optional eine default-Anweisung ausgeführt werden. Das Ziel ist es, den Ausdruck effizient mit den n Werten zu vergleichen, was auf verschiedene Art und Weise möglich ist. Beispiel 5 zeigt den 3AC, der bei der Übersetzung einer Switch-Anweisung generiert wird. Das Ergebnis der Auswertung t1 = länge goto test L1 : höhe = 4 goto next switch (länge) { case 5: höhe = 4 case 7: höhe = 8 L2 : höhe = 8 goto next 3AC =⇒ default: höhe = 6 } L3 : höhe = 6 goto next test : if t1 = 5 goto L1 if t1 = 7 goto L2 goto L3 next : Beispiel 5: Übersetzung einer Switchanweisung des Ausdrucks wird in einer temporären Variable t1 gespeichert, um diese mit den jeweiligen Werten der Switchanweisung vergleichen zu können. Trivial übersetzt würde jeweils eine Überprüfung auf die Nicht-Übereinstimmung von t1 und dem jeweiligen Wert, gefolgt von den mit diesem korrespondierenden Instruktionen erscheinen, wobei sich diese Form der Übersetzung nicht von einer Abfolge ineinanderverschachtelter if-else-Schleifen unterscheiden würde. Dieses Vorgehen führt zu unnötig vielen goto-Instruktionen. Besser ist es, wie im obigen Beispiel gezeigt, die Überprüfung und den korrespondierenden Zwischencode zu trennen. Dadurch, dass sämtliche Überprüfungen sequenziell aufeinander folgen und nur bei einer Übereinstimmung 18 Kapitel 4: Zwischencode am Beispiel des Java Bytecode ein Sprung zum entsprechenden 3AC ausgeführt wird, lässt sich auf der einen Seite wiederum der natürliche Fluss ausnutzen, um somit goto-Instruktionen einzusparen, auf der anderen Seite ist es für den Codegenerator leichter, effizienten Code für den Block der Überprüfungen zu erstellen. Falls keine der Überprüfungen Erfolg hat, wird automatisch (wiederum durch den natürlichen Fluss) ein unbedingter Sprung zu den Anweisungen des default“-Falls ausgeführt. Die Überprüfungen könnten ” theoretisch auch an den Anfang gesetzt werden, was jedoch den Nachteil hätte, dass die benötigten Sprungmarken noch nicht bekannt wären und diese nachträglich eingefügt werden müssten. Darüber hinaus ist es möglich, die Anweisungen if t = Vi ” goto Li“ als case t Vi Li“ darzustellen. Obwohl sich inhaltlich keine Änderungen ” ergeben, ist es während der Erzeugung des Maschinencodes in diesem Fall einfacher, Switch“-Anweisungen zu erkennen und ggf. gesondert zu behandeln. ” In Spezialfällen ist eine effizientere Handhabung möglich [ALSU07, S. 419 f.]. Für den Fall, dass sämtliche Werte in einem kleinen Intervall [min, max] liegen, lassen sich die Sprungziele dynamisch ermitteln. In einer Datenstruktur wird hierfür das zu jedem Fall korrespondierende Sprungziel erfasst. Liegt der zu prüfende Ausdruck x außerhalb dieses Intervalls, kann ein unbedingter Sprung zur default“-Anweisung ” ausgeführt werden. Im anderen Fall kann das Sprungziel durch x−min in der Datenstruktur nachgeschlagen werden. Befinden sich innerhalb des Intervals Lücken, d. h. Werte für die kein explizites Sprungziel existiert, müssen diese durch einen Sprung zur default“-Anweisung ausgefüllt werden, sodass für den Einzelfall entschieden ” werden muss, ob die Einzelprüfung oder das Auffüllen des Intervalls das effizientere Vorgehen darstellt (vgl. auch Kaptiel 4). 4 Zwischencode am Beispiel des Java Bytecode Nachdem in den vorherigen Kapiteln die Erzeugung von 3AC anhand einiger wichtiger Strukturen einer Programmiersprache vorgestellt wurde, soll in diesem Kapitel die Erzeugung von Zwischencode am Beispiel der Programmiersprache Java erläutert und eingeordnet werden. Für eine abstraktere Behandlung sei der interessierte Leser auf das im Rahmen dieses Seminars behandelte Thema Laufzeit” umgebungen am Beispiel der Java Virtual Machine“ verwiesen. Vor der eigentlichen Ausführung des Java Quellprogramms, wird dieses im Normalfall in einen Zwischencode umgewandelt (vgl. auch Abbildung 6). Dieser sog. Bytecode hat die Form <index>: <opcode> [<operand1> [<operand2> · · · ]]“ [LY99, Kap. 7.1], wobei ” jeder opcode“ durch genau 1 Byte repräsentiert wird. Hierdurch ist es möglich, ” theoretisch bis zu 256 verschiedene Instruktionen bereitzustellen, welche auf einer 19 Kapitel 4: Zwischencode am Beispiel des Java Bytecode Compile-Zeit Quellcode Laufzeitumgebung Compiler Bytecodes (Java Runtime Enviroment) Abbildung 6: Erzeugung von Zwischencode am Bsp. des Java Bytecode beliebigen Anzahl an Operanden (operand1, operand2, · · · ) agieren. Der Bytecode ist stackorientiert, sodass sämtliche Operanden auf dem Stack erwartet und eventuelle Ergebnisse auf dem selbigen abgelegt werden [LY99, Kap. 7.2]. Der erstellte Code wird in einem speziellen Format in .class“-Dateien abgespeichert. Erst jetzt ” erfolgt eine Interpretation mithilfe einer Java Virtual Machine (JVM). Bedeutend ist es zu erwähnen, dass der Zwischencode vor der Ausführung verifiziert wird, was die Interpretation durch die JVM beschleunigt, da potentiell schadhafte Operationen (wie illegale Typkonvertierungen etc. ) ausgeschlossen sind [Go95, S. 114]. Durch die JVM wird der Bytecode plattformunabhängig, da er überall dort ausgeführt werden kann, wo diese implementiert ist [LY99, Kap. 1.2]. Laut [ALSU07, S. 2 f.] handelt es sich bei dieser Kombination aus Compiler und Interpreter um einen hybriden Compiler. Der Vollständigkeit halber sei erwähnt, dass neben der Erzeugung des Zwischencodes und der späteren Ausführung durch die JVM, zusätzlich echte“ Compiler existieren, welche entweder den Quellcode oder den Bytecode in ” direkt ausführbaren Maschinencode übersetzen, was dem oben beschriebenen Vorgehen entspricht [vgl. z. B. GNU Compiler]. Einen Mittelweg beschreiten die sog. Just-in-Time“-Compiler, welche den Zwischencode erst zur Ausführung bei Be” darf in Maschinencode übersetzen und somit die Flexibilität eines Interpreters mit der Geschwindigkeit eines Compilers verbinden [LY99, Kap. 3.13]. Ziel dieses Kapitels soll es jedoch nicht sein, die weitere Verwendung des Bytecodes detailliert zu erläutern, sondern die Struktur dieses Zwischencodes an einem Beispiel zu skizzieren und anhand des bisher Beschriebenen einzuordnen. Hierbei wird jedoch kein Anspruch auf eine vollständige Abbildung des Bytecodes gelegt. Beispiel 6 zeigt die Bytecodeübersetzung einer Java-Methode in Anlehnung an das bereits mehrfach verwendete Codefragment. Um die kryptischen Befehle der class“-Dateien lesbarer zu machen, ist jedem ein mnemonic zugeordnet (erzeugt ” durch javap -c“) [LY99, Kap. 9]. Um den booleschen Ausdruck länge > 5“ aus” ” werten zu können, müssen zunächst dessen Operanden auf den Stack gelegt werden [LY99, Kap. 7.2]. Per Konvention werden die Parameter der Methode im statischen Fall in den lokalen Variablen 0 − n abgelegt, sodass der Befehl i load 0“ den Wert ” des Parameters länge“ auf den Stack legt (im nicht statischen Fall enthält die Va” 20 Kapitel 4: Zwischencode am Beispiel des Java Bytecode public static void bestimmeHöhe(int länge, int breite){ int höhe; if (länge > 5 && breite < 3) höhe = 6; else höhe = 3; return höhe;} 10 : bipush 0 : iload 0 12 : istore 2 1 : iconst 5 Java Bytecode =⇒ 2 : if icmple 13 : goto 16 18 16 : iconst 3 5 : iload 1 17 : istore 2 6 : iconst 3 7 : if icmpge 6 18 : iload 2 16 19 : ireturn Beispiel 6: Übersetzung in Java Bytecode riable 0 eine Referenz zur jeweiligen Instanz [LY99, Kap. 7.6]). Hervorzuheben ist, dass der Java Bytecode verschiedene Befehle für dieselbe Aktion, jedoch ausgeführt auf unterschiedlichen Datentypen (Integer, Double, etc.) bereitstellt. Im vorliegenden Fall wird durch i load x“ ein Integerwert geladen. Es existieren jedoch nicht für ” jeden Datentyp gleich viele auf diesen abgestimmte Befehle, sodass ein Ergebnis in vielen Fällen nur durch Typkonvertierung oder die Komposition mehrerer einfacher Befehle erreicht werden kann. Darüber hinaus sind für häufig durchgeführte Aktionen eigene Befehle vorhanden, um deren Ausführung zu beschleunigen. So kann auf der einen Seite die Konstante 5 durch einen eigenen Befehl iconst 5“ auf den Stack ” gelegt werden, auf der anderen Seite benötigt man für Konstanten größer als 5 (vgl. Instruktion 10) bzw. kleiner als −1 zusätzlich einen Parameter, welcher den Wert der Konstante enthält und das Programm hierdurch größer werden lässt. Nachdem beide Operanden auf den Stack gelegt wurden, lässt sich ein bedingter Sprung mithilfe des Befehls if icmple 16“ ausführen, der, falls der Vergleich erfüllt wird, zur ” entsprechenden Anweisung 16 springt. Auffällig ist, dass, wie bereits bei der Erstellung des 3AC versucht wird, möglichst viele goto-Instruktionen einzusparen (vgl. auch 3.3.2). Da jedoch ein expliziter ifFalse“-Befehl fehlt, wird dieses durch die ” Negation des relationalen Operators erreicht (z. B. > zu ≤). Ist der relationale Ausdruck nicht erfüllt, d. h. der ursprüngliche relationale Ausdruck ist erfüllt, wird der natürliche Fluss der Anweisungen ausgenutzt, um mit der Überprüfung des zweiten 21 Kapitel 5: Zusammenfassung und Ausblick Teils des booleschen Ausdrucks wie gewohnt fortzufahren. Falls beide Ausdrücke und somit der Gesamtausdruck erfüllt sind, wird der Wert der lokalen Variable höhe“ ” entsprechend gesetzt. Hierfür wird wiederum die Konstante zunächst auf den Stack gelegt (vgl. aber in diesem Fall bipush 6“ mit iconst 5“) und diese dann in der ” ” lokalen Variable 2, welche durch die oben aufgeführte Deklaration definiert ist, abgespeichert. Zuletzt erfolgt ein unbedingter Sprung an das Ende des Bytecodes, um die Ausführung der folgenden Instruktion zu verhindern. Die Zuweisung des Wertes an die Variable höhe“ im Falle, dass der Gesamtvergleich nicht erfüllt ist, wird ” auf ähnliche Weise implementiert. Zuletzt wird das Ergebnis der Ermittlung der entsprechenden höhe“ auf den Stack gelegt und durch die Anweisung ireturn“ ” ” zurückgegeben. Insgesamt lässt sich festhalten, dass 3AC und Bytecode ähnliche Elemente enthalten. Beim Bytecode handelt es sich im wesentlichen um eine komprimierte Form des 3AC, bei der Operanden implizit auf dem Stack erwartet werden [Go95, S. 117]. Viele der oben beschriebenen Techniken, z. B. zur Ausnutzung des natürlichen Flusses, kommen offensichtlich auch bei der Erstellung des Bytecodes zum Einsatz. Jedoch geht der Bytecode stark über den Umfang des 3AC hinaus. So übersteigt die Anzahl der beim Bytecode zur Verfügung stehenden Befehle die des 3AC um ein Vielfaches, sodass der Zugriff auf Arrays und Switches durch eigene Befehle unterstützt wird und somit effizienter durchzuführen ist [LY99, Kap. 7.9 bzw. 7.10]. Im Falle der Switchanweisungen werden hierbei beide in Kapitel 3.3.4 beschriebenen Vorgehen durch eigene Befehle lookupswitch“ bzw. tableswitch“ ermöglicht, so ” ” dass je nach der Beschaffenheit der zu prüfenden Fälle die effizientere Möglichkeit gewählt werden kann. Im Gegensatz zum 3AC bietet der Bytecode darüber hinaus Lösungen, um die objektorientierten Features von Java abbilden zu können. Abschließend bleibt festzustellen, dass die oben beschriebene Funktion des Zwischencodes zwecks Reduzierung der Anzahl benötigter Compiler durch die vorgestellte Vorgehensweise des Bytecode gut umgesetzt wurde. 5 Zusammenfassung und Ausblick Zwischencode ist eine wichtige Phase im Laufe der Compilierung. In der vorliegenden Ausarbeitung wurde diese Phase zunächst in den Compile-Prozess eingeordnet, um daraufhin verschiedene Arten von Zwischencode vorzustellen. Als Basis für das weitere Vorgehen wurde im nächsten Kapitel mit dem 3AC ein Zwischencode vorgestellt, welcher als eine linearisierte Form eines Syntaxbaumes verstanden werden kann. 22 Kapitel 5: Zusammenfassung und Ausblick Im weiteren Verlauf der Ausarbeitung wurde die Übersetzung/Handhabung verschiedener Konstrukte einer Programmiersprache mithilfe syntaxgerichteter Formulierungen aufgezeigt. Hervorzuheben sind hier die Handhabung von Deklarationen, welche in Einträgen von Symboltabellen resultierten, in denen deren Bezeichner, Typ und relative Adresse für die weitere Verwendung abgespeichert wurde. Im Mittelpunkt des Kapitels stand jedoch die Übersetzung von Kontrollstrukturen, welche auf boolesche Ausdrücke zurückgriff. Nach der Übersetzung durch ererbte Attribute wurde diese durch zwei Erweiterungen modifiziert. Hierdurch konnte auf der einen Seite die Anzahl der benötigten goto-Instruktionen reduziert werden und zum anderen wurde durch eine veränderte Behandlung von Sprüngen die Übersetzung in einem Durchgang ermöglicht. Nicht behandelt wurde die Übersetzung von Prozeduraufrufen, da hierbei große Überschneidungen mit dem Thema Laufzeitumgebungen“ ” nicht zu vermeiden gewesen wären. In einem letzten Abschnitt wurde Zwischencode am Beispiel des Java Bytecode skizziert und eingeordnet. Hierbei wurde gezeigt, dass der Bytecode auf der einen Seite Analogien zum 3AC aufweist, auf der anderen Seite jedoch weit über dessen Möglichkeiten hinausgeht. Die effiziente Erzeugung und Ausführung von Zwischencode lässt sich in der Realität oft nur durch eine Abweichung von der Struktur der Compilierung aus Kapitel 2.1 erreichen. Da ein allumfassender Zwischencode auf Grund der Unterschiede einzelner Quellsprachen nicht zu realisieren ist, bieten hybride Lösungen (vgl. Java Bytecode) einen guten Trade-off zwischen der Berücksichtigung spezieller Strukturen bzw. Eigenschaften einer Quellsprache und der Reduktion der Beziehungen, welche zur Übersetzung benötigt werden. Durch die ständige Weiterentwicklung der Quellsprachen durch neue Features, entsteht auch für die Zwischensprachen erheblicher Veränderungsbedarf. Die direkte Abbildung neuer Strukturen bzw. Ansätze (z. B. Modularisierungsmechanismen [RHBD08, S. 865-867]) durch den jeweiligen Zwischencode besitzt Vorteile ggü. der Abbildung durch bereits bestehende Strukturen. Ein anderer Punkt ist die stärkere Verbreitung und Verwendung von Mehrkernprozessoren [HPP09, S. 60], wodurch die Parallelität erhöht wird. Hierbei sind die Abstraktion der zugrunde liegenden Prozessorkerne von der Quellsprache, bzw. die direkte Abbildung der Parallelität im Zwischencode zwei mögliche Ansätze für Erweiterungen. 23 Literaturverzeichnis Literatur [ALSU07] A. V. Aho, M. S. Lam, R. Sethi, J. D. Ullman: Compilers: principles, techniques, and tools, 2nd. ed., Pearson Studium, 2007. [Ap02] A. W. Appel: Modern compiler implementation in Java, 2nd. ed., Cambridge University Press, 2002. [Go95] J. Gosling: Java Intermediate Bytecode, ACM SIGPLAN Workshop on Intermediate Representations, S.111 - 118, 1995. [Gu99] R. H. Güttig: Übersetzerbau: Techniken, Werkzeuge, Anwendungen, Springer, 1999. [HPP09] M. Hall, D. Padua, K. Pingali: Compiler research: The next 50 years, Communication of the ACM 52(2), S.60-67, 2009. [LY99] T. Lindholm, F. Yellin: The JavaTM Virtual Machine Specification, 2nd. ed., Addison-Wesley, 1999. [RHBD08] H. Rajan, M. Haupt, C. Bockisch, R. Dyer: Virtual machines and intermediate languages for emerging modularization mechanisms, OOPSLA Companion, S.865-868, 2008. [SOWMTS58] J. Strong, J. Olsztyn, J. Wegstein, O. Mock, A. Tritter, T. Steel: The problem of programming communication with changing machines - A proposed solution, Communication of the ACM 1(8), S.12-18, 1958. 24