Compilerbau

Software

Neu! Verzeichnis Software/ enthält die neuesten Versionen von muHwi, risc386, und runtime.c, sowie Beispielprogramme.

  1. Zur Einführung: das Tool dataGen erleichtert die schematisch Erzeugung von Klassen, die abstrakte Syntaxbäume u.ä. representieren
  2. Lexikalische Analyse und Parsing von MiniJava Programmen.

    Zur lexikalischen Analyse verwenden wir das Tool jflex. Mit diesem Aufruf wird aus dem Lexer file ein Java file erstellt: jflex MJLex.x (genauer /soft/bin/jflex).

    Zum Parsing verwenden wir den Parser Generator java-cup der in /soft/IFI/lang/java-cup/java-cup-v11a.jar installiert ist (oder als java-CUP .jar file heruntergeladen werden kann).

    Mehrere MiniJava Beispielsprogramme koennen heruntergeladen werden. (Siehe auch Buch Homepage.)

    Für die Ausführung des lexers, ist der CLASSPATH wie folgt zu ändern: export CLASSPATH=$CLASSPATH:/soft/IFI/lang/java-cup/java-cup-v11a.jar

    Material zum Download:

  3. Zwischensprache: Der Code für die Kanonisierung des Zwischencodes, wie in PK 7 vorgestellt, ist hier verfügbar, zusammen mit den Hilfspaketen List.java und Utils.java. Zum Einbinden in den eigenen Code, sollte für jeden Methodenrumpf this.body folgendes Codestück ausgeführt werden:
        LinkedList blks = IntmTrafo.intmTrafo(this.body); 
        this.canon_body = blks;
         

    Der muHwI Interpreter: Um das Testen des MiniJava Compilers zu vereinfachen, ist hier ein Interpreter für die Zwischensprache verfügbar. Die aktuelle Version ist 1.10.

    muHwI wird "mux-wi'" ausgesprochen (Details zur Aussprache von Klingonischen Wörtern).

    Struktur des Eingabefiles: Das Eingabefile besteht aus einem Deklarationsblock und einer Folge von Funktionsdefinitionen, die den Rümpfen der Methoden in MiniJava entsprechen. Im Deklarationsblock werden die Namen von Framepointer usw. sowie Stack- und Heapgrösse definiert, zB:

    SET NAME fp = t33  // Framepointer
    SET NAME sp = t30  // Stackpointer
    SET NAME rp = t3   // Return-value register
    SET VALUE sz = 12000   // 12kB of stack
    SET VALUE hz = 800000  // 800kB of heap
    
    Im Kopf der Funktionsdefinition muss der Name (mit beginnendem "L"!) und die Parameterliste angegeben sein. Folgende Deklarationen für die Parameter sind möglich:
    • REG ein (abstraktes) Register,
    • LOC eine (relative) Adresses auf dem Frame, und
    • MEM eine (absolute) Adresse im Heap.
    Das erste Argument der Parameterliste, muss ein Register mit dem THIS Objekt sein. Optional kann danach die Frame-grösse mit FS angegeben werden. Als Beispiel, hier die Struktur der Factorial Funktion im Zwischencode:
    FCT LFactorial$main(REG t1, REG t77) FS 4 =
    {
    ...
    }
         
    Links zu weiteren Beispielen finden sich am Ende der Web page.

    Zum Ausdrucken des Codes kann der pretty-printing Visitor in PPIntm verwendet werden (neue Version im package IntmTrafo. Diese Funktion muss für jeden Methodenrumpf in der Symboltabelle aufgerufen werden. Der Kopf der Funktionsdefinition muss separat aus der weiteren Information in der Symboltabelle erzeugt werden.

    Wichtige Konventionen:

    • Registernamen müssen mit t beginnen, zB: t33
    • Funktionsnamen und labels müssen mit L beginnen, zB: Lfact
    • Das Resultatregister muss trp sein (oder mit -r name setzen), der Framepointer muss tfp sein (oder mit -f name setzen), der Stackpointer muss tsp sein (oder mit -s name setzen). Die Namen können auch im Deklarationsteil des Files bestimmt werden (siehe obiges Besipiel). .
    • Als "calling convention" werden vor dem Aufruf einer Funktion sämtliche Register abgespeichert. D.h. es ist (noch) nicht nötig die Funktion procEntryExit1 aus der Vorlesung zu implementieren. In der Endversion der Zwischensprachengenerierung müssen sämtliche callee-save Register durch den erzeugten Code gesichert werden. Dann ist eine einfachere "calling convention" im Interpreter zu verwenden.

    Weitere nützliche Informationen:

    • Laufzeitwerte werden im Interpreter als A int für Adressen und I int für integers repräsentiert.
    • Der Stack startet üblicherweise bei Adresse 8000 und wächst nach unten (zu kleineren Adressen), der Heap startet bei Adresse 8004 und wächst nach oben.
    • Mit flag -C werden die aktuellen Werte für jeden Methodenaufruf ausgedruckt.
    • Mit flag -? erhält man weitere Hinweise zur Verwendung des Interpreters.

    Der Interpreter (Name: muHwI) wird mit dem Filenamen des Zwischencodes und den Parametern der L...$main Funktion aufgerufen:

         muHwI Fact3.intm 5
         
    Während der Ausführung druckt der Interpreter Meldungen über die Ausführung bestimmter Kommandos aus (z.B. Funktionsaufrufe mit Parameterwerten). Am Ende wird das Resultat ausgegeben.

    Name für spezielle externe Funktionen, die der Interpreter kennt:

    • L_halloc_obj: Allokierung eines heap objects; Parameter: Grösse in Bytes
    • L_init_obj: Initialisierung eines heap objects; Parameter: Pointer zum Objekt, Grösse in Bytes
    • L_halloc_arr: Allokierung eines Arrays; Parameter: Länge (Anzahl der Einträge)
    • L_init_arr: Initialisierung eines Arrays; Parameter: Pointer zum Objekt, Länge (Anzahl der Einträge) (Indizierung startet mit 0; Grösse ist im Index -1 abgelegt)
    • L_print: Ausdrucken eines Wertes; Parameter: Wert (Integer oder Adresse)
    • L_raise: Laufzeitfehler; Parameter: Error-Code (Integer)

    Hier die wichtigsten Optionen:

      -?         --help             show usage information
      -v         --verbose          be verbose
      -a         --ast              show AST
      -T Traces  --defTrace=Traces  default tracing flags (NOT IMPLEMENTED)
      -C         --callTrace        trace function calls
      -J         --jumpTrace        trace jumps
      -M         --moveTrace        trace moves
      -A         --allocTrace       trace memory allocations
      -P         --paramTrace       trace parameter passing
      -R         --regMap           print register maps
      -Q         --memMap           print memory maps
      -L         --labMap           print label maps
      -D         --debugging        debug interpreter itself
      -f fp      --framepointer=fp  name of frame pointer register
      -s sp      --stackpointer=sp  name of stack pointer register
      -h hp      --heappointer=hp   name of heap pointer register
      -r rp      --returnvalue=rp   name of this pointer register
      -S size    --stacksize=size   stack size
      -H size    --heapsize=size    heap size
      -W         --warnings         print warnings
      -z         --stdcallconv      standard calling convention (default: all regs are callee saved)
      -o FILE    --output=FILE      output file (NOT IMPLEMENTED)
    $Revision: 1.7 $)
    

    Weitere Beispiele für Zwischencode (so wie er von muHwI eingelesen wird):

    MiniJava Programme: ArrSumBUG.java, ArrSum.java, Newton.java, NewtonOO.java

  4. Instruktionsauswahl: Um sich mit der Zielarchitektur vertraut zu machen, empfehlen wir in folgenden Dokumenten nachzulesen:

    Intel x386:

    MIPS R2000/R3000:



    Experimenteller i386 Simulator (Haskell sources (ghc-6.4 und höher) und Linux-binary) risc386.

    Besonderes Feature: Erlaubt eine unbegrenzte Zahl von Hilfsregistern t0,t1,... Diese werden bei Funktionsaufruf automatisch gesichert und bei Rückkehr rückgesichert. Man kann also seinen Code vor Registerzuweisung testen.
    Eingabeformat:

    • Liest .s-Dateien in Intel syntax. Beispiel Factorial.raw.s:
              .intel_syntax
              .type LFactorial$main, @function
              #args
      LFactorial$main:
              push    %ebp
              mov     %ebp, %esp
      L3:     push    0
              call    L_halloc_obj
              add     %esp, 4
              mov     t1001, %eax
              push    0
              push    t1001
              call    L_init_obj
              add     %esp, 8
              push    10
              push    t1001
              call    LFac$ComputeFac
              add     %esp, 8
              mov     t1002, %eax
              push    t1002
              call    L_print
              add     %esp, 4
      L4:     leave
              ret
              .type LFac$ComputeFac, @function
              #args LOC 0, LOC 4
      LFac$ComputeFac:
              push    %ebp
              mov     %ebp, %esp
      L5:     cmp     DWORD PTR [%ebp+12], 1
              jl      L0
              jmp     L1
      L2:     mov     %eax, t8
              jmp     L6
      L1:     mov     t1004, DWORD PTR [%ebp+12]
              mov     t1005, -1
              add     t1005, DWORD PTR [%ebp+12]
              push    t1005
              push    DWORD PTR [%ebp+8]
              call    LFac$ComputeFac
              add     %esp, 8
              mov     t1003, %eax
              mov     %eax, t1004
              imul    t1003
              mov     t8, %eax
              jmp     L2
      L0:     mov     t8, 1
              jmp     L2
      L6:     leave
              ret
      
    • as-Direktiven, beginnend mit "." (Punkt), werden ignoriert.
    • Zeilen, die mit "# " (Doppelkreuz Leer) beginnen (Kommentare), werden ebenfalls ignoriert.
    • Das erste Label startet die erste Prozedur, die mit ret endet.
    • Will man dem Simulator die Argumente der Prozedur mitteilen (für schönere Traces), tut man das mit dem Pragma #args, gefolgt von einer Komma-separierten Liste von Argumentdeskriptoren im muHwI-Stil.
    • Die Prozedur, deren Name auf main endet, wird ausgeführt.
    Befehlssatz (RISC): Sehr reduziert, nur 32bit-Befehle
    • Zuweisung: mov
    • Arithmetik: add sub imul idiv cdq neg inc dec lea sal sar
    • Booleans: and or xor not
    • Stack: push pop leave und enter _,0
    • Kontrollfluss: call ret jmp je jne jl jle jg jge
    • Flags: cmp (Nur dieser Befehl setzt Flags, entgegen der Intel Spec.)
    Speichermodell - Heap:
    • Mit h_alloc_obj oder h_alloc_arr kann man sich eine Heap-Zelle anfordern, diese werden als h0, h1, ... geschrieben.
    • Eine Heapadresse besteht aus einem Zellen-Bezeichner und einem Offset, also z.B. h1:-4 für die Länge des bei h1 liegenden Arrays oder h0:4 für das erste Feld des Objektes in der Zelle h0.
    • Offsets, die nicht durch 4 teilbar sind oder aus der Zelle herauszeigen, sind ungültig.
    • Zugriff auf eine ungültige Heap-Adresse wirft eine Ausnahme.
    • Ebenso das Laden einer nicht-initialisierten Heap-Adresse.
    Speichermodell - Stack:
    • Der Stack ist ein linearer Adressraum, getrennt vom Heap.
    • Interna: beginnt mit 0, nach unten wachsend (also Stackadressen sind intern negativ).
    • Die Register esp und ebp dürfen nur mit Stack-Adressen geladen werden.
    Kontrollfluss:
    • Jumps und calls nur auf feste labels!
    • call legt eine Rücksprungadresse auf den Stack, die von ret wieder entfernt wird. Intern ist das der Bezeichner der aufgerufenen Funktion. Die Sprungadresse kann nicht verwendet werden.
    • Ein call sichert alle temp-Register.
    Debuggingoptionen:
    • NYI
    • risc386 schreibt einen Call- und execution-trace nach stderr, mehr gibts im Moment noch nicht.

  • Aktivitätsanalyse: Zur Visualisierung der Graphen, die in der Analyse erzeugt werden, bietet sich das dotty tool an, das auf den CIP Rechnern installiert ist (Packet: graphviz).

    Aufruf zur Erstellung eines PostScript files (dotty Beispielsfile):

           dot -Tps example.cfg.dot -o example.cfg.ps
    

    Dokumentation:

  • Zusammenfassung: Zum Aufruf der Systemfunktionen aus dem generierten Assembler code, kann man folgende Laufzeitbibliothek verwenden.
    Valid HTML 4.01!
    Hans-Wolfgang Loidl
    Last modified: Thu Feb 7 13:42:44 2008 Stardate: [-29]8932.64
    Valid CSS!