2017-10-07 3 views
1

Ich habe eine alte Bibliothek (ca. 2005), die Byte-Code-Manipulation durchführt, aber nicht die Stackmap berührt. Folglich beschwert sich mein jvm (java 8), dass sie ungültige Klassen sind. Die einzige Möglichkeit, die Fehler zu umgehen, besteht darin, den jvm mit -noverify auszuführen. Aber das ist keine langfristige Lösung für mich.Gibt es eine Möglichkeit, stackmap aus Byte-Code zu regenerieren?

Gibt es eine Möglichkeit, die Stapelkarte neu zu generieren, nachdem die Klassen bereits generiert wurden? Ich sah die Klasse ClassWriter hatte eine Option, um die Stack-Map neu zu generieren, aber ich bin mir nicht sicher, wie in einer Byte-Klasse gelesen und neu geschrieben werden. Ist das machbar?

Antwort

2

Wenn Sie alte Klassen instrumentieren, die keine Stackmaps haben, und ihre alte Versionsnummer beibehalten, wird es kein Problem geben, da sie von der JVM wie zuvor verarbeitet werden und keine Stackmaps benötigen. Dies bedeutet natürlich, dass Sie keine neueren Bytecode-Funktionen einfügen können.

Wenn Sie neuere Klassendateien mit gültigen Stackmaps vor der Umwandlung instrumentieren, werden diese Probleme nicht auftreten described by Antimony. So können Sie ASM verwenden, um stackmaps zu regenerieren:

byte[] bytecode = … // result of your instrumentation 
ClassReader cr = new ClassReader(bytecode); 
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); 
cr.accept(cw, ClassReader.SKIP_FRAMES); 
bytecode = cw.toByteArray(); // with recalculated stack maps 

Der Besucher API entwickelt wurde, mit einem Schriftsteller einfache Kaskadierung eines Lesers zu ermöglichen und nur Code fügen Sie diese Artefakte abfangen Sie ändern möchten.

Da wir wissen, dass wir die Stackmap Frames von Grund auf mit ClassWriter.COMPUTE_FRAMES neu generieren werden, können wir ClassReader.SKIP_FRAMES an den Leser übergeben, um es zu sagen, die Quellframes nicht zu verarbeiten, die wir sowieso ignorieren werden.

Es ist eine weitere Optimierung möglich, wenn wir wissen, dass sich die Klassenstruktur nicht ändert. Wir können den ClassReader an den Konstruktor ClassWriter übergeben, um einen Vorteil von der unveränderten Struktur, z. Der Zielkonstantenpool wird mit einer Kopie des Quellkonstantenpools initialisiert. Diese Option muss jedoch mit Vorsicht behandelt werden. Wenn wir Methoden überhaupt nicht abfangen, wird es ebenfalls optimiert, d. H. Der Code wird vollständig kopiert, ohne die Stapelrahmen neu zu berechnen. Also brauchen wir einen Besucher benutzerdefinierte Methode, so zu tun, dass der Code möglicherweise ändern könnte:

byte[] bytecode = … // result of your instrumentation 
ClassReader cr = new ClassReader(bytecode); 
// passing cr to ClassWriter to enable optimizations 
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES); 
cr.accept(new ClassVisitor(Opcodes.ASM5, cw) { 
    @Override 
    public MethodVisitor visitMethod(int access, String name, String desc, 
            String signature, String[] exceptions) { 
     MethodVisitor writer=super.visitMethod(access, name, desc, signature, exceptions); 
     return new MethodVisitor(Opcodes.ASM5, writer) { 
      // not changing anything, just preventing code specific optimizations 
     }; 
    } 
}, ClassReader.SKIP_FRAMES); 
bytecode = cw.toByteArray(); // with recalculated stack maps 

So wie der Konstanten-Pool unverändert Artefakte kann direkt an die Ziel Bytecode kopiert werden, während die stackmap Rahmen noch neu berechnet bekommen.

Es gibt jedoch einige Vorbehalte. Stackmaps von Grund auf neu zu erstellen bedeutet, dass kein Wissen über die ursprüngliche Codestruktur oder die Art der Transformation verwendet wird. Z.B. Ein Compiler würde die formalen Typen von lokalen Variablendeklarationen kennen, wohingegen der ClassWriter verschiedene tatsächliche Typen sehen könnte, für die er den gemeinsamen Basistyp finden muss. Diese Suche kann sehr teuer sein, weil das Laden von Klassen, die zurückgestellt wurden oder nicht einmal während der normalen Ausführung verwendet werden. Der resultierende Typ kann sich sogar von dem allgemeinen Typ unterscheiden, der im ursprünglichen Code deklariert wurde. Es wird ein korrekter Typ sein, aber die Verwendung von Klassen im resultierenden Code kann wieder geändert werden.

Wenn Sie die Instrumentierung in einer anderen Umgebung ausführen, können ASMs Versuche, die Klassen zum Ermitteln des allgemeinen Typs zu laden, fehlschlagen. Dann müssen Sie ClassWriter.getCommonSuperClass(…) mit einer Implementierung überschreiben, die den Vorgang in dieser Umgebung ausführen kann. Dies ist auch der Ort, um Optimierungen hinzuzufügen, wenn Sie mehr über den Code wissen und ohne teure Suchen durch die Typhierarchie Antworten geben können.

Im Allgemeinen wird empfohlen, die alte Bibliothek so zu refaktorieren, dass ASM anstelle eines nachfolgenden Anpassungsschritts verwendet werden kann. Wie oben erläutert, kann ASM bei der Codetransformation mit einer Kette von ClassReader und ClassWriter mit aktivierten Optimierungen alle unveränderten Methoden einschließlich ihrer Stackmaps kopieren und nur die Stackmaps der tatsächlich geänderten Methoden neu berechnen. In dem obigen Code, der die Neuberechnung in einem nachfolgenden Schritt durchführt, mussten wir die Optimierung deaktivieren, da wir nicht mehr wissen, welche Methoden tatsächlich geändert wurden.

Der nächste logische Schritt wäre es, Stackmap-Handling in die Instrumentierung zu integrieren, da das Wissen über die eigentliche Transformation mehr als 99% der bestehenden Frames behalten und die anderen einfach anpassen kann, anstatt eine teure Neuberechnung zu benötigen kratzen.

1

Soweit wie in der Klasse zu lesen, sollten Sie in der Lage sein, nur eine ClassReader verwenden.

Die allgemeinere Frage nach der Möglichkeit, Stapelkarten automatisch zu alten Klassen hinzuzufügen, ist in den meisten Fällen möglich. Es gibt jedoch einige obskure Fälle, in denen dies nicht möglich wäre, hauptsächlich aufgrund der Tatsache, dass der Inferenzverifizierer laxer ist als der Stackmap-Verifizierer. Beachten Sie, dass diese nur für den Fall gelten, dass eine Stack-Map zu einem alten Code hinzugefügt wird, der nie einen hat. Wenn Sie vorhandenen Java 8-Code ändern, können Sie all dies ignorieren.

Zuerst sind die jsr und ret Anweisungen, die nur in classfiles Version < = 49 (entsprechend Java 5) erlaubt sind. Wenn Sie Code mit diesen Ports portieren möchten, müssten Sie den Code neu schreiben, um alle Unterroutinenkörper zu duplizieren und zu inline zu schreiben.

Abgesehen davon gibt es kleinere Probleme. Mit dem Inferenzverifizierer können Sie beispielsweise Boolesche und Byte-Arrays (die vom Verifizierer als identisch angesehen werden) frei mischen, aber der Stackmap-Verifizierer behandelt sie als unterschiedliche Typen. Ein anderes mögliches Problem ist, dass bei der Inferenz-Verifikation toter Code überhaupt nicht überprüft wird, während der Stackmap-Verifizierer immer noch verlangt, dass Sie Stack-Maps für alles festlegen. In diesem Fall ist die Fehlerbehebung einfach - löschen Sie den gesamten toten Code.

Schließlich gibt es das Problem, dass Stackmaps erfordern, dass Sie die gemeinsamen Oberklassen von Typen im Voraus angeben, wenn sie im Steuerungsfluss zusammengeführt werden, während Sie bei der Inferenzverifizierung Obertypen nicht explizit angeben müssen. Meistens spielt dies keine Rolle, da Sie eine bekannte Vererbungshierarchie haben. Es ist jedoch theoretisch möglich, von Klassen zu erben, die nur zur Laufzeit über einen ClassLoader definiert werden.

Und natürlich benötigen die stackmaps entsprechende Einträge im constant pool, was bedeutet, dass Sie im konstanten Pool für alles andere weniger Platz haben. Wenn Sie eine Klasse haben, die nahe an der maximalen konstanten Poolgröße liegt, ist das Hinzufügen einer Stapelzuordnung möglicherweise nicht möglich. Dies ist sehr selten, kann aber bei automatisch generiertem Code vorkommen.

P.S. Es besteht auch die Möglichkeit, in die andere Richtung zu gehen. Wenn Ihr Code keine Version 51.0 oder 52.0 spezifische Features (die im Grunde nur invokedynamic, alias Lambda) ist, verwenden, können Sie die Classfile-Version auf 50.0 setzen, die Notwendigkeit für eine Stapelkarte entfernen. Natürlich ist dies eine Art Rückwärtslösung und wird immer schwieriger werden, da zukünftige Classfile-Versionen attraktivere Features (wie Lambdas) hinzufügen werden.

+0

Danke für das Tutorial; Ich hatte keine Ahnung, dass es so involviert war - ich hatte gehofft, ich könnte einfach in einer Klasse lesen und sie mit einer Stackmap umschreiben.Davon abgesehen wird der Basiscode mit dem J8-Compiler kompiliert (obwohl er bestenfalls nur J6-Code ist). Es gibt dann eine Persistenz-Bibliothek, die die Byte-Erweiterung durchführt - ich glaube, es fügt Felder und Getter/Setter hinzu und schreibt die Klassendatei neu. Angesichts dessen, was Sie gesagt haben, scheint es, dass ich eine Menge Arbeit haben würde, um sicherzustellen, dass die neuen Methoden und Felder die Kriterien erfüllen. Gibt es einen einfachen Weg zu validieren, wie viel Arbeit beteiligt wäre? –

+0

Wenn ich den Code als J6-Code erzeuge/kompiliere, würde das meine Probleme lindern? Gibt es eine Möglichkeit, J7 + -Code als J6-Ziel zu kompilieren? Würde ich von der Verwendung von JEE7- oder JEE8-Konzepten eingeschränkt werden, indem ich mich auf J6-Code beschränke? Ich kann mir das nicht vorstellen, außer die JEE Apis sind für J7 + geschrieben. –

+0

@Eric B. Sie können fast sicher nur ClassReader und ClassWriter verwenden und es sollte einfach funktionieren. Alle Probleme, die ich erwähnt habe, sind theoretische Probleme, die Sie im Code der realen Welt wahrscheinlich nicht sehen werden. – Antimony

Verwandte Themen