Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Vorwort
1 Neues in Java 7
2 Threads und nebenläufige Programmierung
3 Datenstrukturen und Algorithmen
4 Raum und Zeit
5 Dateien, Verzeichnisse und Dateizugriffe
6 Datenströme
7 Die eXtensible Markup Language (XML)
8 Dateiformate
9 Grafische Oberflächen mit Swing
10 Grafikprogrammierung
11 Netzwerkprogrammierung
12 Verteilte Programmierung mit RMI
13 RESTful und SOAP Web-Services
14 JavaServer Pages und Servlets
15 Applets
16 Datenbankmanagement mit JDBC
17 Technologien für die Infrastruktur
18 Reflection und Annotationen
19 Dynamische Übersetzung und Skriptsprachen
20 Logging und Monitoring
21 Java Native Interface (JNI)
22 Sicherheitskonzepte
23 Dienstprogramme für die Java-Umgebung
Stichwort
Download: - openbook, ca. 21,3 MB
Buch bestellen
Ihre Meinung?
<< zurück | Galileo Computing / <openbook> / Java 7 - Mehr als eine Insel | vor >>
Java 7 - Mehr als eine Insel von Christian Ullenboom Das Handbuch zu den Java SE-Bibliotheken
Java 7 - Mehr als eine Insel Galileo Computing 1433 S., 2012, geb. 49,90 Euro, ISBN 978-3-8362-1507-7 |
| 23 Dienstprogramme für die Java-Umgebung
|  | 23.1 Programme des JDK
|  | 23.2 Monitoringprogramme vom JDK
|  |  | 23.2.1 jps
|  |  | 23.2.2 jstat
|  |  | 23.2.3 jmap
|  |  | 23.2.4 jstack
|  |  | 23.2.5 VisualVM
|  | 23.3 Programmieren mit der Tools-API
|  |  | 23.3.1 Eigene Doclets
|  | 23.4 Ant
|  |  | 23.4.1 Bezug und Installation von Ant
|  |  | 23.4.2 Das Build-Skript build.xml
|  |  | 23.4.3 Build den Build
|  |  | 23.4.4 Properties
|  |  | 23.4.5 Externe und vordefinierte Properties
|  |  | 23.4.6 Weitere Ant-Tasks
|  | 23.5 Disassembler, Decompiler und Obfuscator
|  |  | 23.5.1 Der Diassembler javap
|  |  | 23.5.2 Decompiler
|  |  | 23.5.3 Obfuscatoren
|  | 23.6 Weitere Dienstprogramme
|  |  | 23.6.1 Sourcecode Beautifier
|  |  | 23.6.2 Java-Programme als Systemdienst ausführen
|  | 23.7 Zum Weiterlesen
## 23.5 Disassembler, Decompiler und Obfuscator Ein Disassembler ist ein Werkzeug, das den Bytecode und die Struktur einer Java-Klassendatei anzeigt. Ein Decompiler geht einen Schritt weiter und versucht aus Klassendateien wieder Quellcodedateien zu gewinnen, die, wenn sie später compiliert werden, wieder den gleichen Bytecode ergeben würden. Genau diese »leichte« Lesbarkeit von Bytecode oder eine Decompliation möchte ein anderes Werkzeug, der Obfuscator, erschweren. Das Ziel von Obfuscation ist das möglichst effektive Verschleiern aller Bytecodeinformationen, sodass Menschen den Spaß am Bytecode verlieren und Decompiler bei ihrer Rücktransformation sogar aus dem Tritt kommen. Das Eclipse-Plugin ByteCodeDeluxe unter http://www.idedeluxe.com/bytecode/ visualisiert die Struktur einer Bytecode-Datei. ### 23.5.1 Der Diassembler javap Das JDK liefert im bin-Verzeichnis der Installation mit javap ein Werkzeug aus, das zwar nicht die Implementierung von Methoden hervorzaubert, aber immerhin die statische Struktur einer Klasse mit den Vererbungsbeziehungen, Variablen, Methoden, Parametern anzeigt. In der einfachsten Variante wird javap mit dem Klassennamen aufgerufen. Wir nehmen im Folgenden an, dass javap im Suchpfad steht und wir uns auf der Kommandozeile direkt im bin-Verzeichnis vom JDK befinden. Wir setzen zuerst den Klassenpfad und geben anschließend als Kommandozeilenargument für javap die Klasse Quadrat aus dem ersten Kapitel an, die disassembliert werden soll.
    $ javap -classpath Z:\Documents\Insel\programme\1_01_Intro Quadrat
    Compiled from "Quadrat.java"
    public class Quadrat extends java.lang.Object{
        public Quadrat();
        static int quadrat(int);
        static void ausgabe(int);
        public static void main(java.lang.String[]);
    }
Abzulesen sind auch Dinge, die der Compiler automatisch generiert und die im Bytecode stehen, die wir aber im Allgemeinen nicht schreiben würden, etwa der voll qualifizierte Klassenname java.lang.Object oder java.lang.String, die Vererbungsbeziehung zu Object oder der automatisch angelegte Standardkonstruktor. Das Tool javap erlaubt noch mehr Parameter, die die Option -help anzeigt.
    $ javap -help
    Usage: javap <options> <classes>...

    where options include:
       -c                        Disassemble the code
       -classpath <pathlist>     Specify where to find user class files
       -extdirs <dirs>           Override location of installed extensions
       -help                     Print this usage message
       -J<flag>                  Pass <flag> directly to the runtime system
       -l                        Print line number and local variable tables
       -public                   Show only public classes and members
       -protected                Show protected/public classes and members
       -package                  Show package/protected/public classes
                                 and members (default)
       -private                  Show all classes and members
       -s                        Print internal type signatures
       -bootclasspath <pathlist> Override location of class files loaded
                                 by the bootstrap class loader
       -verbose                  Print stack size, number of locals and args for methods
                                 If verifying, print reasons for failure
Hinweis
Auch der Java-Compiler kann seit Java 7 mit dem neuen Schalter –Xprint ähnliche Informationen ausgeben:
    $ javac.exe -Xprint Quadrat.java
    public class Quadrat {
      public Quadrat();
      static int quadrat(int n);
      static void ausgabe(int n);
      public static void main(java.lang.String[] args);
    }
#### Java-Bytecode am Beispiel Interessanter ist der Schalter -c, der den Bytecode der Methoden/Konstruktoren/Initialisierer anzeigt. Am Beispiel:
    $ javap -c -classpath Z:\Documents\Insel\programme\1_01_Intro Quadrat
    Compiled from "Quadrat.java"
    public class Quadrat extends java.lang.Object{
Steigen wir nicht chronologisch ein, sondern bei der statischen Methode quadrat(). Sie bekommt als Argument ein int und liefert es multipliziert mit sich selbst zurück:
    static int quadrat(int);
      Code:
       0:   iload_0
       1:   iload_0
       2:   imul
       3:   ireturn
Die Ausgabe macht die Stack-Natur des JVM sichtbar. Der Übergabeparameter n von quadrat(int n) steht auf Position 0 im Stack. Um das Quadrat zu bilden, wird der aktuelle Parameter zweimal mit iload_0 auf den Stapel gelegt und dann mit imul multipliziert. imul löscht die beiden Werte vom Stapel und ersetzt sie durch das Ergebnis der Multiplikation. ireturn liefert den obersten Stackwert als int zurück. Das Präfix »i« bei imul und ireturn zeigt, dass die Operationen auf Ganzzahlen durchgeführt werden. Andere Präfixe sind »b« für byte, »c« für char, »d« für double oder »a« für Objektreferenzen. Kommen wir zur statischen main-Methode:
    public static void main(java.lang.String[]);
      Code:
       0:   iconst_4
       1:   invokestatic    #59; //Method ausgabe:(I)V
       4:   return
    }
Im Rumpf der main()-Methode steht der Aufruf ausgabe(4), was im Bytecode dazu führt, dass mit iconst_4 der Wert 4 auf den Stack gelegt wird und dann invokestatic die Methode ausgabe() aufruft. Der Standardkonstruktor von Quadrat ruft lediglich den Standardkonstruktor der Oberklasse Object auf.
    public Quadrat();
      Code:
       0:   aload_0
       1:   invokespecial   #8; //Method java/lang/Object."<init>":()V
       4:   return
Bei Konstruktoren und Objektmethoden ist automatisch an Position 0 im Stack die this-Referenz gesetzt, da die Aufrufer automatisch an Position 0 diese Referenz übergeben. (Bei statischen Methoden ist das nicht so.) Bei unserem Konstruktor Quadrat() setzt also aload_0 diese this-Referenz auf den Stack, damit der Konstruktor der Oberklasse, also Object aufgerufen werden kann. Konstrukturaufrufe werden im Bytecode mit invokespecial durchgeführt. Im Kern ist ein Konstruktor eine Methode mit dem speziellen Namen <init> und der Rückgabe void. Die Angabe #8 ist ein Verweis auf eine interne Tabelle, doch der Java-Kommentar macht den Eintrag für uns lesbar. Am komplexesten ist die statische Methode ausgabe(). Im Original sieht sie so aus:
    String s;
    int    i;
    for ( i = 1; i <= n; i = i + 1 )
    {
      s = "Quadrat(" + i  + ") = " + quadrat(i);
      System.out.println( s );
    }
Parameter und lokale Variablen werden auf dem Stack gespeichert und haben intern nur Positionen.

Tabelle 23.4: Variablen im Beispiel und Stack-Positionen

Variable | Positionen der Parameter/Variablen
n | 0
s | 1
i | 2
Der erste Ausschnitt ohne Schleifenrumpf sieht so aus:
    static void ausgabe(int);
      Code:
       0:   iconst_1
       1:   istore_2
       2:   goto    44
       5:   ...
       ...
       41:  iinc    2, 1
       44:  iload_2
       45:  iload_0
       46:  if_icmple       5
       49:  return
Unsere for-Schleife beginnt mit 1, und so setzt iconst_1 den Wert 1 auf den Stack, und istore_2 speichert den Wert für die Variable i auf dem Stack. Da for-Schleifen kopfgesteuerte Schleifen sind, also erst testen, bevor sie den Rumpf durchlaufen, springt goto – im Bytecode gibt es diesen Sprungbefehl – über den Schleifenrumpf zum Test. iload_2 lädt die Variable i und vergleicht sie mit n, der Parametervariablen auf Position 0 auf dem Stack. Ist i < n, dann springt if_icmple (für engl. integer compare less equal) zur Position 5, was den Schleifenrumpf ausführt. In der Zeile 41 inkrementiert iinc 2, 1 die Variable i (2 steht hier für den Index der Variablen, denn die Variable i steht auf Platz 2 des Stacks) und 1 ist das Inkrement. Der Rumpf der Schleife besteht aus zwei Teilen: einmal aus dem Zusammenhängen der Strings und einmal aus der Ausgabe auf dem Bildschirm. Zur Konkatenation müssen wir uns zurückerinnern, dass das + bei Strings zu einer Folge von append()-Aufrufen auf ein StringBuilder (vor Java 5 StringBuffer) wird.
     5:   new     #20; //class java/lang/StringBuilder
     8:   dup
     9:   ldc     #22; //String Quadrat(
    11:  invokespecial   #24; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
    14:  iload_2
    15:  invokevirtual   #27; //Method java/lang/StringBuilder.append:(I)Ljava/lang/
    StringBuilder;
    18:  ldc     #31; //String ) =
    20:  invokevirtual   #33; //Method java/lang/StringBuilder.append:(Ljava/lang/
    String;)Ljava/lang/StringBuilder;
    23:  iload_2
    24:  invokestatic    #36; //Method quadrat:(I)I
    27:  invokevirtual   #27; //Method java/lang/StringBuilder.append:(I)Ljava/lang/
    StringBuilder;
    30:  invokevirtual   #38; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    33:  astore_1
Nach dem Ablauf steht die Referenz auf das String-Objekt auf dem Stack, und astore_1 überträgt sie auf den Stack an Position 1, der für die String-Variable s reserviert war. Es folgt die Ausgabe auf dem Bildschirm:
    34:  getstatic       #42; //Field java/lang/System.out:Ljava/io/PrintStream;
    37:  aload_1
    38:  invokevirtual   #48; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
Hier greift getstatic auf die statische Variable System.out zu und setzt die Referenz auf den aktuellen PrintStream auf den Stack. Dann setzt aload_1 die Referenz vom String s auf den Stack, und invokevirtual ruft println() aus. Dieses Beispiel gibt einen kleinen Einblick in den Bytecode von Java. Weitere Informationen gibt »The Java Virtual Machine Specification« und insbesondere das Kapitel »Compiling for the Java Virtual Machine« unter http://java.sun.com/docs/books/jvms/second_edition/html/Compiling.doc.html. Normale Programmierer müssen Bytecode nicht lesen und schreiben können, aber das Wissen ist nützlich und auch nötig, wenn Bytecode selbst erstellt werden soll.
Interna
Wichtig zu wissen ist, dass das Programm im Bytecode keine strikte Ablaufanweisung für die JVM ist. Java-Bytecode ist nur das »Transportformat«, in dem Programmcode zur JVM gelangt. Die JVM wiederum liest den Bytecode ein und transformiert ihn in hochoptimierten Maschinencode. Eine Interpretation gibt es zwar noch am Anfang der Ausführung, aber nach ein paar Durchläufen beginnt HotSpot mit der Übersetzung in Maschinencode, und da fallen diverse Operationen aus dem Bytecode heraus.
### 23.5.2 Decompiler Der Java-Compiler erzeugt aus der Quellcodedatei eine Klassendatei, und der Decompiler dreht die Arbeitsweise um. Decompiler sind Werkzeuge zum Reverse Engineering, bei dem es darum geht, aus einer fertigen Software, die nur etwa in Form von Jar-Dateien vorliegt, die Java-Quellen und Ressourcen zurückzugewinnen. Decompiler gibt es für die verschiedenen Programmiersprachen, und Java gehört zu den Sprachen, bei denen die Zurückübersetzung einfacher ist als bei optimierten Maschinenprogrammen, die zum Beispiel ein C++-Compiler erzeugt. Der Grund ist, dass im Bytecode viele wertvolle Informationen enthalten sind, die im herkömmlichen Maschinencode nicht auftauchen. Darunter sind etwa Typinformationen oder Hinweise, ob ein Methodenaufruf virtuell ist oder nicht. Sie sind für die Java-Laufzeitumgebung wichtig und eine große Hilfe, wenn es darum geht, mit einem Decompiler verlorenen Quellcode wiederzubeleben oder an fehlende Informationen aus Paketen von Fremdherstellern zu gelangen. Ein Decompiler liest die Klassendatei als Bytefeld ein und beginnt mit der Analyse. Da der Bytecode gut dokumentiert ist, ist das Extrahieren von Variablen- oder Methodennamen einfach. Schwierig sind die Anweisungen. Aus dem Java-Bytecode für eine Methode baut ein Decompiler einen Kontrollfluss-Graphen auf und versucht, Anweisungen und Ausdrücke zu erkennen, die bei der Übersetzung bestimmter Sprachkonstrukte entstanden sein müssten. Das ist eine nicht-triviale Aufgabe und immer noch Gegenstand einiger Diplomarbeiten. Und da Variablennamen durch einen Obfuscator eventuell ungültig gemacht worden sind, muss ein guter Decompiler diese illegalen Bezeichnernamen korrigieren und weitere Tricksereien des Obfuscators rückgängig machen. Diese Umbenennung ändert den Algorithmus nicht, und ein Decompiler hat es bei dieser Art von Verschleierung einfach.
Ist das legal?
Wenn wir einen Decompiler auf fremden Programmcode loslassen, kann das ein rechtliches Problem darstellen. Das Reverse Engineering von vollständigen, unter Urheberschutz stehenden Anwendungen muss nicht unbedingt ein juristisches Nachspiel haben. Vielmehr beginnt die Straftat, wenn dieser zurückgewonnene Quelltext verändert und als Eigenleistung verkauft wird.
Da mittlerweile auch andere Compiler auf dem Markt sind, die Java-Bytecode erzeugen – etwa aus EIFFEL-Programmen oder aus diversen LISP-Dialekten –, ist über den Umweg Compiler/Klassendatei/Decompiler ein Crosscompiling denkbar. Hier sind jedoch einige Einschränkungen bezüglich der auf dem Markt befindlichen Decompiler erkennbar. Denn fremde Compiler, die Java-Bytecode erstellen, haben andere Techniken, die der Decompiler dann nicht immer passend übersetzen kann. #### Java Decompiler Project (JD) und Alternativen Das Angebot an leistungsstarken Decompilatoren ist sehr übersichtlich. Das beste Tool (aber auch nicht ganz fehlerfrei) ist zurzeit JD (http://java.decompiler.free.fr/). Das frei verfügbare – aber nicht quelloffene – Programm ist als Bibliothek JD-Core, als alleinstehende grafische Anwendung JD-GUI und Eclipse-Plugin[121](Für NetBeans und IntelliJ IDEA gibt es Initiativen von anderer Seite.) JD-Eclipse verfügbar. JD selbst ist in C++ geschrieben und benötigt daher keine JVM. JD verarbeitet den Bytecode verschiedener Compiler, wobei das JDK 1.1 bis (bisher) JDK 6 selbstverständlich mit in der Liste ist, genauso wie der Eclipse-Compiler. (Die Unterscheidung ist nicht ganz uninteressant, da die Compiler sich in machen Details in der Bytecode-Abbildung doch unterscheiden.) JD-GUI ist für die Plattformen Windows, Linux und Mac unter http://java.decompiler.free.fr/?q=jdgui#downloads verfügbar und bietet neben dem Decompilieren einzelner Java-Klassen und ganzer Java-Archive eine angenehme Quellcodedarstellung mit farblicher Unterlegung und Drag & Drop. Sehr lange war der Decompiler Jad die Referenz. Doch nur von 1997 bis 2001 hat Pavel Kouznetsov das Kommandozeilenprogramm in C++ entwickelt, und dann hat er 2009 seine Webseite vom Netz genommen. Eine Privatperson hat jedoch die Webseite gespiegelt, und unter http://www.varaneckas.com/jad lebt das Projekt (auf unbestimmte Zeit) weiter. Wer Projekte bis Java 1.4 decompilieren möchte, ist mit dem Tool sehr gut bedient. Für Java 5-Projekte hilft JadRetro (http://jadretro.sourceforge.net/) noch ein wenig nach, indem es Java 5-Bytecode auf Java 1.4 anpasst und kleine Änderungen im Bytecode durchführt. FrontEnd Plus ist eine grafische Oberfläche für Jad, doch auch sie ist aus dem Internet verschwunden, seitdem es Jad nicht mehr offiziell gibt. Abbildung 23.7: Decompilation der Quadrat-Klasse mit JD-GUI ### 23.5.3 Obfuscatoren Die Existenz eines Disassemblers wie javap und eines Decompilers wie JD verunsichert Hersteller, da diese in der Regel nicht möchten, dass ihr Quellcode untersucht wird und vielleicht neu zusammengesetzt den Weg zurück in den Markt findet. Ein Obfuscator ist ein Werkzeug, das diverse Transformationen am Bytecode vornimmt, um eine einfache Zurückverwandlung zu verhindern. Es bieten sich eine Reihe von Versteck-Aktionen an: das Löschen von Debug-Informationen das Umbenennen aller Bezeichner für Pakete, Klassen, Methoden und Variablen. Klassen heißen dann zum Beispiel C1, C2, ... und Methoden m1, m2, ... das Benennen von Bezeichnern mit Java-Schlüsselwörtern oder nicht in Java erlaubten Zeichenfolgen das Verlegen von Anweisungen oder Anweisungsfolgen in Methoden (also die Umkehroperation zum Methoden-Inline) das Berechnen von Konstanten, sodass etwa 1 in println(1); dynamisch durch int abcde = 2; println(abcde-1); berechnet wird das Einführen leerer Anweisungen, die zur Laufzeit wegoptimiert werden, aber den Bytecode füllen, etwa if ( 1 != 1) { int a = 123334; } das dynamische Entschlüsseln von Strings, sodass etwa aus println("google") ein println($("tokepa")) wird; die Methode $() entschlüsselt »topeka« zurück zu »google«. das Umsortieren oder das Einfügen unsinniger Bytecode-Folgen, sodass sie nicht mehr dem Muster entsprechen, die ein Java-Compiler erzeugt und auch nicht so einfach vom Decompiler wiedererkannt werden.[122](Ideen beschreibt das Paper »Protection Methods of Java Bytecode« unter http://tutego.de/go/bytecodeprotection.)
Effektivität
Ein Obfuscator verhindert das einfache Zurückverwandeln in Java-Quellcode, kann aber niemals verhindern, dass javap den Bytecode offenbart. Ein Obfuscator kann somit nur Dinge möglichst unleserlich machen, aber wirklich geschützt ist das Programm nicht. Dennoch lohnt sich der Einsatz eines Obfuscators, denn er schreckt ab und verdirbt die Lust am Schnüffeln, denn Klassennamen von C1 bis C3484 mit Methoden von m1 bis m12743 verraten erst einmal überhaupt nichts. Noch besserer Schutz ist nur mit einem eigenen Klassenlader zu erreichen, der verschlüsselten Bytecode einliest. Hier muss dann die JVM gepatcht werden, um an diese Klassendateien zu kommen. Wirklich unmöglich ist das auch nicht.
#### Das Obfuscator-Programm ProGuard ProGuard (http://proguard.sf.net/) ist ein Open-Source-Projekt unter der GPL-Lizenz,[123](Obwohl ProGuard selbst unter der GPL steht, darf die Software auch auf Software angewendet werden, die nicht unter der GPL steht.) das Java-Klassen und Jar-Archive verschleiert. Neben der Verschleierung hat es den Nebeneffekt, dass die Klassendateien kleiner werden, was insbesondere für Applets und Midlets (mobile Anwendungen) interessant ist. ProGuard 3 bietet Unterstützung für das Bytecode-Format von Java 5 und Java ME – ProGuard 4 bietet Obfuscation für Java 6. Die Software lässt sich über eine Swing-Oberfläche oder über die Kommandozeile bedienen oder aber auch über ein Ant-Skript steuern. Die Webseite nennt ein Beispiel, das alle Typen bis auf Applets eines Java-Archivs berücksichtigt. Auf der Kommandozeile ist Folgendes anzugeben:
    $ java -jar proguard.jar –injars in.jar -outjars out.jar
    –libraryjars $JAVA_HOME/lib/rt.jarkeep public class * extends java.applet.Applet
Einige Dinge dürfen nicht umbenannt werden, sodass sie von ProGuard ausgenommen werden müssen. Das betrifft etwa bestimmte Klassen, die dynamisch geladen werden, wie Plugins, wo es auf den genauen Paket- und Klassennamen ankommt. Unter http://www.certiv.net/projects/proguarddt.html gibt es ein Plugin, das die ProGuard-Konfigurationen in Eclipse editieren und ProGuard über ein Menü komfortabel aufrufen kann. ### Ihr Kommentar Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen. >> Zum Feedback-Formular
<< zurück | <top> | vor >>
Zum Katalog
Java 7 – Mehr als eine Insel Jetzt bestellen
Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen? Ihre Meinung
Buchempfehlungen
| Java und XML
| Einstieg in Eclipse 3.7
| Android 3
| NetBeans Platform 7
| Java ist auch eine Insel
| Apps entwickeln für Android 4
| Java 7
Shopping
Versandkostenfrei bestellen in Deutschland und Österreich Info

Copyright © Galileo Press 2012
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

[Galileo Computing]
Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de