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 은 시스템의 "출력" 됩니다.
ldc - 상수를 스택으로 푸시합니다. 이 경우, 문자열 "Hello World"
invokevirtual - 스택에로드 된 참조에 대한 메소드를 호출하고 그 결과를 스택에 저장합니다. 메서드의 매개 변수도 스택에서 가져옵니다.
글쎄, 더 옳은가?
255 개의 opcode가 있지만 아직 구현되지 않은 것은 있습니다. 모든 현재 opcode가있는 표는 Java 바이트 코드 명령 목록에 있습니다.
바이트 코드를 어떻게 작성하고 편집 할 수 있습니까?
바이트 코드를 작성하고 편집하는 여러 가지 방법이 있습니다. 컴파일러를 사용하거나, 라이브러리를 사용하거나, 프로그램을 사용할 수 있습니다.
작문 :
편집 용 :
- 도서관
- 도구들
- 바이트 코드 - 뷰어
- JBytedit
- reJ - Java 8+를 지원하지 않습니다.
- JBE - Java 8+를 지원하지 않습니다.
바이트 코드에 대해 더 자세히 알고 싶습니다!
아마도 바이트 코드를위한 특정 문서 페이지가있을 것입니다. 이 페이지는 다른 라이브러리와 도구를 사용하여 바이트 코드를 수정하는 것에 중점을 둡니다.
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 파일로 추가된다.