ASM介绍
asm4-guide.pdf
ASM是操纵分析字节码的一个框架,ASM库的目标是生成、转换和分析编译的 的Java类,以字节数组的形式表示(因为它们存储在磁盘上并加载到 在Java虚拟机中加载)。为此目的,ASM提供了一些工具来读、写和转换这些字节数组。 为此,ASM提供了一些工具,通过使用比字节更高层次的概念来读取、写入和转换这些字节数组。
jdk内置了一个asm,如果没有去maven库下载,本文操作采用6.2版本
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>6.2</version>
</dependency>
asm库有三个重要的核心方法:
ClassReader :解析class字节码ClassWriter :构建class类ClassVisitor :抽象类,用来定义实现要对class进行的操作
用asm可以很方便的对一个class文件完成改造,也可以生成一个新的class
先写一个Test.class用作例子
ClassReader 与 ClassVisitor
代码实现
ClassReader类用来解析字节码,支持传入类名、class字节码流
package asm;
import org.objectweb.asm.*;
import java.io.FileInputStream;
public class Reader {
public static void main(String[] args) throws Exception {
FileInputStream fis = new FileInputStream("src\\main\\java\\asm\\Test.class");
ClassReader cr = new ClassReader(fis);
System.out.println("ClassName:" + cr.getClassName() + "\nSuperName:" + cr.getSuperName() + "\nInterfaces:" + cr.getInterfaces());
}
}
创建好ClassReader后会自动解析class 通过ClassReader只能获取简单的class文件信息,要获取变量、方法就要用到accept方法 而accept方法需要传入ClassVisitor类,这是一个抽象类,我们可以在里面定义要访问获取哪些内容: 写一个子类继承它,实现功能。
构造器直接调用ClassVisitor的,这里要传入一个api编号,由于我们下载的版本号是ASM6,因此就填入ASM6版本的api编号,在Opcodes操作码中已经有存储为Opcode.ASMx`。 接着要重写方法,ClassReader.accept调用时会按以下顺序执行这些方法,如同名字所示这些方法是用来获取对应内容的,如果要获取方法就在ClassVisitor重写visitMethod方法,然后获取name参数即可
读取类信息、变量、方法的例子
编写一个ClassInfoPrinter.java获取类的信息
package asm;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class ClassInfoPrinter extends ClassVisitor {
public ClassInfoPrinter() {
super(Opcodes.ASM6);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
System.out.println("Version:" + version + " Access:" + access + " Name:" + name + " SuperName:" + superName + " Interfaces"+ interfaces);
System.out.println();
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
System.out.println("Field Access: " + access + " Name:" + name + " Descriptor:" + descriptor + " Value:" + value);
return null;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println("Method Access: " + access + " Name:" + name + " Descriptor:" + descriptor);
return null;
}
}
调用ClassReader.accept即可,第二个参数为0,参数主要是跳过解析某些部分 观察输出,我们要的信息全部输出了
读取方法中变量的例子
AdviceAdapter已经不用了
- 先写一个ClassVisitor的子类、重写visitMethod拿到方法数组
- 再写一个MethodVisitor的子类、重写visitLocalVariable方法,然后在里面就能获取
MethodInfoPrinter.java
package asm;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class MethodInfoPrinter extends ClassVisitor {
public MethodInfoPrinter() {
super(Opcodes.ASM6);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if(name.equals("main")){
System.out.println("Method: " + name);
return new MyMethodVisitor();
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
}
class MyMethodVisitor extends MethodVisitor{
public MyMethodVisitor() {
super(Opcodes.ASM6);
}
@Override
public void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index) {
System.out.println(name + " " + descriptor + " " +index);
}
}
ReadMethod.java
package asm;
import org.objectweb.asm.ClassReader;
import java.io.FileInputStream;
public class ReadMethod {
public static void main(String[] args) throws Exception{
FileInputStream fis = new FileInputStream("src\\main\\java\\asm\\Test.class");
ClassReader cr = new ClassReader(fis);
System.out.println("ClassName:" + cr.getClassName() + "\nSuperName:" + cr.getSuperName() + "\nInterfaces:" + cr.getInterfaces());
System.out.println();
cr.accept(new MethodInfoPrinterBy(), 0);
}
}
与javap效果一致
ClassReader解析原理
利用ASM读取class
对于ClassReader的构造器:传入文件输入流那么就读取解析、传入类名就先用类加载器加载该类然后再分别读取解析
对于ClassReader的accept方法:通过位移量定位然后解析
功能总结
ClassReader读取二进制类数据,ClassVisitor配置获取的属性,ClassReader.accept通过属性对类数据执行操作。
我们想要完成的操作统统定义在ClassVisitor里面
ClassWriter
代码实现
ClassWriter这个类是ClassVisitor的子类,用来直接在二进制层面修改或生成class文件 它的构造器有两个,传入一个int配置选项,ClassReader是可选的参数。两个构造器对应了:在原class文件修改或直接新建class文件 int配置参数一般设置为CLASSWRITER.COMPUTE_FRAMES ,因为修改class文件可能会导致stack和local参数改变导致jvm无法识别,配置该参数会自动计算
之前注意到:ClassVisitor有两个构造器(其实第一个是特殊情况) 如果要进行修改,第二个参数ClassVisitor就设置为ClassWriter用于存储,然后具体修改的值在新建的ClassVisitor用return返回 然后设置accept的选项为扩大frame 最后将ClassWriter转化成byteArray写入文件即可
修改类信息、变量、方法的例子
将Test.class的类变量hello==1修改为hi==10
首先重写一个ClassVisitor,查找hello变量
ClassInfoModifier.java
package asm;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Opcodes;
public class ClassInfoModifier extends ClassVisitor {
public ClassInfoModifier(ClassWriter cw) {
super(Opcodes.ASM6, cw);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
if(name.equals("hello")){
return super.visitField(access, "hi", descriptor, signature, 10);
}
return super.visitField(access, name, descriptor, signature, value);
}
}
然后编写主方法Write.java
package asm;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class Writer {
public static void main(String[] args) throws Exception {
FileInputStream fis = new FileInputStream("src\\main\\java\\asm\\Test.class");
ClassReader cr = new ClassReader(fis);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
cr.accept(new ClassInfoModifier(cw), ClassReader.EXPAND_FRAMES);
FileOutputStream fos = new FileOutputStream(new File("src\\main\\java\\asm\\newTest.class"));
fos.write(cw.toByteArray());
}
}
生成了newTest.class,反编译发现已经修改成功!
方法中添加java代码
这部分参考其他师傅的笔记:
关于java字节码框架ASM的学习 ASM库的介绍和使用
ASM写HelloWorld
Java用ASM写一个HelloWorld程序
package asm;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.io.FileOutputStream;
public class WriteHelloWorld {
public static void main(String[] args) throws Exception {
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "HelloWorld", null, "java/lang/Object", null);
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("HelloWorld!");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
cw.visitEnd();
FileOutputStream fos = new FileOutputStream("src\\main\\java\\asm\\HelloWorld.class");
fos.write(cw.toByteArray());
}
}
之后java -classpath class文件目录 HelloWorld 即可运行
功能总结
修改class文件较复杂,大致分为以下步骤:
- 新建ClassReader解析class
- 新建ClassWriter,传入ClassReader并且参数设置为ClassWriter.COMPUTE_FRAMES
- 重写一个ClassVisitor,要修改的值通过return super返回
- 调用ClassReader的accept方法,传入重写的ClassVisitor并且选项设置为ClassReader.EXPAND_FRAMES
- 新建FileOutputStream文件输出流
- ClassWriter.toByteArray写入文件
完
欢迎关注我的CSDN :@Ho1aAs 版权属于:Ho1aAs 本文链接:https://blog.csdn.net/Xxy605/article/details/123132137 版权声明:本文为原创,转载时须注明出处及本声明
|