A Java Class File Parser

Bachelorarbeit A Java Class File Parser Wolfgang Estgfäller (0617349) [email protected] 1 März 2010 Betreuer: Friedrich Neu...
Author: Christina Kuntz
4 downloads 1 Views 975KB Size
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