Java Language
バイトコードの変更
サーチ…
バイトコードとは何ですか?
バイトコードは、JVMで使用される一連の命令です。これを説明するために、このHello Worldプログラムを利用しましょう。
public static void main(String[] args){
System.out.println("Hello World");
}
これは、バイトコードにコンパイルされたときに変わります。
public static main([Ljava/lang/String; args)V
getstatic java/lang/System out Ljava/io/PrintStream;
ldc "Hello World"
invokevirtual java/io/PrintStream print(Ljava/lang/String;)V
これの背後にある論理は何ですか?
getstatic - クラスの静的フィールドの値を取得します。この場合、 PrintStreamはシステムの "Out"になります 。
ldc - 定数をスタックにプッシュします。この場合、文字列 "Hello World"
invokevirtual - スタック上のロードされた参照のメソッドを呼び出し、その結果をスタックに格納します。メソッドのパラメータもスタックから取得されます。
まあ、もっと正しいことがありますか?
255のオペコードがありますが、それらのすべてがまだ実装されていません。現在のオペコードをすべて含むテーブルがここにあります: Javaバイトコード命令リスト 。
バイトコードの書き込み/編集はどうすればできますか?
バイトコードの書き込みと編集には複数の方法があります。コンパイラの使用、ライブラリの使用、プログラムの使用が可能です。
書くため:
編集のため:
- 図書館
- ツール
- バイトコード - ビューア
- JBytedit
- reJ - Java 8+をサポートしていません
- JBE - Java 8+をサポートしていません
私はバイトコードについてもっと学びたいと思っています!
たぶん、バイトコードのための具体的なドキュメンテーションページがあります。このページでは、さまざまなライブラリとツールを使用してバイトコードを変更する方法について説明します。
ASMでjarファイルを編集する方法
まず、瓶からのクラスをロードする必要があります。このプロセスには3つの方法があります:
- loadClasses(File)
- readJar(JarFile、JarEntry、Map)
- getNode(byte [])
Map<String, ClassNode> loadClasses(File jarFile) throws IOException {
Map<String, ClassNode> classes = new HashMap<String, ClassNode>();
JarFile jar = new JarFile(jarFile);
Stream<JarEntry> str = jar.stream();
str.forEach(z -> readJar(jar, z, classes));
jar.close();
return classes;
}
Map<String, ClassNode> readJar(JarFile jar, JarEntry entry, Map<String, ClassNode> classes) {
String name = entry.getName();
try (InputStream jis = jar.getInputStream(entry)){
if (name.endsWith(".class")) {
byte[] bytes = IOUtils.toByteArray(jis);
String cafebabe = String.format("%02X%02X%02X%02X", bytes[0], bytes[1], bytes[2], bytes[3]);
if (!cafebabe.toLowerCase().equals("cafebabe")) {
// This class doesn't have a valid magic
return classes;
}
try {
ClassNode cn = getNode(bytes);
classes.put(cn.name, cn);
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
return classes;
}
ClassNode getNode(byte[] bytes) {
ClassReader cr = new ClassReader(bytes);
ClassNode cn = new ClassNode();
try {
cr.accept(cn, ClassReader.EXPAND_FRAMES);
} catch (Exception e) {
e.printStackTrace();
}
cr = null;
return cn;
}
これらのメソッドを使用すると、jarファイルの読み込みと変更はマップ内のClassNodesを変更する簡単な問題になります。この例では、Tree APIを使用して、jarのすべてのStringを大文字のStringに置き換えます。
File jarFile = new File("sample.jar");
Map<String, ClassNode> nodes = loadClasses(jarFile);
// Iterate ClassNodes
for (ClassNode cn : nodes.values()){
// Iterate methods in class
for (MethodNode mn : cn.methods){
// Iterate instructions in method
for (AbstractInsnNode ain : mn.instructions.toArray()){
// If the instruction is loading a constant value
if (ain.getOpcode() == Opcodes.LDC){
// Cast current instruction to Ldc
// If the constant is a string then capitalize it.
LdcInsnNode ldc = (LdcInsnNode) ain;
if (ldc.cst instanceof String){
ldc.cst = ldc.cst.toString().toUpperCase();
}
}
}
}
}
すべてのClassNodeの文字列が変更されたので、変更を保存する必要があります。変更を保存して作業結果を出力するには、いくつかのことを行う必要があります。
- ClassNodesをバイトにエクスポートする
- 非クラスのjarエントリ(例:Manifest.mf / jarのその他のバイナリリソース)をバイトとしてロードする
- すべてのバイトを新しいjarに保存する
上の最後の部分から、3つのメソッドを作成します。
- processNodes(Map <String、ClassNode> nodes)
- loadNonClasses(File jarFile)
- saveAsJar(Map <String、byte []> outBytes、String fileName)
使用法:
Map<String, byte[]> out = process(nodes, new HashMap<String, MappedClass>());
out.putAll(loadNonClassEntries(jarFile));
saveAsJar(out, "sample-edit.jar");
使用される方法:
static Map<String, byte[]> processNodes(Map<String, ClassNode> nodes, Map<String, MappedClass> mappings) {
Map<String, byte[]> out = new HashMap<String, byte[]>();
// Iterate nodes and add them to the map of <Class names , Class bytes>
// Using Compute_Frames ensures that stack-frames will be re-calculated automatically
for (ClassNode cn : nodes.values()) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
out.put(mappings.containsKey(cn.name) ? mappings.get(cn.name).getNewName() : cn.name, cw.toByteArray());
}
return out;
}
static Map<String, byte[]> loadNonClasses(File jarFile) throws IOException {
Map<String, byte[]> entries = new HashMap<String, byte[]>();
ZipInputStream jis = new ZipInputStream(new FileInputStream(jarFile));
ZipEntry entry;
// Iterate all entries
while ((entry = jis.getNextEntry()) != null) {
try {
String name = entry.getName();
if (!name.endsWith(".class") && !entry.isDirectory()) {
// Apache Commons - byte[] toByteArray(InputStream input)
//
// Add each entry to the map <Entry name , Entry bytes>
byte[] bytes = IOUtils.toByteArray(jis);
entries.put(name, bytes);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
jis.closeEntry();
}
}
jis.close();
return entries;
}
static void saveAsJar(Map<String, byte[]> outBytes, String fileName) {
try {
// Create jar output stream
JarOutputStream out = new JarOutputStream(new FileOutputStream(fileName));
// For each entry in the map, save the bytes
for (String entry : outBytes.keySet()) {
// Appent class names to class entries
String ext = entry.contains(".") ? "" : ".class";
out.putNextEntry(new ZipEntry(entry + ext));
out.write(outBytes.get(entry));
out.closeEntry();
}
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
それでおしまい。すべての変更は "sample-edit.jar"に保存されます。
ClassNodeをクラスとしてロードする方法
/**
* Load a class by from a ClassNode
*
* @param cn
* ClassNode to load
* @return
*/
public static Class<?> load(ClassNode cn) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
return new ClassDefiner(ClassLoader.getSystemClassLoader()).get(cn.name.replace("/", "."), cw.toByteArray());
}
/**
* Classloader that loads a class from bytes.
*/
static class ClassDefiner extends ClassLoader {
public ClassDefiner(ClassLoader parent) {
super(parent);
}
public Class<?> get(String name, byte[] bytes) {
Class<?> c = defineClass(name, bytes, 0, bytes.length);
resolveClass(c);
return c;
}
}
jarファイル内のクラスの名前を変更する方法
public static void main(String[] args) throws Exception {
File jarFile = new File("Input.jar");
Map<String, ClassNode> nodes = JarUtils.loadClasses(jarFile);
Map<String, byte[]> out = JarUtils.loadNonClassEntries(jarFile);
Map<String, String> mappings = new HashMap<String, String>();
mappings.put("me/example/ExampleClass", "me/example/ExampleRenamed");
out.putAll(process(nodes, mappings));
JarUtils.saveAsJar(out, "Input-new.jar");
}
static Map<String, byte[]> process(Map<String, ClassNode> nodes, Map<String, String> mappings) {
Map<String, byte[]> out = new HashMap<String, byte[]>();
Remapper mapper = new SimpleRemapper(mappings);
for (ClassNode cn : nodes.values()) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
ClassVisitor remapper = new ClassRemapper(cw, mapper);
cn.accept(remapper);
out.put(mappings.containsKey(cn.name) ? mappings.get(cn.name) : cn.name, cw.toByteArray());
}
return out;
}
SimpleRemapperは、ASMライブラリの既存のクラスです。ただし、クラス名の変更のみが可能です。フィールドとメソッドの名前を変更する場合は、Remapperクラスの独自の実装を作成する必要があります。
Javassist Basic
Javassistはバイトコード計測ライブラリで、Javassistによってバイトコードに変換され、実行時にインストルメントされたクラス/メソッドに追加されるJavaコードを注入するバイトコードを変更できるようにします。
実際に仮想クラス "com.my.to.be.instrumented.MyClass"を取る最初のトランスフォーマーを記述し、各メソッドの命令にログ呼び出しを追加します。
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
public class DynamicTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
byte[] byteCode = classfileBuffer;
// into the transformer will arrive every class loaded so we filter
// to match only what we need
if (className.equals("com/my/to/be/instrumented/MyClass")) {
try {
// retrive default Javassist class pool
ClassPool cp = ClassPool.getDefault();
// get from the class pool our class with this qualified name
CtClass cc = cp.get("com.my.to.be.instrumented.MyClass");
// get all the methods of the retrieved class
CtMethod[] methods = cc.getDeclaredMethods()
for(CtMethod meth : methods) {
// The instrumentation code to be returned and injected
final StringBuffer buffer = new StringBuffer();
String name = meth.getName();
// just print into the buffer a log for example
buffer.append("System.out.println(\"Method " + name + " executed\" );");
meth.insertBefore(buffer.toString())
}
// create the byteclode of the class
byteCode = cc.toBytecode();
// remove the CtClass from the ClassPool
cc.detach();
} catch (Exception ex) {
ex.printStackTrace();
}
}
return byteCode;
}
}
このトランスフォーマーを使用するには(ロード時にJVMが各クラスのメソッド変換を呼び出すように)、このインストーラにエージェントを追加する必要があります。
import java.lang.instrument.Instrumentation;
public class EasyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
// registers the transformer
inst.addTransformer(new DynamicTransformer());
}
}
最初の計測器実験を開始する最後のステップは、このエージェントクラスを実際にJVMマシンの実行に登録することです。実際に行う最も簡単な方法は、シェルコマンドにオプションで登録することです。
java -javaagent:myAgent.jar MyJavaApplication
私たちが見ることができるように、エージェント/トランスフォーマープロジェクトは、実際に私たちの注入されたコードを実行するために "com.my.to.be.instrumented.MyClass"という名前のクラスを含むはずのMyJavaApplicationという名前のアプリケーションの実行にjarとして追加されます。