Sök…


Vad är Bytecode?

Bytecode är den uppsättning instruktioner som används av JVM. För att illustrera detta låt oss ta det här Hello World-programmet.

public static void main(String[] args){
    System.out.println("Hello World");
}

Detta är vad det förvandlas till när det sammanställs till bytecode.

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

Vad är logiken bakom det här?

getatisk - Återgår värdet på ett statiskt fält i en klass. I det här fallet PrintStream "Out" av systemet .

ldc - Skjut en konstant på bunten. I detta fall strängen "Hello World"

invokevirtual - Anropar en metod på en laddad referens på bunten och sätter resultatet på bunten. Parametrar för metoden tas också från stapeln.

Det måste vara mer rätt?

Det finns 255 opcoder, men inte alla har implementerats än. En tabell med alla aktuella opcoder kan hittas här: Java bytecode instruktionslistor .

Hur kan jag skriva / redigera bytecode?

Det finns flera sätt att skriva och redigera bytecode. Du kan använda en kompilator, använda ett bibliotek eller använda ett program.

För skrivande:

För redigering:

Jag skulle vilja lära mig mer om bytecode!

Det finns förmodligen en specifik dokumentationssida specifikt för bytecode. Denna sida fokuserar på modifiering av bytecode med olika bibliotek och verktyg.

Hur man redigerar burkfiler med ASM

Först måste klasserna från burk laddas. Vi använder tre metoder för den här processen:

  • loadClasses (Fil)
  • läsJar (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;
}

Med dessa metoder blir lastning och ändring av en jarfil en enkel fråga om att ändra ClassNodes på en karta. I det här exemplet kommer vi att ersätta alla strängar i burken med stora bokstäver med hjälp av Tree API.

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();
                }
            }
        }
    }
}

Nu när alla ClassNodes strängar har ändrats måste vi spara ändringarna. För att spara ändringarna och ha en fungerande utgång måste några saker göras:

  • Exportera klassnoder till byte
  • Ladda icke-klassade burkposter (Ex: Manifest.mf / andra binära resurser i burken) som byte
  • Spara alla byte i en ny burk

Från den sista delen ovan skapar vi tre metoder.

  • processNoder (Karta <String, ClassNode> noder)
  • loadNonClasses (File jarFile)
  • saveAsJar (Karta <String, byte []> outBytes, Strängfilnamn)

Användande:

Map<String, byte[]> out = process(nodes, new HashMap<String, MappedClass>());
out.putAll(loadNonClassEntries(jarFile));
saveAsJar(out, "sample-edit.jar");

Metoderna som används:

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();
    }
}

Det är allt. Alla ändringar sparas i "sample-edit.jar".

Hur du laddar en ClassNode som en klass

/**
 * 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;
    }
}

Hur man byter namn på klasser i en jarfil

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 är en befintlig klass i ASM-biblioteket. Men det gör det bara möjligt att ändra klassnamn. Om du vill byta namn på fält och metoder bör du skapa din egen implementering av Remapper-klassen.

Javassist Basic

Javassist är ett bytekodinstrumentbibliotek som gör att du kan ändra bytekod som injicerar Java-kod som konverteras till bytekod av Javassist och läggs till den instrumenterade klassen / metoden vid körning.

Låter skriva den första transformatorn som faktiskt tar en hypotetisk klass "com.my.to.be.instrumented.MyClass" och lägger till instruktionerna för varje metod ett loggsamtal.

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;
    }
}

För att använda den här transformatorn (så att vår JVM kommer att anropa metodomvandlingen på varje klass vid belastningstid) måste vi lägga till denna instrument eller detta med en agent:

import java.lang.instrument.Instrumentation;
 
public class EasyAgent {
 
    public static void premain(String agentArgs, Instrumentation inst) {
         
        // registers the transformer
        inst.addTransformer(new DynamicTransformer());
    }
}

Det sista steget för att starta vårt första instrumentförsök är att faktiskt registrera denna agentklass till JVM-maskinutförandet. Det enklaste sättet att faktiskt göra det är att registrera det med ett alternativ i shell-kommandot:

java -javaagent:myAgent.jar MyJavaApplication

Som vi kan se agent / transformator-projektet läggs till som en burk till exekveringen av alla applikationer med namnet MyJavaApplication som är tänkt att innehålla en klass med namnet "com.my.to.be.instrumented.MyClass" för att verkligen köra vår injicerade kod.



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow