Java Language
Изменение байтового кода
Поиск…
Что такое Bytecode?
Bytecode - это набор инструкций, используемых 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» of System .
ldc - Вставьте константу в стек. В этом случае строка «Hello World»
invokevirtual - вызывает метод на загруженной ссылке в стеке и помещает результат в стек. Параметры метода также берутся из стека.
Ну, должно быть, правильнее?
Есть 255 опкодов, но не все они реализованы. Таблицу со всеми текущими кодами операций можно найти здесь: списки инструкций для байт-кода Java .
Как я могу писать / редактировать байт-код?
Существует несколько способов записи и редактирования байт-кода. Вы можете использовать компилятор, использовать библиотеку или использовать программу.
Для записи:
Для редактирования:
- Библиотеки
- инструменты
- Bytecode-Viewer
- JBytedit
- reJ - не поддерживает Java 8+
- JBE - не поддерживает Java 8+
Я хотел бы узнать больше о байтекоде!
Вероятно, определенная страница документации специально для байт-кода. Эта страница посвящена модификации байт-кода с использованием разных библиотек и инструментов.
Как редактировать файлы jar с помощью ASM
Сначала нужно загружать классы из банки. Мы будем использовать три метода для этого процесса:
- loadClasses (File)
- readJar (JarFile, JarEntry, Карта)
- getNode (байт [])
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.
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 / другие двоичные ресурсы в банке) в виде байтов
- Сохранить все байты в новой банке
Из последней части выше мы создадим три метода.
- processNodes (Map <String, ClassNode> узлы)
- loadNonClasses (Файл jarFile)
- saveAsJar (Карта <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 - это инструментальная библиотека байт-кода, которая позволяет вам модифицировать байт-код, вводящий Java-код, который будет преобразован в байт-код Javassist и добавлен к инструментальному классу / методу во время выполнения.
Давайте напишем первый трансформатор, который фактически возьмет гипотетический класс «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
Поскольку мы видим, что проект agent / transformer добавлен как банка к исполнению любого приложения с именем MyJavaApplication, которое должно содержать класс с именем «com.my.to.be.instrumented.MyClass», чтобы фактически выполнить наш введенный код.