A Java Class File Parser

Werbung
Bachelorarbeit
A Java Class File Parser
Wolfgang Estgfäller
(0617349)
[email protected]
1 März 2010
Betreuer:
Friedrich Neurauter
Zusammenfassung (Englisch)
Java
is one of the most widely spread and used programming languages. One
reason for that fact is the portability of Java. Java source code is compiled into a platform-independent intermediate representation, the so-called bytecode.
The aim of this project was to write a parser for the binary
class les
contain-
ing the bytecode and to present the gained contents by a graphical user interface.
Inhaltsverzeichnis
1 Einleitung
1
2 Kontextfreie Grammatiken und Parser
2
2.1
Formale Sprachen und kontextfreie Grammatiken . . . . . . . . .
2
2.2
Parser
4
2.3
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.1
Top-down Parser . . . . . . . . . . . . . . . . . . . . . . .
5
2.2.2
Bottom-up Parser
6
ANTLR
. . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3.1
Überblick
2.3.2
LL(*)
2.3.3
ANTLR - Grammatik
7
. . . . . . . . . . . . . . . . . . . . . . . . . . .
7
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
. . . . . . . . . . . . . . . . . . . .
9
3 Class-File Format
11
4 Vergleichbare Tools
16
5 Implementierung
18
5.1
Programmstruktur
5.2
Funktionsweise
5.3
Grammatik
5.4
Pakete im Detail
5.5
18
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
19
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19
. . . . . . . . . . . . . . . . . . . . . . . . . . .
20
5.4.1
Classle . . . . . . . . . . . . . . . . . . . . . . . . . . . .
20
5.4.2
Gui
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
22
5.4.3
Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23
Benutzerhandbuch
6 Zusammenfassung
iv
. . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . .
23
27
1 Einleitung
Java Class-Files
sind binäre Dateien, die von einem Java-Compiler erzeugt wer-
den. Sie enthalten beispielsweise Informationen über Methoden, Felder, Konstanten und natürlich auch den
Java-Bytecode.
Der Bytecode ist ein Zwischen-
code, der vom Compiler generiert wird. Er enthält Instruktionen, die von der
Java Virtual Machine
ausgeführt werden können. Eine Eigenschaft vom Byte-
code ist seine Plattformunabhängigkeit. Dadurch kann ein kompiliertes Java
Programm auf mehreren Plattformen ausgeführt werden. Natürlich unter der
Voraussetzung, dass auf der Zielplattform eine Java Virtual Machine vorhanden
ist.
Dadurch existiert eine Vielzahl von Java Programmen, welche unter anderem
aus dem Internet bezogen werden können. Seien es kommerzielle oder freie
Programme. Oft wird nur die ausführbare Variante eines Programms angeboten. Auÿerdem sind viele Programme schlecht dokumentiert und geben unzureichende Informationen über deren Inhalt oder Funktionsweise. Treten dann
zum Beispiel bei der Verwendung einer Bibliothek Fehler auf, kann es mühsam
werden diese zu beseitigen.
Motivation für diese Arbeit war es, eine Lösung für Probleme solcher Art zu
bieten. Man sollte die Möglichkeit haben Class-Files einzulesen und anzuzeigen.
Dies kann hilfreich sein, wenn der Quellcode aus irgendwelchen Gründen verloren ging oder auch einfach nur um den Inhalt zu analysieren.
Ziel dieser Arbeit war es ein Programm zu entwerfen, das die Inhalte der Java Class-Files präsentiert. Dabei sollte ein Parser mit dem bekannten Parser
Generator ANTLR erzeugt und implementiert werden. Dem Benutzer sollte die
Möglichkeit gegeben werden, die Inhalte der Class-Files benutzerfreundlich zu
durchstöbern.
Diese Arbeit gliedert sich wie folgt. Der erste Teil beschäftigt sich mit grundlegendem Wissen 2 über kontextfreie Grammatiken, Parser im Allgemeinen und
dem ANTLR Framework, mit dessen Hilfe Parser erstellt werden können. Dann
folgt eine kurze Einführung in das Format der Java Class-Files 3. Dabei wird auf
deren Struktur und Inhalte eingegangen. Im Kapitel 5 wird die Implementation
des Programms beschrieben. Am Ende wird noch eine Zusammenfassung über
das Erarbeitete gegeben.
1
2 Kontextfreie Grammatiken und
Parser
2.1 Formale Sprachen und kontextfreie Grammatiken
Die nachfolgenden Denitionen sind eine Einführung in die Theorie der Alphabete, Wörter, Sprachen und kontextfreien Grammatiken. Grundlage dafür waren
[6], [4], [7] und [9].
Ein
Alphabet Σ
ist eine endliche, nichtleere Menge von Symbolen (Zeichen),
Wort (Zeichenkette) w über Σ ist eine endliche
w = w0 , w1 , . . . , wn−1 mit wi ∈ Σ, n ∈ N, wobei |w| = n die Länge des
∗
Wortes w bezeichnet. Σ bezeichnet die Menge {w | w = w0 , w1 , . . . , wn−1 , wi ∈
Σ, n ∈ N}, also alle Wörter über dem Alphabet Σ. Σ+ steht für die Menge aller
+ = Σ∗ \ {ε}, wobei ε dem leeren Wort
nichtleeren Wörter über Σ, genauer Σ
∗
entspricht, für welches |ε| = 0 und εw = wε = w gilt. Eine Teilmenge L ⊆ Σ
heiÿt formale Sprache über Σ.
z.b. Buchstaben oder Zahlen. Ein
Folge
Kontextfreie Grammatiken
Denition 2.1.
Eine
kontextfreie Grammatik G
(Abk. KFG) ist ein 4-Tupel
G = (N, Σ, P, S)
mit folgenden Eigenschaften:
• N
ist ein Alphabet, dessen Elemente als
Nichtterminale
(engl. nontermi-
nals) bezeichnet werden.
• Σ ist ein Alphabet, dessen Elemente als Terminale
(engl. terminals) beze-
ichnet werden.
• Es gilt N ∩ Σ = ∅.
• P ist eine endliche Teilmenge von N ×V, wobei das Vokabular V die Menge
N ∪ Σ ist.
• Ein Element (A, α) aus P , mit A ∈ N und α ∈ V ∗ , wird als Produktion
bezeichnet und wird in der Form A → α geschrieben.
• S ∈ N ist das Startsymbol.
Beispiel 2.2.
Ein Beispiel für eine kontextfreie Grammatik, die die Sprache
einfacher arithmetischer Ausdrücke beschreibt:
G = ({expr, term, f actor, number, digit}, {0, 1, . . . , 9, +, ∗, (, )}, P, expr),
2
2.1 Formale Sprachen und kontextfreie Grammatiken
P
mit
bestehend aus den Produktionen
expr → term + expr | term
term → f actor ∗ term | f actor
f actor → number | ( expr )
number → digit | number digit
digit → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Die Nichtterminal-Symbole sind
expr, term, f actor, number
0 bis 9, +, ∗, ( und ).
und
digit,
die
Terminal-Symbole sind die Ziern
Um eine kompaktere Schreibweise einer Menge von Produktionen zu erhalten wird allgemein die Konvention verwendet, dass Produktionen der Form
A → α1 , A → α2 , . . . , A → αn
A → α1 |α2 | . . . |αn .
Denition 2.3.
Seien
abgekürzt dargestellt werden können als
Gegeben sei eine kontextfreie Grammatik
α, β, γ ∈ V ∗ .
Dann kann
β
aus
α
in einem
G = (N, Σ, P, S).
direkten Schritt abgeleitet
werden, geschrieben als
α−
→ β,
G
∗
wenn es zwei Worte v und w über V
und eine Produktion A → γ in P gibt,
sodass α = vAw
und
β = vγw. Falls v ∈ Σ∗ k, dann wird die Ableitung
als
Linksableitung
bezeichnet
Wenn
∗
−
→
G
und
∗
{w ∈ Σ∗ | S −
→ w}
G
S∈V
∗ und
w ∈ Σ∗
als
Rechtsableitung.
ist die reexive und transitive Hülle von
w ∈ V∗
Beispiel 2.4.
bezeichnet und falls
∗
S −
→ w
G
wird als
gilt, dann ist
w
in
Gegeben sei die kontextfreie Grammatik
β = 3 + 4 ∗ 12 ∈
G
Satzform.
die von G erzeugte Sprache
Weiters
−
→.
Die Menge
L(G) =
bezeichnet.
G
aus Beispiel 2.2,
α=
Σ∗ . Dann gilt
∗
α−
→β
G
∗
∗
expr −
→ term + expr −
→ 3 + expr −
→ 3 + term −
→ 3 + f actor ∗ term −
→
G
G
∗
G
G
G
∗
3 + 4 ∗ term −
→ 3 + 4 ∗ number −
→ 3 + 4 ∗ digitnumber −
→ 3 + 4 ∗ 12
G
G
Denition 2.5.
w∈
Sei
G = (N, Σ, P, S)
ein Ableitungsbaum
Σ∗ . Dann ist
G
eine kontextfreie Grammatik und sei
(engl. parse tree)
B
von
w
ein markierter
Baum, wenn gilt:
(1) Die Wurzel ist mit
S
markiert.
(2) Jedes Blatt ist mit einem Element aus
Σ∗
markiert.
(3) Für einen inneren Knoten mit der Markierung A muss gelten
A ∈ N.
k die Markierung K hat und seine Kinder mit den
K0 , K1 , . . . , Kn−1 versehen sind (Ki ∈ V, 0 ≤ i ≤ n − 1),
K → K0 , K1 , . . . , Kn−1 eine Produktion in P sein.
(4) Wenn der Knoten
Markierungen
dann muss
3
2 Kontextfreie Grammatiken und Parser
(5) Das Wort w
w
ergibt sich aus der Verkettung der Markierungen aller
Blätter von links nach rechts.
Die Grammatik
G wird als mehrdeutig (engl. ambiguous) bezeichnet, wenn es für
v aus L(G) mehrere unterschiedliche Ableitungsbäume
mindestens ein Element
gibt.
Beispiel 2.6.
Gegeben sei eine kontextfreie Grammatik
G = (N, Σ, P, {S}),
wobei
• N = {S, E, O}
• Σ = {0, 1, +, ·}
• P = {S → E, E → EOE, E → 0, E → 1, O → +, O → ·}
Für
w =0·1+1·0
sieht Ableitungsbaum folgendermaÿen aus:
S
E
E
O
E
O
E
0
·
1
+
E
E
O
E
1
·
0
2.2 Parser
Unter dem Begri
parsen
(engl. to parse analysieren) versteht man die syn-
taktisch Analyse einer Sequenz von Symbolen. Dabei wird die Eingabe in eine
Sequenz von sogenannten Tokens, beispielsweise Wörter, umgewandelt. Diese
Sequenz wird dann mit einer Grammatik abgearbeitet. Dabei wird durch die Anwendung der Produktionen der Grammatik auf die Eingabesequenz ein Ableitungsbaum erstellt. Wenn die Sequenz erfolgreich abgearbeitet wird, ist die Eingabe
gültig.
In der Informatik werden die dafür zuständigen Programme als
Parser
bezeich-
net. Meistens sind Parser eine Komponente eines Compilers oder Interpreters,
die eine Eingabe auf die Korrektheit der Syntax überprüft und eine Datenstruktur erstellt, um die ausgelesenen Informationen weiterverarbeiten zu können.
In den meisten Fällen wird eine Art Baumstruktur erstellt, genannt der Syntaxbaum, was nichts anderes ist, als ein reduzierter Ableitungsbaum (siehe
Denition 2.5). Diese abstrahierten Baumstrukturen enthalten nur mehr die
notwendigen Informationen. Elemente die ausschlieÿlich zur Strukturierung dienen, wie zum Beispiel ; bei der Programmiersprache Java, werden dabei
üblicherweise weggelassen. Oft werden auch zusätzliche Informationen eingefügt, um eine bessere/einfachere Struktur zu erhalten.
4
2.2 Parser
Das Einlesen der Eingabesymbole und die Erstellung von Tokens wird in vielen
Fällen von einem separaten lexikalischen Scanner übernommen. Die erstellten
Tokens werden dann Stück für Stück an den eigentlichen Parser weitergegeben.
Die folgende Abbildung veranschaulicht den Ablauf eines üblichen Parsers.
Abbildung 2.1: Illustration der einzelnen Arbeitsschritte
Parser Typen
Generell unterscheidet man zwischen zwei Typen von Parsern:
1.
Top-down
2.
Bottom-up
Parser
Parser
2.2.1 Top-down Parser
Ein Top-down Parser leitet die Eingabe nach dem Prinzip der Linksableitung
(siehe Denition
2.3)
ab. Bei der schrittweise durchgeführten Ableitung wird
von links nach rechts abgeleitet, das heiÿt, es wird immer das Nichtterminal der
aktuellen Satzform durch die entsprechende Produktionsregel ersetzt. Das ganze
nennt sich Top-down parsen, weil die Expansion des Ableitungsbaumes von der
Wurzel zu den Blättern erfolgt. Ein Beispiel dazu gibt es im Anschluss.
Wenn zwei oder mehrere Alternativen mit dem selben Nichterminal oder dem
selben Terminal beginnen, ist die Wahl der Alternative
nichtdeterministisch. Da
Nichtdeterminismus jedoch praktisch nicht realisierbar ist, müssen andere Lösungen verwendet werden.
Eine Lösung für dieses Problem ist Backtracking. Dabei merkt sich der Parser
die Entscheidungspunkte und wenn eine Ableitung fehlschlägt, springt er zum
5
2 Kontextfreie Grammatiken und Parser
jeweiligen Entscheidungspunkt zurück. Der fehlgeschlagene Entscheidung wird
markiert und es wird mit einer anderen Alternative fortgefahren. Das macht die
Lösung mächtig, aber auch langsam aufgrund der resultierenden exponentiellen
Komplexität.
Eine andere Möglichkeit ist das prädiktive Parsen, bei der die Wahl der richtigen Produktionsregel mit einer Vorschau, genannt
Lookahead,
von einem oder
mehreren Symbolen getroen wird. Parser dieser Variante werden häug verwendet, da die Erstellung sehr intuitiv ist.
Die bekanntesten prädiktiven Parser sind LL(k) Parser, wobei k für die Anzahl
der Lookahead Symbole steht. LL steht für das Abarbeiten der Eingabe von
Links nach rechts und die Ableitung mithilfe der Linksableitung.
2.2.2 Bottom-up Parser
Da bei dieser Bachelorarbeit kein Bottom-up Parser verwendet wurde, wird
diese Thematik nur kurz angeschnitten. Anders als beim Top down Parser, erfolgt die Erstellung des Ableitungsbaumes nicht durch eine Expansion, sondern
durch eine Reduktion von Satzformen (siehe Beispiel
2.7).
Bottom up Parser
schlieÿen von den kleinsten gefunden Einheiten auf den höheren Zusammenhang. Sie arbeiten sich also von unten nach oben.
Grammatiken die Zykel oder
ε-Produktionen
enthalten terminieren mit einem
Bottom up Parser nicht.
Ein bekannter Bottom-up Parser ist der LR oder LR(k) Parser, der die Eingabe
von Links nach rechts abarbeitet und die Rechtsableitung verwendet.
Beispiel 2.7.
Gegeben sei die KFG
G = ({S, A, B, C}, {a, b, c}, P, {S}) mit den
Produktionen
S ⇒ AB
A ⇒ aA|A
B ⇒ bB|b
C ⇒ cC|c
Ableitung des Wortes aabb mit einer Sequenz von Linksableitungen:
S−
→ ABC −
→ aABC −
→ aaBC −
→ aabBC −
→ aabbC −
→ aabbcC −
→ aabbcc
G
G
G
G
G
G
G
Und die Rechtsableitung:
S−
→ ABC −
→ ABcC −
→ ABcc −
→ AbBcc −
→ Abbcc −
→ aAbbcc −
→ aabbcc
G
G
G
G
G
G
G
Der dazugehörige Ableitungsbaum ist bei beiden Ableitungen identisch, die Reihenfolge bei der Erzeugung unterscheidet sich jedoch:
6
2.3 ANTLR
S
1
Expansions-Reihenfolge bei
einer Linksableitung:
A
a
3
2
B
5
A
b
a
C
B
6
1, 2, 5, 3, 6, 4, 7
4
c
b
7
C
c
Reduktions-Reihenfolge bei
einer Rechtsableitung:
7, 4, 6, 3, 5, 2, 1
2.3 ANTLR
Da das im Rahmen dieses Arbeit erstellte Programm ANTLR als Parser Generator verwendet, wird in diesem Abschnitt eine kurze Einführung in das von
Terence Parr erstellte Framework gegeben. Jedoch wird nur ein allgemeiner
Überblick gegeben, für mehr Informationen wird das das Buch [10] von Terence Parr wärmstens empfohlen. Alternativ kann auch die Internet-Präsenz [1]
des Projektes besucht werden.
2.3.1 Überblick
ANTLR, ANother Tool for Language Recognition, ist ein objektorientierter
Parser Generator, der in Java geschrieben wurde. Das Tool erstellt lexikalische
Scanner, Parser und Treeparser anhand von LL(*) Grammatiken. Auÿerdem unterstützt das Tool eine Reihe von Zielsprachen, wie zum Beispiel
und
Python.
C, C#, Java
Das Projekt wird seit 1989 von Terence Parr an der Universität von San Francisco entwickelt. Die aktuelle Version ist ANTLRv3 und ist als Freie Software
unter der BSD Lizenz verfügbar.
Arbeitsweise
Abbildung
2.2
veranschaulicht die Arbeitsweise von ANTLR. Die Rechtecke
repräsentieren die Übersetzungsphasen und die Kanten die Datenstrukturen die
dabei erstellt werden. Am Anfang erhält der Lexer eine Sequenz von Eingabesymbolen, welche er in Tokens umwandelt. Der Parser erstellt anhand der denierten
Grammatik gegebenenfalls einen abstrakten Syntaxbaum (engl. abstract syntax
tree AST), den ein sogenannter Treewalker dann durchläuft. Oft sind mehrere
Iterationen von Nöten bis das Endresultat erreicht wird, also übergibt der Treewalker seinen Erstellten AST an den nächsten und so weiter. Der Parser und
die Treewalker können sich gemeinsame Strukturen teilen. Treewalker werden
bei ANTLR nämlich auch mit einer Grammtik erstellt, die der Grammatik des
Parser meist sehr ähnlich sieht. Das erleichtert deren Erstellung ungemein.
7
2 Kontextfreie Grammatiken und Parser
Abbildung 2.2: Datenuss beim Parse-Vorgang
Benutzung
Die von ANTLR verwendete Sprache ist eine Mischung aus einer Grammatik,
benutzerdenierten Einstellungen und Befehlen in der gewählten Zielsprache.
Ein Beispiel für eine sehr einfache Grammatik, die helloworld gefolgt von einem
oder mehreren ! generiert:
grammar Simple;
s : a b (c)+;
a : 'hello';
b : 'world';
c : '!';
NL: ('\n')+ {$channel=HIDDEN;} ; // ignoriere newline
2.3.2 LL(*)
Es wurde bereits erwähnt, dass ANTLR die Generierung von LL(*) Parsern,
also Parsern die theoretisch einen beliebigen Lookahead erlauben, unterstützt.
Der Unterschied dabei ist, dass LL(k) Parser, wenn sie zwischen verschiedenen
Alternativen unterscheiden müssen, maximal k Symbole vorausschauen. LL(k)
Parser werden durch deterministische endliche Automaten (DEA) realisiert, die
zyklenfrei sind. Beim LL(*) Parsing hingegen wird bei der Ermittlung, welche
Alternative verwendet werden soll, ein DEA erstellt, der Zyklen enthalten kann.
Sobald die Alternative ermittelt ist, wird das LL Parsen wie sonst üblich fortgeführt.
Die folgende Grammatik ist zum Beispiel nicht LL(k):
grammar T;
def :
(modifier)+ 'int' ID '=' INT ';' //z.B. "public int x=5;"
|(modifier)+ 'int' ID ';'
//z.B. "public static int x;"
modifier :
'public'
|'static'
;
8
2.3 ANTLR
Da hier beliebig viele Modier vorkommen könnten, kann diese Grammatik für
ein xes k nicht verwendet werden. Für einen LL(k) Parser müsste die Regel
def links-faktorisiert werden. Mit einem LL(*) Parser, wie ANTLR, kann ein
solches Konstrukt aber gelesen werden.
2.3.3 ANTLR - Grammatik
Wie schon angesprochen, ist die Sprache von ANTLR eine Kombination aus Einstellungen, Grammatik und eigenem Code. In diesem Abschnitt werden einige
Funktionalitäten beschrieben, um sich ein besseres Bild machen zu können.
Datei Kopf
Im Datei Kopf hat man die Möglichkeit globale Einstellungen zu setzen: Stellt
die Datei eine Grammatik oder eine Baumgrammatik dar? Welche Zielsprache
wird verwendet? Welche Tokens werden verwendet? Soll ein AST erstellt werden? Code, der zum Datei Kopf hinzugefügt werden soll . . .
Aktionen und Attribute
Die in ANTLR verwendete Sprache erlaubt, wie bereits erwähnt, das Einbinden
von eigenem Code in der gewählten Zielsprache. Diese Code-Blöcke werden
als Aktionen bezeichnet und können in den Grammatik-Regeln, auch zwischen
einzelnen Symbolen oder Regelaufrufen, beliebig platziert werden. Man kann
für jede Regel auch einen
init-
oder
after-Codeblock
denieren, in dem Code
formuliert werden kann, der beim Eintritt in die Regel beziehungsweise beim
Verlassen der Regel ausgeführt wird.
Regeln und Tokens stellen vordenierte Attribute zur Verfügung, die in Aktionen
verwendet werden können. Damit kann beispielsweise auf den Text zugegrien
werden der, von einer Regel bzw. einem Token geparst wurde.
Variablen mit Gültigkeitsbereich
ANTLR ermöglicht die Erstellung von Scopes. Darunter versteht man Variablen mit einem begrenzten Gültigkeitsbereich. Eine Regel kann angeben, ob
sie ein Scope verwenden will. Dieses Scope ist dann in allen Regeln, die im Zuge
ihrer Ausführung aufgerufen werden, sichtbar. Realisiert wird dieses Konzept
mit Stacks. Beim Eintritt in eine Regel wird ein Element auf den Stack gelegt,
beim Verlassen wieder heruntergenommen. Somit besteht die Möglichkeit Scopes
zu verschachteln.
Man hat einerseits die Möglichkeit sogenannte globale Scopes zu erstellen. Diese
globalen Scopes werden mit einem einzigen Stack realisiert. So können sich
beispielsweise mehrere Regeln ein gemeinsames Scope teilen. Andererseits gibt
es auch die dynamischen Scopes, die für einzelne Regeln deniert werden können. Hierbei wird immer ein separater Stack verwendet.
9
2 Kontextfreie Grammatiken und Parser
Backtracking und Memoization
Wenn für die spezizierte Grammatik kein gültiger LL(*) Parser erstellt werden
kann, ist es möglich die Option Backtracking zu aktivieren. Dabei probiert der
Parser einfach alle Alternativen durch bis eine Alternative zutrit, oder eben
alle Möglichkeiten durchprobiert wurden. Zusätzlich kann die Option Memoization aktiviert werden, dabei werden Ergebnisse, die beim Backtracking entstehen, zwischengespeichert und können wiederverwendet werden. Backtracking in
Kombination mit Memoization soll eine (fast) lineare Laufzeit garantieren.
Prädikate
ANTLR erlaubt die Verwendung von syntaktischen und semantischen Prädikaten. LL(*) ist zwar schon viel besser als LL(k), jedoch hat LL(*) trotzdem noch
einige Schwächen. So können mit dem Einsatz von Prädikaten auch Grammatiken, die kontextsensitive Teile, enthalten behandelt werden. Prädikate ermöglichen die Auswertung von mehrdeutigen Regeln, beispielsweise durch die
Selektion einer Regel anhand des Kontextes, in dem sie aufgerufen wurde. Allgemein ausgedrückt stellen Prädikate Bedingungen dar, die zur Laufzeit ausgewertet werden.
Baumerstellung
Wird eingestellt, dass ANTLR einen Baum als Ausgabe verwenden soll, erstellt
ANTLR automatisch einen Ableitungsbaum. Dieser Baum kann dann mit einer
neuen Grammatik, die als tree
grammar
deniert wird, geparst werden. Auch
für eine Baum-Grammatik kann als Ausgabe wiederum ein Baum erstellt werden. Somit können mehrere Iterationen leicht durchgeführt werden.
Standardmäÿig wird die von ANTLR vorgegebene Datenstruktur verwendet,
jedoch kann diese auch an eigene Wünsche angepasst werden.
10
3 Class-File Format
Dieses Kapitel soll dem Leser Einblick in das
Java Class-File
Format geben.
Als Class-Dateien bezeichnet man Dateien, die beim Kompilieren aus Java
Quellcode erzeugt werden. Dabei wird eine Art Zwischencode, der sogenannte
Bytecode,
erzeugt. Dieser Zwischencode ist platformunabhängig und kann mit
Hilfe einer Java Laufzeitumgebung ausgeführt werden. Das Prinzip dahinter
ist Write Once, Run Everywhere , zu deutsch Einmal schreiben, überall ausführen . Die Java Laufzeitumgebung (Java Runtime Environment, JRE) enthält
eine virtuelle Maschine (Java Virtual Machine, JVM) welche den Bytecode in
Maschinencode übersetzt und ausführt.
Im Folgenden wird der Aufbau dieser Datei erleutert, wobei diese Informationen
hauptsächlich aus dem Buch [8] entnommen wurden, welches die Java Version
Java Specication
[2] entnommen, welcher im Java Community Process (JCP)
1.5 beschreibt. Details zur aktuellen Version 1.6 wurden dem
Request
JSR 202
eingetragen ist. Die Datei besteht aus einem Stream von Bytes. Normalerweise
entsprechen entweder 8, 16, 32 oder 64 Bit (u1, u2, u4, oder u8) einem Datensatz, wobei Datensätze die aus mehreren Bytes zusammengesetzt sind, immer
in
Big-Endian 1
Ordnung vorliegen. Die Tabelle veranschaulicht die Class-File
Struktur.
Class File {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Kopfdaten
Konstanten-Pool
Klassen-Deskriptor
Felder
Methoden
Klassen-Attribute
Tabelle 3.1: Class-File Struktur
1
Bezeichnet die Anordnung der Bytes im Speicher, wobei höherwertige Bytes zuerst vorkommen.
11
3 Class-File Format
Inhalt
Um einen besseren Überblick zu bieten wurde der Inhalt in verschiedene Bereiche (siehe Tabelle
3.1) unterteilt. Auf diese Bereiche wird im folgenden kurz
eingegangen.
Kopfdaten
Jedes Class-File beginnt mit der sogenannten
muss dem Wert
0xCAFEBABE
Magic Number.
Diese Signatur
entsprechen. Weiters wird hier noch die Java Ver-
sion speziziert, welche für die Ausführung dieser Datei erforderlich ist. Diese
Nummer setzt sich aus der
Major
und
Minor
Versionsnummer zusammen. Die
Versionen unterliegen einer lexikographischen Ordnung. Wenn zum Beispiel Mi-
2 ist und Major
1.1 < 1.2 < 2.1.
nor gleich
wobei
gleich
1,
dann ist die Versionsnummer gleich
1.2,
Konstanten-Pool
Der Konstanten-Pool enthält mehrere der folgenden Konstanten:
01
03
04
05
06
07
08
09
10
11
12
CONSTANT_Utf8
CONSTANT_Integer
CONSTANT_Float
CONSTANT_Long
CONSTANT_Double
CONSTANT_Class
CONSTANT_String
CONSTANT_Fieldref
CONSTANT_Methodref
CONSTANT_InterfaceMethodref
CONSTANT_NameAndType
Die Konstanten Utf8, Integer, Float, Long, Double und String repräsentieren
ihren jeweiligen Datentyp. Fieldref, Methodref und InterfaceMethodref repräsentieren Felder, Methoden und Interface-Methoden. Der Typ Class repräsentiert
ein Interface oder eine Klasse. NameAndType Konstanten enthalten zwei Verweise auf Utf8 Einträge im Konstanten Pool, die den Name und Typ eines Feldes
oder einer Methode repräsentieren. Die Einträge Long und Double beanspruchen
zwei Indexe im Pool. Der Tag
02
wird nicht verwendet.
Abbildung 3.1: Beziehungen zwischen den Konstanten.
12
Klassen-Deskriptor
Im Klassen-Deskriptor ist eine Maske von Access Flags deniert, welche die Zugrisrechte und Eigenschaften der Klasse bzw. des Interfaces bestimmt. Weiters
enthält sie zwei Verweise auf
CONSTANT_CLASS
Einträge im Konstanten Pool,
welche die this-Klasse und Superklasse repräsentieren. Abschlieÿend folgen noch
alle Interfaces, die von der Klasse implementiert werden.
Attribute
Da die Felder, Methoden und die Klasse selbst Attribute enthalten kann, wird
zuerst eine kurzer Überblick über die verschiedenen Attribute gegeben. Es werden nur die Attribute beschrieben, die im Programm zu dieser Arbeit verwendet
wurden.
Generell hat ein Attribut folgenden Aufbau:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
Der Name Index verweist auf einen
CONSTANT_Utf8
Eintrag im Pool, der den
Name des Attributes enthält. Die Attributlänge deniert die Anzahl der Bytes
die folgen. Danach folgt der eigentliche Inhalt des Attributes, dieser ist von
Attribut zu Attribut verschieden.
ConstantValue:
Ein statisches Feld wird mit dem Wert von
ConstantValue
initialisiert.
Code:
Der Code den eine Methode enthält.
Exceptions:
Exceptions die von einer Methode geworfen werden.
InnerClasses:
Eine innere Klasse.
EnclosingMethod:
Wenn die Klasse anonym oder eine Local Class ist.
Synthetic:
Nicht sichtbar im Quellcode.
Signature:
Eine Signatur die eine Klasse, Methode oder ein Feld beschreibt.
SourceFile:
13
3 Class-File Format
Name der Klasse bzw. des Interfaces.
LineNumberTable:
Eine Debug Erweiterung, mit der ermittelt werden kann, welcher Teil vom Code
zu einer bestimmten Zeilennummer im Quellcode gehört.
LocalVariableTable:
Diese Tabelle kann von einem Debugger verwendet werden um den Wert einer
lokalen Variable während der Ausführung einer Methode zu bestimmen.
Deprecated:
Wenn eine Klasse oder Methode auf deprecated gesetzt sein sollte.
Felder
Hier werden alle Felder der Klasse bzw. des Interfaces abgespeichert. Jedes Feld
ist wie folgt aufgebaut:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Access Flags beschreiben wieder eine Maske, die Informationen über die Zugrisrechte und Eigenschaften enthält. Der Name Index zeigt auf einen
CONSTANT_Utf8
Eintrag im Pool, der den Namen des Feldes repräsentiert. Das Gleiche gilt für
den Deskriptor Index. Danach folgen noch die Attribute des Feldes. Gültige Attribute für ein Feld sind
ConstantValue, Synthetic, Signature und Deprecated.
Methoden
Hier werden alle Methoden der Klasse bzw. des Interfaces abgespeichert. Die
Methoden haben den gleichen Aufbau wie die Felder vorhin:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Die Attribute, die für Methoden gültig sind, sind
Signature
14
und
Deprecated.
Code, Exceptions, Synthetic,
Klassen-Attribute
Wie auch Methoden und Felder, kann die Klasse bzw. das Interface Attribute en-
InnerClasses, EnclosingMethod, Synthetic,
Deprecated.
thalten. Dazu zählen die Attribute
SourceFile, Signature
und
15
4 Vergleichbare Tools
In diesem Kapitel wird kurz auf zwei andere frei verfügbare Tools eingegangen.
Anschlieÿend werden die Tools mit dem im Rahmen dieser Arbeit entwickelten
Programm
Java ByteCode Explorer (Jbce)
verglichen.
Jclazz Utils
Jclazz Utils [5] ist ein Tool, das in Java geschrieben wurde. Es relativ aktuell
und bietet deshalb eine fast vollständige Unterstützung für Java Class-Files mit
Version 1.6. Das Programm bietet eine sehr einfache graphische Schnittstelle, die
einem ermöglicht ein einzelnes Class-File zu parsen. Die Inhalte werden in einer
Baumstruktur angezeigt und beim Klick auf ein Element, werden zusätzliche
Informationen dargestellt. Das Programm bietet die Möglichkeit Class-Dateien
zu dekompilieren. Der Decompiler dieses Programmes wurde in dieser Arbeit
verwendet.
Jdec
1 ist ein sehr umfangreiches Tool. Jdec unterstützt Class-Files bis zur Ver-
Jdec
sion 1.5. Die GUI von Jdec bietet eine Reihe von Funktionalitäten. Jdec besitzt
einen Decompiler, kann Assemblercode anzeigen lassen, kann .jar Dateien önen und noch vieles mehr. Leider ist das Programm unübersichtlich und beim
Einlesen gröÿerer Class-Files kann es zu Problemen kommen.
Vergleich
Im Rahmen der Arbeit sollte ein Programm geschrieben werden, dass ClassFiles einliest und deren Inhalt anzeigt. Diese Anforderung wird von Jbce am
besten erfüllt. Der Inhalt des Class-Files wird in klare Bereiche unterteilt und die
Navigation durch das Class-File gestaltet sich übersichtlich. Der Inhalt wird in
einer ansprechenden Form dargestellt. Bei Jclazz wird das Clazzle nur sehr grob
unterteilt, was bei gröÿeren Class-Files unübersichtlich werden kann, auÿerdem
ist die Darstellung der Inhalte nicht so umfangreich, wie bei Jbce. Jdec bietet
keine Möglichkeit, das Class-File genau zu betrachten, lediglich der KonstantenPool, lokale Variablen und ein paar Allgemeine Information können angezeigt
werden. Jdec richtet sich eher an jemanden, für den hauptsächlich der Quellcode
interessant ist.
Jedoch erlaubt Jdec das einlesen von .jar Dateien, ein Feature, das bei Jbce und
Jclazz Utils nicht vorhanden ist.
Während Jbce und Jclazz Java 1.6 mehr oder weniger unterstützen, wird diese
1
Die Homepage von Jdec ist
16
http://jdec.sourceforge.net/.
Version bei Jdec nicht unterstützt. Jclazz hat in diesem Punkt jedoch gegenüber
Jbce auch einen Vorteil, da bei Jbce noch einzelne Elemente der Version 1.6
fehlen.
Beim Parsen von gröÿeren Dateien kam es bei Jdec zu Probleme, währen Jbce
und Jclazz keine Problemen mit komplexeren Class-Files haben.
17
5 Implementierung
In diesem Kapitel wird die Funktionsweise des Programmes Java Byte Code Ex-
plorer beschrieben, welches im Rahmen dieser Bachelorarbeit entwickelt wurde.
Das Programm liest Java Class-Files ein und stellt den Inhalt anhand einer
Baumstruktur dar. Der Benutzer kann durch die einzelnen Elemente der ClassFiles navigieren, dabei werden genauere Informationen über das selektierte Element angezeigt. Das Programm erlaubt das Önen mehrerer Class-Files und
deren Darstellung.
Das Programm selbst ist vollständig in Java [11] geschrieben. Als ParserGenerator wird ANTLR [1] verwendet. Für das Dekompilieren des Codes wird
der Decompiler der Java Clazz Utils 1.2.2 [5] verwendet. Ein eigener Decompiler
würde den Rahmen der Arbeit sprengen. Syntax Highlighting wird mit dem
jEdit Syntax Paket [3] realisiert und wurde nicht selbst geschrieben, da es nur
eine zusätzliche graphische Aufwertung darstellt.
5.1 Programmstruktur
Das Programm ist in mehrere Pakete eingeteilt:
•
Classle
Stellt Datenstrukturen zur Verfügung, welche den Inhalt eines Class-Files
repräsentieren. Auÿerdem wird eine Vielzahl von Funktionen geboten die
abgespeicherten Informationen auszuwerten.
•
Exceptions
Das Paket enthält die verschiedenen Exceptions, die im Programm verwendet werden.
•
Gui
Die graphische Schnittstelle ist in diesem Paket implementiert. Mit der
graphischen Schnittstelle kann das Programm gesteuert werden. Über sie
werden die Class-File Dateien, die eingelesen werden sollen ausgewählt
und anschlieÿend dargestellt.
•
Parser
Mit Hilfe dieses Paketes werden die Daten aus einem Class-File ausgelesen
und in eine eigene Datenstruktur geschrieben.
•
Util
Bietet einige Funktionalitäten, die im gesamten Programm verwendet werden. Hauptsächlich um Bytecode zu analysieren oder Daten umzuwandeln.
18
5.2 Funktionsweise
5.2 Funktionsweise
Nachdem das Programm gestartet wurde, wird als erstes das Haupfenster dargestellt. Nun hat der Benutzer die Möglichkeit ein Class-File auszuwählen, welches
eingelesen werden soll. Wenn eine Datei zum Parsen ausgewählt wurde, wird
ein Parser Objekt wird erstellt, welches den Inhalt der Datei abarbeitet. Der
Lexer erstellt Tokens aus den Bytes der Datei. Der Parser versucht die TokenSequenz vom Lexer anhand der Regeln der Grammatik, abzuarbeiten und dabei
einen Parsetree zu erzeugen. Wenn dieser Vorgang erfolgreich war übergibt der
Parser seinen erstellten Parsetree einem Tree Parser, der den Parsetree durchläuft und eine Datenstruktur erzeugt, die die gesammelten Informationen enthält. Anschlieÿend wird ein internes Fenster erzeugt, welches die vorhin erstellte Datenstruktur darstellen soll. Dazu wird eine Baumstruktur erstellt, die
in die gleichen Bereiche aufgeteilt ist, wie das Class-File im vorigen Kapitel.
Diese Baumstruktur enthält einerseits Daten aus dem Class-File und andererseits wird die Struktur verwendet, um durch das Class-File zu navigieren. Wenn
das Class-File innere Klassen besitzt, wird auch deren Inhalt eingelesen und
dargestellt. Wenn das Class-File samt innerer Klassen eingelesen wurde und die
Struktur somit vollständig ist, wird sie im inneren Fenster dargestellt. Jetzt kann
der Anwender durch den Baum navigieren und Elemente selektieren. Die ausgewählten Elemente werden auch im inneren Fenster, neben der Baumstruktur,
dargestellt.
5.3 Grammatik
Zentraler Bestandteil des Programmes ist die Grammatik
Jbce.g,
die erstellt
wurde um den Inhalt von den Class-Files zu lesen. Die Grammatik ist eine Mischung aus der ANTLR - Grammatik Notation und Java Code. Die Tokens der
Grammatik, also der Input für den Parser, sind die Zahlen 0-9 und Buchstaben
A-F. Also die Werte 0 - 15 als hexadezimale Zeichen. Das Problem beim Parsen
ist, dass die Struktur des Class-Files nicht allein mit der syntaktischen Analyse
ermittelt werden kann. Anders als in einer Programmiersprache gibt es keine
Schlüsselwörter oder andere Elemente wie den Strichpunkt, also Elemente, die
zur Strukturierung verwendet werden. Für das Parsen eines Class-Files genügt
keine kontextfreie Grammatik, da die Spezikation des Class-File Formates ein
Sprache deniert, die sehr wohl kontext-sensitiv ist. Aus diesem Grund werden Prädikate verwendet. Als Beispiel dient die Regel
attributes,
welche die
Attribute einer Klasse, Methode oder eines Feldes parst:
attributes
scope{int count; int index;}
@init{$attributes::index=0;}
:
//attribute count
u2
{$attributes::count = toInt($u2.text);}
//attribute array
19
5 Implementierung
(
{$attributes::index < $attributes::count}?
attribute_info
{$attributes::index++;}
)*
;
Die Regel deniert zuerst einmal zwei dynamische Variablen Scopes,
und
index,
count
vom Typ int. Der Wert von index, wird beim Eintreten in die Regel
auf 0 gesetzt. Count erhält den Wert, der in
repräsentiert
u2
u2
gespeichert ist, in diesem Fall
die Anzahl der Attribute. Dazu wird mit Regelname.text auf
u2 liest zwei Bytes, also sind hier maximal
0xFFFF Attribute möglich. Anschlieÿend kommt ein geklammerter Ausdruck, der
den Wert der Regel zugegrien.
beliebig oft vorkommen kann, aufgrund des *, der hinter den beiden Klammern
steht. Innerhalb dieses Audruckes bendet sich an erster Stelle das Prädikat
{$attributes::index < $attributes::count}?, welches besagt, dass der Ausdruck abgebrochen wird, wenn index nicht mehr kleiner als count ist. Wenn
die Bedingung jedoch gilt, wird attribute_info geparst und anschlieÿend index
inkrementiert. Praktisch wird attribute_info genau count mal gelesen. Durch
Konstrukte dieser Art wird eine Mehrdeutigkeit der erstellten Grammatik vermieden.
Im Datei-Kopf wurde die Einstellung gesetzt, dass ANTLR als Ausgabe einen
AST erstellen soll. ANTLR generiert also einen abstrakten Ableitungsbaum,
der alles eingelesene enthält. Dieser AST wird dann an die Baumgrammatik
JbceTree.g weitergegeben. Diese Baumgrammatik sieht der Grammatik Jbce.g
sehr ähnlich. Rein von der Grammatik Notation ist sie etwas kürzer, da Elemente, wie zum Beispiel die Tokens der Ziern 0-9, ausgelassen werden können.
Die Baumgrammatik durchläuft also das bereits geparste ein weiters mal. Der
Grund dafür ist, dass das Problem durch die iterative Vorgehensweise vereinfacht wird. Würde nur eine Grammatik erstellt, wäre diese weitaus komplexer.
Die Aufgabe der Baumgrammatik ist es anhand der vorhin erstellten Baumstruktur ein
ClassFile
Objekt zu erzeugen und es mit den Daten zu füllen.
5.4 Pakete im Detail
Auf die wichtigsten drei Pakete,
classfile, gui und parser wird im Folgenden
näher eingegangen.
5.4.1 Classle
Das Paket enthält alle Objekte, die verwendet werden um den Inhalt eines
Class-Files zu repräsentieren. Die Datenstruktur ist hierarchisch aufgebaut. Das
Wurzelelement bildet die Klasse
ClassFile,
in der man alle weiteren Objek-
te des Paketes verschachtelt wieder ndet. Das Objekt enthält die Klassen
FileHeader, ClassDescriptor, ConstantPool
und Listen, welche die Method-
en, Felder, Interfaces und inneren Klassen enthalten. Weiters werden Methoden
20
5.4 Pakete im Detail
geboten, um auf die soeben angesprochenen Elemente, Eigenschaften und andere
Attribute abzurufen. Das
ClassFile
Objekt wird beim Parsevorgang erzeugt,
dabei werden die Inhalte nach und nach hinzugefügt. Wenn alle Daten abgespeichert wurden, muss das ClassFile Objekt noch aktualisiert werden, da Referenzen zu einigen der enthaltenen Daten erstellt werden müssen, die für die
korrekte Darstellung in der GUI von Nöten sind.
Classle.attribute
In diesem Unterpaket ndet man die Objekte, die als Datenstrukturen für
die Attribute der Klassen, Felder und Methoden dienen. Die Attribute, die
Repräsentiert werden, werden im Kapitel
3 beschrieben. Jedes Attribut enthält
eigene Daten, welche hier, aufgrund ihres Umfanges, nicht näher erläutert werden.
Classle.constant
Wie der Name schon vermuten lässt, handelt es sich hier um die Datenstrukturen
für die Konstanten, also die Einträge im Konstanten-Pool. Jede Konstante die
in Kapitel
3
beschrieben wurde, verfügt über ein eigenes Objekt. Einige der
Konstanten verweisen auf andere Einträge im Pool (siehe Abbildung
3.1).
Da
diese Verweise zum Teil aufgelöst werden, also beispielsweise statt dem Index als
Ganzzahl eine Referenz auf das entsprechende Objekt abgespeichert wird, muss
der Konstanten-Pool nach seiner vollständigen Erstellung mehrmals durchlaufen
werden um die entsprechenden Referenzen einzufügen.
Classle.descriptor
Bestimmte Konstanten verweisen auf Typinformationen, die in Utf8 Konstanten
im Pool abgespeichert sind. Diese Typinformationen beschreiben die Signaturen
von Methoden oder Feldern. Die dort enthaltenen Strings repräsentieren kodierte
Typinformationen. Die Klassen in diesem Paket lesen diese Typinformationen
aus und bieten Methoden, um auf die Inhalte zuzugreifen.
Die Methode parseFile mit folgender Methoden-Signatur
ClassFile parseFile(File file)
hat den Deskriptor:
(Ljava/io/File;)Lat/ac/uibk/jbce/classfile/ClassFile;
Classle.signature
Wenn Generics verwendet werden, erhalten Methoden, Klassen oder Felder ein
Signatur-Attribut. Dieses Attribut enthält einen Verweis auf eine Utf8 Konstante im Pool. Der dort enthaltene String enthält die Informationen wiederum
in einer kodierten Form, welche mit diesem Paket geparst werden kann.
Zum Beispiel hätte die Methode
<T extends ArrayList<? extends U>&Iterator> T methodname(V var){}
21
5 Implementierung
die Signatur
<T:Ljava/util/ArrayList<+TU;>;:Ljava/util/Iterator;>(TV;)TT;
.
5.4.2 Gui
Im
gui Paket benden sich alle Klassen, die für die Darstellung der graphischen
Schnittstelle benötigt werden. Wenn das Programm ausgeführt wird, erstellt der
Controller, der ebenfalls im
bildung
gui Paket enthalten ist, das Hauptfenster (siehe Ab-
5.1) der Anwendung.
Wenn ein Class-File ausgewählt wurde, startet der Controller den Parse Vorgang, indem er sich des Modules
parser
bedient. Da das Parsen ein zeitinten-
sives Unterfangen ist, wird es vom Controller in einen eigenen Thread ausgelagert. Nach dem erfolgreichen Parsen der Datei, wird ein internes Fenster erstellt, dem die Class-File Datenstruktur übergeben wird. Diese internen Fenster
werden auf dem Hauptfenster präsentiert. Vom Controller werden alle geparsten
Class-Files zwischengespeichert. Sobald ein internes Fenster geschlossen wird,
wird das Class-File und die dazugehörigen inneren Class-Files aus dem Cache
entfernt. Interne Fenster werden von der Klasse
JbceFrame
repräsentiert. In-
terne Fenster sind an das Hauptfenster gebunden.
Jedes interne Fenster wird in zwei Teile geteilt um erstens die Baumstruktur
des Class-Files und zweitens den ausgewählten Inhalt anzuzeigen. Die Baumstruktur wird von der Klasse
Navigator implementiert
View.
und der gezeigte Inhalt
ist eine Implementierung der Klasse
Gui.navigator
Die Klasse
Navigator
ist eine Erweiterung der Klasse
JTree
aus dem Swing
Paket von Java. Bei der Erstellung eines Navigator-Objektes, wird die vom
Parser erstellte
ClassFile
Instanz übergeben. Der Navigator durchläuft diese
Datenstruktur und erstellt einen Baum, dessen Struktur an das Class-File angelehnt ist. Der Baum wird in die selben Bereiche eingeteilt, die auch in Kapitel
3 verwendet wurden. Jeder Knoten im Baum erhält ein Objekt zugewiesen, das
die jeweiligen Daten enthält. Diese Objekte werden zur Visualisierung des selektierten Elementes verwendet. Die Knoten des Baumes sind Objekte vom Typ
ClassFileTreeNode,
eine Erweiterung des DefaultMutableTreeNode.
Wenn der Navigator bei der Erstellung auf eine innere Klasse stöÿt, fügt er diese
zu seinem
ParseTaskScheduler hinzu. Die inneren Klassen werden erst im An-
schluss, sobald die eigentliche Struktur erstellt wurde, geparst. Das Parsen der
inneren Klassen erfolgt selbstverständlich auch in einem eigenen Thread. Enthält eine innere Klasse weitere innere Klassen werden diese wiederum in den
ParseTaskScheduler eingefügt. Der ParseTaskScheduler stellt eine geordnete Fifo Warteschlange dar. Die Elemente werden nach ihrer Tiefe eingeordnet. Direkte innere Klassen des Wurzel Class-Files werden also vor den inneren Klassen
der inneren Klassen geparst.
Die Verschachtelung der Klassen kann nämlich etwas umständlich sein. Ein
Beispiel: Klasse A hat zwei innere Klassen B und C. Es kommt vor, dass
B wiederum C als innere Klasse deniert. Da JTree keine Duplikate zulässt,
22
5.5 Benutzerhandbuch
müssten in so einem Fall zwei Knoten erstellt werden, die jedoch die gleichen
Daten enthalten. Auf diese Redundanz wurde bewusst verzichtet. Innere Klassen
werden immer nur ein einziges Mal zum Baum hinzugefügt. Wenn sie ein weiteres mal vorkommen, wird lediglich ein Knoten mit dem Namen der Klasse
hinzugefügt. Der ParseTaskScheduler garantiert, dass die inneren Klassen immer auf der höchstmöglichen Ebene eingefügt werden.
Gui.view
Die Klasse
ViewFactory
erhält ein
ClassFileTreeNode
Objekt und entschei-
det anhand des Knotens und dem dazugehörigen Objekt, welche View erstellt
werden soll. Jede View erstellt eine Swing Komponente welche dann im internen Fenster dargestellt werden kann. Bei den meisten Views wird
HTML Code
generiert um die Darstellung graphisch aufzuwerten. Die ClassFileView und
die MethodView generieren den Quellcode und Assemblercode für das gesamte
Class-File bzw. für die Methode. Das Dekompilieren übernimmt, wie schon erwähnt, der Decompiler der Jclazz Utils. Assemblercode wird mit der BCEL
Bibliothek, die Teil von Java ist, generiert.
Die erstellten Views werden vom jeweiligen
JbceFrame zwischengespeichert. Die
Erzeugung der Views kann bei gröÿeren ClassFiles nämlich viel Zeit beanspruchen
und so muss jede View nur einmal erzeugt werden. Vor allem das Syntaxhighlighting kann bei groÿen ClassFiles länger dauern. Deshalb wird die Erstellung
einer View vom Controller in einen eigenen Thread ausgelagert.
5.4.3 Parser
JbceParser, JbceTreeParser und der ByteLexer. Parser und TreeParser wurden mit ANTLR generiert, deshalb wird auch
Im Parser Paket benden sich der
nicht näher darauf eingegangen. Der ByteLexer ist eine eigene Lexer Implementation, für einen ANTLR-Parser. Der Lexer liest Byte für Byte aus einer Datei
und erstellt sich pro Byte zwei Tokens. Jedes Token enthält den Wert von 4 Bit
und entspricht somit einem Zeichen der hexadezimalen Darstellung. Somit lässt
sich die Grammatik einfacher beschreiben und lesen. Der Lexer liest ein Byte,
gibt eines der beiden Tokens zurück und puert das andere. Wenn das nächste
Token angefordert wird, wird der Puer entleert.
5.5 Benutzerhandbuch
Das Programm das im Rahmen dieser Arbeit geschrieben wurde, trägt den Namen Java ByteCode Explorer , kurz Jbce. Für die Ausführung des Programmes
muss die Java Laufzeitumgebung Version 1.6
1 installiert sein. Das Programm
muss nicht installiert werden, sondern ist als ausführbare jar Datei erhältlich. Die
jar Datei lässt sich über die Kommandozeile mit
java -jar Jbce.jar
starten.
Wenn das Programm ausgeführt wird, önet sich zuerst das Hauptfenster:
1
Die Java Laufzeitumgebung kann von
http://www.java.com/de/
bezogen werden.
23
5 Implementierung
Abbildung 5.1: Das Hauptfenster von Jbce.
Das Hauptfenster enthält die Menüleiste (1), einen internen Desktop (2) auf dem
mehrere interne Fenster (3) angeordnet werden können und eine Statusleiste (4).
Um ein Class-File zu parsen muss auf das Menü
Jbce und dann auf Open geklickt
werden. Ein Dateiauswahl-Dialog önet sich mit dem ein Class-File ausgewählt
werden kann:
Abbildung 5.2: Datei-Auswahl
Der Dateiauswahl-Dialog begrenzt die Auswahl durch einen Filter auf Dateien
mit
24
.class-Endung.
Nachdem ein Class-File ausgewählt wurde, startet der
5.5 Benutzerhandbuch
Parse Vorgang. Wenn der Vorgang abgeschlossen ist önet sich ein inneres Fenster und zeigt den Inhalt der Class Datei an. Innere Fenster sind in zwei Bereiche
eingteilt:
Abbildung 5.3: Aufteilung eines Inneres Fenster
Links (1) bendet sich der Navigator mit dessen Hilfe durch das Class-File
navigiert werden kann. Wird ein Element selektiert, wird rechts (2) der Inhalt
des gewählten Elementes dargestellt. Das ausgewählte Element wird rot umrandet, damit es gleich ins Auge sticht. Wenn ein Element ausgewählt oder ein
Teilbaum expandiert wird, werden alle Subelemente inklusive dem gewählten Element mit einem blauen Hintergrund hervorgehoben. Die einzelnen Bereiche des
ClassFiles wurden mit Icons versehen um einen besseren Überblick zu schaen.
Bei den meisten Elementen besteht der Inhalt nur aus einer Seite. Bei manchen
kann über Reiter zwischen Kategorien hin und her gewechselt werden, wie in
der Abbildung oben. Jedes innere Fenster trägt den absoluten Pfad zur Klasse
als Titel. Damit sind die Fenster leicht voneinander unterscheidbar. Zwischen
den geöneten Fenstern kann auch über die Menüleiste hin und her gewechselt
werden. Im Menü
Windows
sind alle geöneten Fenster aufgelistet.
Wie schon erwähnt werden auch alle inneren Klassen geparst und dementsprechend verschachtelt dargestellt. Ein Beispiel dazu:
25
5 Implementierung
Abbildung 5.4: Darstellung innerer Klassen
Innere Klassen die beim Parsen nicht aundbar sind, werden übersprungen.
Im Baum ndet sich dann ein Eintrag unable to parse . . . . Jedoch funktioniert
das Dekompilieren nur, wenn alle inneren Klassen präsent sind. Wenn der Inhalt
einer Datei nicht zur Grammatik passt wird der Parse Vorgang abgebrochen.
Es erscheint ein Dialog der eine kurze Fehlermeldung enthält. Für genauere
Informationen sollte auf die Konsolenausgabe geschaut werden.
26
6 Zusammenfassung
Mögliche Verbesserungen
Das Programm das im Rahmen dieser Arbeit entwickelt wurde, erfüllt die Anforderungen die gestellt wurden. Jedoch beschränkt sich die Funktionalität in
einigen Fällen auf ein Minimum, da für eine umfangreiche Applikation mehr
Zeit von Nöten gewesen wäre. Deshalb wird nun eine Liste von Möglichen zur
Verbesserung und der Erweiterung des Programms aufgeführt.
•
Die Java Class-File Spezikation sieht weitere Attribute vor, die aus zeitlichen Gründen nicht implementiert wurden. Das
StackMapTable -Attribut
und die Annotations-Attribute.
•
Aktuell verwendet das Programm eine Bibliothek für die Generierung des
Quellcode, da die Erstellung eines eigenen Decompilers den Rahmen der
Arbeit überschritten hätte.
•
Erweiterte Funktionalitäten zur Erkennung von Fehlern in Java Class-Files
sind in dieser Version nicht vorhanden.
•
Möglichkeiten zur Manipulation von Class-Files.
•
Unterstützung von .jar Dateien.
Zusammenfassung
Abschlieÿend kann man sagen, dass das Ziel der Arbeit, die Erstellung eines
Parsers für Java Class-Files unter der Verwendung des ANTLR Frameworks,
erreicht wurde. Es wurde eine Grammatik erstellt, die zum Parsen eines ClassFiles verwendet werden kann. Das Programm implementiert den aus dieser
Grammatik erzeugten Parser. Die graphische Schnittstelle bietet die Möglichkeit
mehrere Class-Files zur selben Zeit anzeigen zu lassen. Mit Hilfe des Navigators,
der die Class-Files in verschiedene Bereiche gliedert, kann deren Inhalt bequem
und verständlich angezeigt werden. Der Inhalt wird durch generierten HTMLCode in einer graphisch ansprechenden Form dargestellt. Zusätzlich werden die
inneren Klassen der geparsten Klasse eingelesen und ebenfalls im Navigator
dargestellt.
27
Literaturverzeichnis
[1] ANother Tool for Language Recognition.
http://www.antlr.org/.
[2] Java Community Process, JSR-000202 JavaTM Class File Specication
Update (Final Release). http://jcp.org/aboutJava/communityprocess/
final/jsr202/index.html.
[3] jEdit 2.2.1 Syntax Paket.
http://syntax.jedit.org/.
[4] Alfred V. Aho and Jerey D. Ullman.
and compiling.
The theory of parsing, translation,
Prentice-Hall, Inc., Upper Saddle River, NJ, USA, 1972.
[5] Andrew Dmitriev. Java Clazz Utils 1.2.2.
net/.
http://jclazz.sourceforge.
[6] John E. Hopcroft, Rajeev Motwani, and Jerey D. Ullman.
to Automata Theory, Languages and Computability.
Introduction
Addison-Wesley Long-
man Publishing Co., Inc., Boston, MA, USA, 2001.
[7] Dexter C. Kozen.
Automata and Computability. Springer-Verlag New York,
Inc., Secaucus, NJ, USA, 1997.
[8] Tim Lindholm and Frank Yellin.
edition.
Java Virtual Machine Specication, 2nd
Addison-Wesley Longman Publishing Co., Inc., Boston, MA, USA,
1999.
[9] Anton Nijholt.
ing.
Context-Free Grammars: Covers, Normal Forms, and Pars-
Springer-Verlag New York, Inc., Secaucus, NJ, USA, 1980.
[10] Terence Parr.
Languages.
The Denitive ANTLR Reference: Building Domain-Specic
Pragmatic Bookshelf, 2007.
[11] Sun/Oracle. Java - Programmiersprache.
28
http://java.sun.com/.
Herunterladen