수색…


바이트 코드 란 무엇입니까?

바이트 코드는 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시스템의 "출력" 됩니다.

ldc - 상수를 스택으로 푸시합니다. 이 경우, 문자열 "Hello World"

invokevirtual - 스택에로드 된 참조에 대한 메소드를 호출하고 그 결과를 스택에 저장합니다. 메서드의 매개 변수도 스택에서 가져옵니다.

글쎄, 더 옳은가?

255 개의 opcode가 있지만 아직 구현되지 않은 것은 있습니다. 모든 현재 opcode가있는 표는 Java 바이트 코드 명령 목록에 있습니다.

바이트 코드를 어떻게 작성하고 편집 할 수 있습니까?

바이트 코드를 작성하고 편집하는 여러 가지 방법이 있습니다. 컴파일러를 사용하거나, 라이브러리를 사용하거나, 프로그램을 사용할 수 있습니다.

작문 :

편집 용 :

바이트 코드에 대해 더 자세히 알고 싶습니다!

아마도 바이트 코드를위한 특정 문서 페이지가있을 것입니다. 이 페이지는 다른 라이브러리와 도구를 사용하여 바이트 코드를 수정하는 것에 중점을 둡니다.

ASM으로 jar 파일을 편집하는 방법

첫째로 병에서 종류는 적재 될 필요가있다. 이 프로세스에는 세 가지 방법이 사용됩니다.

  • 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를 변경하는 간단한 작업이됩니다. 이 예제에서는 jar 파일의 모든 문자열을 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();
                }
            }
        }
    }
}

이제 모든 ClassNode의 문자열이 수정되었으므로 변경 내용을 저장해야합니다. 변경 사항을 저장하고 작업 결과를 얻으려면 몇 가지 작업을 수행해야합니다.

  • ClassNodes를 바이트로 내보내기
  • 비 클래스 jar 항목 (예 : Manifest.mf / jar의 다른 2 진 리소스) 을 바이트로로드합니다.
  • 모든 바이트를 새로운 항아리에 저장하십시오.

위의 마지막 부분부터 세 가지 방법을 만듭니다.

  • processNodes (Map <String, ClassNode> nodes)
  • loadNonClasses (File jarFile)
  • saveAsJar (Map <String, byte []> outBytes, 캐릭터 라인 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