Zwischencode-Erzeugung Sebastian Hanneken

Werbung
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
Herunterladen