サーチ…


バイトコードとは何ですか?

バイトコードは、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バイトコード命令リスト

バイトコードの書き込み/編集はどうすればできますか?

バイトコードの書き込みと編集には複数の方法があります。コンパイラの使用、ライブラリの使用、プログラムの使用が可能です。

書くため:

編集のため:

私はバイトコードについてもっと学びたいと思っています!

たぶん、バイトコードのための具体的なドキュメンテーションページがあります。このページでは、さまざまなライブラリとツールを使用してバイトコードを変更する方法について説明します。

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として追加されます。



Modified text is an extract of the original Stack Overflow Documentation
ライセンスを受けた CC BY-SA 3.0
所属していない Stack Overflow