Folienkopien zur Vorlesung 1 Einleitung 1.1 Ein Blick auf den Übersetzungsprozeß 1.2 Aufgaben eines Compilers Übersetzung objektorientierter Sprachen 1.3 Aufgaben eines Compilerbauers 1.4 Struktur eines Compilers 1.5 T-Diagramme 1.6 Grobe Historie des Compilerbaus Martin Lehmann Wintersemester 2007/2008 1.7 Zusammenfassung Beispiel einer Übersetzung: Zielarchitektur: Einfache Registermaschine Aufgabe: Berechnung des größten gemeinsamen Teilers zweier natürlicher Zahlen. Seien a, b ∈ N : Register R0 R1 R2 M Hauptspeicher 0 1 2 .. . .. . falls a = b ⎧ a, ⎪ ggT(a, b ) = ⎨ ggT(a − b, b ), falls a > b ⎪ ggT(a, b − a ), falls a < b ⎩ Formulierung in einer algorithmischen Sprache: a b .. . ggT (a, b: natural) = begin while a ≠ b do if a > b then a := a – b else b := b – a; return a end; Programm .. . Schritte bei der Übersetzung der Schleife: 1. 2. Bildung des Strukturbaums: Bildung der Lexeme hier: nur für den Schleifenkörper Symbol Klasse while a ≠ b do if a > b then a := a – b else b := b – a ; Schlüsselwort Bezeichner Vergleichsoperator Bezeichner Schlüsselwort Schlüsselwort Bezeichner Vergleichsoperator Bezeichner Schlüsselwort Bezeichner Zuweisungsoperator Bezeichner Additionsoperator Bezeichner Schlüsselwort Bezeichner Zuweisungsoperator Bezeichner Additionsoperator Bezeichner Trennsymbol Bemerkung: Formatierungszeichen wurden entfernt. Grammatikfragment für Quellsprache: .. . Anweisung ::= "while" Ausdruck "do" Anweisung | "if" Ausdruck "then" Anweisung | "if" Ausdruck "then" Anweisung "else" Anweisung | Zuweisung | ... Ausdruck ::= Ausdruck Operator Ausdruck | Variable | Konstante | ... Operator ::= | | | Additionsoperator Multiplikationsoperator Vergleichsoperator ... Vergleichsoperator .. . ::= "=" | "≠" | "<" | "≤" | ">" | "≥" Ausschnitt aus Strukturbaum: 3. Transformation des Strukturbaums, Attributierung: while : Schleife, Ende Anweisung Vergleich : ≠ while do Ausdruck Anweisung ... Ausdruck Operator if : Else, Ifende Variable : a Variable : b Ausdruck Vergleich : > Zuweisung Zuweisung .. . Variable a Vergleichsoperator Variable ≠ b Variable : a Variable : b Variable : a Variable : a Summe : – Variable : b 4. Einfache schablonengesteuerte Codeerzeugung: Erzeugter Code: Codeerzeugung mittels Schablonensatz: Schleife: Vergleich: ≠ A load sub jnpos R1, a R1, b Else load sub store jump R1, a R1, b R1, a Ifende Else: load sub store R1, b R1, a R1, b Ifende: jump Schleife R?, A R?, B M? Load Sub jnpos R?, A R?, B M? B M Summe: + A R1, a R1, b Ende B Vergleich: < A Load Sub jzero load sub jzero Load Add R?, A R?, B ; jump on not positive Ende: B Bemerkung: Es wird angenommen, daß das analysierte Programm korrekt ist, daher keine Berücksichtigung möglicher Fehlrechnungen. Bemerkung: In der Regel werden Codeschablonen umgebungsunabhängig eingesetzt. Der erzeugte Code bedarf daher der nachträglichen Glättung. 5a. Codeoptimierung, hier: Gucklochoptimierung: 5b. Codeoptimierung, hier: Flußgraphbetrachtung: Ersetzen von Befehlen und Befehlsfolgen durch vermutlich aufwandsärmere: Schleife: Else: Ifende: Ende: load sub jzero R1, a R1, b Ende load sub jnpos R1, a R1, b Else ; Berechnung schon ; erfolgt load sub store jump R1, a R1, b R1, a Ifende ; Berechnung schon ; erfolgt jump Schleife load sub R1, b R1, a store R1, b jump Schleife load sub store R1, a R1, b R1, a ; Vermeidung von ; Sprungkaskaden ; ; ; ; ; Ersetzen dieser zwei Befehle durch neg R1 liegt außerhalb der Gucklochoptimierung load sub store ; Marke wird nicht ; mehr benötigt. Problem: Lohnt der Aufwand? R2, b R2, a R2, b Zielcode für die Schleife auf einer Mehrregistermaschine: Schleife: load sub jzero R1, a R1, b Ende jnpos Else Aufgaben eines Compilers: Fehlerangaben Quelltext Else: store jump R1, a Schleife load sub store R2, b R2, a R2, b jump Schleife Ende: (ii) Verletzungen von Implementationsgrenzen Zielcode Eigenschaften des Zielcodes: Bemerkungen: (i) Compiler Die algorithmische Formulierung zur Berechnung des größten gemeinsamen Teilers ist nicht vorbildlich. Der erzeugte Code ist nicht optimal. 1. Bedeutungstreue Realisierung des Quelltextes. 2. Ökonomische Nutzung der Ressourcen des Zielrechners. Eigenschaft der Beanstandungen: Genaue Angabe, in welchen Teilen der Quelltext die Sprachdefinition oder die Implementationsbeschränkungen verletzt. Aufgaben eines Compilerbauers: Aufgabenunterteilung: Compiler ⇓ 1. Erstellung von Compilern. Analyse 2. Verbesserung von Compilern. 3. Verbesserung der Erstellung von Compilern. 4. Beschreibung von Compilern. 5. Entwurf von Programmiersprachen. 6. Entwurf von Rechnern. Synthese ⇓ Analyse der Form Analyse des Inhalts Synthese ⇓ Lexikalische Aufbereitung Syntaxanalyse Semantikanalyse Codeerzeugung ⇓ Bemerkung: Gegeben seien eine Beschreibung der Quellsprache S, eine Beschreibung der Zielsprache T und Randbedingungen; wieweit darf die in einem Compiler implementierte Sprache S' von der Originalsprache S abweichen? Aufbereitung der Eingabe Lexikalische Analyse Syntaxanalyse Analyse der statischen Semantik Speicherplatz-Zuweisung Erzeugung eines Universalcodes Verbesserung des Universalcodes Erzeugung zielnahen Codes Verbesserung des zielnahen Codes Aufbereitung der Ausgabe Abstraktes Compilermodell: Zweiteilung eines Compilers: Quelltext Lexikalische Analyse Code für abstrakte Maschine Quelltext Zwischentext-1 Zielcode Syntaktische Analyse Zwischentext-2 Semantische Analyse sprachabhängiger Teil maschinenabhängiger Teil Zwischentext-3 Optimierung Analyse: Hauptaufgabe Analyse: trivial Zwischentext-4 Codeerzeugung Zieltext Codeerzeugung: trivial Codeerzeugung: Beschreibung mittels eines kleinen Schablonensatzes Das n Sprachen, m Maschinen Problem, auch UNCOL – Problem: Front-ends Back-ends Sprache 1 Maschine 1 Sprache 2 Sprache 3 .. . Sprache n Beispiele zur Struktur von Compilern: Maschine 2 Darstellung des semantischen Inhalts .. . Maschine m–1 Maschine m Bemerkung: Bisher ist jeder Versuch einer allgemeinen Lösung gescheitert. (i) Phasen des Gier-Algol-Compilers: 1 Übertragung in Hardware-Repräsentation 2 Ersetzung der Bezeichner durch Zahlen 3 Syntaxanalyse 4 Aufbau der Symboltabelle 5 Speicherzuweisung für Variable 6 Typprüfung und Überführung in Polnische Notation 7 Codeerzeugung 8 Endgültige Adreßfestlegung 9 Umordnung der Programmteile auf Trommel (ii) Aufbau des Fortran-Übersetzers von Backus und Mitarbeitern: (iii) Gliederung des York Ada-Compilers Zeilen Quellcode (C-Code) Phasengröße Phasenaufgabe 5500 Instr. Klassifizierung der Anweisungen, Übersetzung arithmetischer Formeln, Teilausgabe nach Datei 1. 6000 Instr. Übersetzung von Indizes innerhalb von Schleifen, Ausgabe nach Datei 2. 2500 Instr. Zusammenführen von Datei 1 und Datei 2. 3000 Instr. Flußanalyse. 5000 Instr. Abbildung des übersetzten Textes auf Maschine mit 3 Indexregistern (IBM 704). 2000 Instr. Endbehandlung. Lexikalische und syntaktische Analyse davon lexikalische Analyse Analyse der statischen Semantik davon Deklarationsbearbeitung Ausdrucksbearbeitung Codeerzeugung davon maschinenunabhängig maschinenabhängig Laufzeitunterstützung davon allgemein Bibliothekscode 5.221 461 40.178 10.835 14.831 31.248 24.319 5.376 17.813 3.514 14.399 Darstellung von Compilern als T-Diagramme: Die Erstellung eines Compilers für die Sprache S läßt sich als iterativer Vorgang betrachten. Name Zielsprache Quellsprache 1. Definiere Folge von Teilsprachen. S 0 < S1 < S Implementationssprache 2. Schreibe Übersetzer. S 3 S1 Einsatz: Quellprogramm S Z S1 2 S0 N Z Q I Zielprogramm Z S0 4 Z Z 1 Z Z Um den gewünschten Übersetzer 4 zu erhalten, sind 1, 2 und 3 zu schreiben. Situation des eigenständigen "bootstrap": Drei Perioden des Compilerbaus: Periode 1: 1945 – 1960 Gegeben: Hauptaugenmerk: Codeerzeugung. 1 Q Q ZC Q 2 ZC 3 ZC ZC Q Interpreter für Zwischencode Es galt für einfache syntaktische und semantische Konstrukte hervorragenden Zielcode zu erzeugen. Es galt zu demonstrieren, daß übersetzte Hochsprachenprogramme bezüglich Speicherplatz und Laufzeit den Assemblerprogrammen ebenbürtig sind. Das leuchtende Beispiel für exzellenten Code ist der frühe Fortran-Compiler (1954). Periode 2: 1960 – 1975 Vorgehen: Hauptaugenmerk: Syntaxanalyse. Q Q 1a Q Manuelle Anpassung M Q 1a Q 2 comp Q M cpz Q ZC M ZC ZC 3a ZC 3a ZC M M M M gewünschter Compiler Diese Zeit war geprägt von dem Wissen, daß die Leistungsfähigkeit von Rechnern exponentiell zunimmt. Daher galt es, programmierfreundliche mächtige Notationen automatisch zu analysieren. Die Güte des erzeugten Codes wurde als zweitrangig betrachtet. Wichtig war die Produktivität des Programmierers. Periode 3: 1975 – heute Hauptaugenmerk: Codeoptimierung. Die Zahl der strukturunterschiedlichen Rechner reduziert sich erheblich. Die Investitionen in Codeverbesserungen sind daher dauerhaft. Beispiel zur Codeoptimierung: Übersetzung als allgemeine Texttransformation: Umbenennung der Register: ursprünglich: mov mul mov mov add mov [mem01], R2 R2, "6", R2 R2, [mem02] [mem03], R2 R2, "7", R2 R2, [mem04] Text Q in Quellsprache Text Z in Zielsprache Compiler Beispiele: Pascal Intel 8086-Code C Assemblercode Fortran Fortran Pascal C Postscript Druckseite HTML Webseite geändert: mov mul mov mov add mov {mov [mem01], T1 T1, "6", T1 T1, [mem02] [mem03], T2 T2, "7", T2 T2, [mem04] T2, R2} // eventuell ? Bemerkung: Diese Art der Codeverbesserung wird heutzutage von der Hardware ausgeführt, Ti sind verborgene Hardware-Register. Übersetzerarten: Assembler Zur Interpretation: Quellcode Makro-Assembler Compiler Präprozessor Disassembler Decompiler Zwischencode Interpretation Objektcode Ausführung Interpreter Bemerkung: Ein Decompiler wird eingesetzt, um aus Objektcode einen Hochsprachentext zu gewinnen, der den compilierten Algorithmus beschreibt. Dies kann sinnvoll sein, um verloren gegangene Quelltexte zu rekonstruieren oder um Malware zu analysieren. Der Einsatz eines Decompilers ist eine Form des Reverse Engineering. Automatisierung der Compilererstellung: Beschreibung der Quellsprache Q Beschreibung der Zielsprache Z Beschreibung der Implementationssprache I C O M P I L E R – G E N E R A T O R Q → I Bemerkung: Für die einzelnen Teile eines Compilers kennt man zugkräftige Beschreibungsmittel. Z