简介
ASM 是一个通用的 Java 字节码操作和分析框架。它可直接以二进制形式用于修改现有类或动态生成类。应用场景有代码转换、优化、代码生成、动态字节码增强等。可以概括为generation、transformation 和 analysis
在学习之前先去看一下这三篇文章做一个前置知识
JAVA安全|字节码篇:字节码文件结构与解读
JAVA安全|字节码篇:常见字节码指令(JVM指令)
JAVA安全|字节码篇:字节码操作框架—ASM(原理)
在来个y4taker博客中的图总结
环境
1 | <properties> |
ASM使用
在asm当中有三个最重要的类,关系如下图
ClassReader类,负责读取.class文件里的内容,然后拆分成各个不同的部分。ClassVisitor类,负责对.class文件中某一部分里的信息进行修改。ClassWriter类,负责将各个不同的部分重新组合成一个完整的.class文件。
ClassVisitor类
用于生成和转换已编译类的 ASM API 是基于 ClassVisitor 抽象类的,将它收到的所有方法调用都委托给另一个 ClassVisitor 类,会调用该类的visitXXX方法,这个类可以看作一个事件筛选器
访问顺序
1 | visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )* |
方法解析
1 | public abstract class ClassVisitor { |
ClassReader类
该类解析 ClassFile 内容,并针对遇到的每个字段、方法和字节码指令调用给定 ClassVisitor 的相应访问方法。这个类可以看作一个事件生产者
ClassWriter类
ClassWriter 类是 ClassVisitor 抽象类的一个子类,它直接以二进制形式生成编译后的类。它会生成一个字节数组形式的输出,其中包含了已编译类,可以用 toByteArray 方法来提取。这个类可以看作一个事件消费者
MethodVisitor类
访问Java方法的访问者类,用于生成和转换已编译方法的 ASM API 是基于 MethodVisitor 抽象类的,它由 ClassVisitor 的 visitMethod 方法返回。
访问顺序
1 | visitAnnotationDefault? |
对非抽象方法,如果存在注解和属性,必须先访问;其次是按顺序访问字节代码,这些访问在visitCode与visitMaxs之间
方法解析
1 | abstract class MethodVisitor { // public accessors ommited |
来一个例子学习一下
读取字节码
1 | // 从文件系统中加载字节码 |
解析字节码
要解析字节码,我们需要创建一个自定义的 ClassVisitor 实现。以下是一个简单的示例,用于打印类名和方法名:
1 | package ocean.zbz; |
visit()方法参数说明:
- version:类文件的版本号,表示类文件的 JDK 版本。例如,JDK1.8 对应的版本号为 52(0x34),JDK 11 对应的版本号为 55(0x37)
- access:类访问标志,表示类的访问权限和属性。例如,ACC_PUBLIC(0x0001)表示类是公共的,ACC_FINAL(0x0010)表示类是 final 的。可以通过位运算组合多个访问标志
- name:类的内部名称,用斜线代替点分隔包名和类名。例如
com/example/MyClass - signature:类的泛型签名,如果类没有泛型信息,此参数为 null
- superName:父类的内部名称。对于除
java.lang.Object之外的所有类,此参数都不为 null - interfaces:类实现的接口的内部名称数组。如果类没有实现任何接口,此参数为空数组
visitMethod()方法参数说明:
- access:方法访问标志,表示方法的访问权限和属性。例如,ACC_PUBLIC(0x0001)表示方法是公共的,ACC_STATIC(0x0008)表示方法是静态的。可以通过位运算组合多个访问标志
- name:方法的名称。例如,
doSomething或<init>(构造方法) - descriptor:方法的描述符,表示方法的参数类型和返回值类型。例如,对于方法
void doSomething(int),描述符为(I)V - signature:方法的泛型签名,如果方法没有泛型信息,此参数为 null
- exceptions:方法抛出的异常的内部名称数组。如果方法没有声明抛出任何异常,此参数为空
实验类
1 | public class Foo { |
1 | public static void main(String[] args) throws IOException { |
修改字节码
字段修改
要添加、修改或删除字段,我们需要扩展 ClassVisitor 类并重写 visitField 方法,下面是一个示例,用于在类中添加一个名为newField的字段,并删除名为toBeRemovedField的字段,测试类
1 | public class Foo { |
1 | package ocean.zbz; |
visitField()的方法参数说明:
- access:字段访问标志,表示字段的访问权限和属性。例如,ACC_PUBLIC(0x0001)表示字段是公共的,ACC_STATIC(0x0008)表示字段是静态的。可以通过位运算组合多个访问标志
- name:字段的名称。例如
myField - descriptor:字段的描述符,表示字段的类型。例如,对于类型为 int 的字段,描述符为
I - signature:字段的泛型签名,如果字段没有泛型信息,此参数为 null
- value:字段的常量值,如果字段没有常量值,此参数为 null。需要注意的是,只有静态且已赋值的字段才会有常量值
1 | public static void main(String[] args) throws IOException { |
方法修改
要添加、修改或删除方法,我们需要扩展 ClassVisitor 类并重写 visitMethod 方法。下面是一个示例,用于在类中添加一个名为newMethod的方法,并删除名为toBeRemovedMethod的方法,测试代码:
1 | public class Foo { |
要修改方法内的指令,我们需要扩展 MethodVisitor 类并重写相应的 visit 方法。以下是一个示例
1 | import org.objectweb.asm.MethodVisitor; |
1 | public static void main(String[] args) throws Exception { |
可以发现已经能修改了原来class
方法的增加和删除
1 | package ocean.zbz; |
1 | public static void main(String[] args) throws Exception{ |
可以看到已经修改成功