ASM基础

356 阅读17分钟

ASM应用场景

ASM(Java字节码操作框架)是一个可以直接在Java字节码级别操作和修改class文件的框架。它提供了一套简单灵活的API,可以在运行时动态地生成、操作和分析字节码,从而实现对Java程序的动态修改和增强。

ASM的工作原理可以简单概括为以下几个步骤:

1. 字节码解析:ASM通过读取class文件中的字节码数据,将其解析成一个个抽象的字节码指令,这样程序就可以以抽象的方式操作和修改字节码。

2. 字节码生成:ASM提供了一套API,可以用来生成新的class文件或者修改class文件中的字节码。开发者可以使用这些API来创建、修改和删除字节码指令,实现对Java类的动态修改和增强。

3. 字节码转换:ASM还提供了一套访问器(Visitor)模式的API,可以用来遍历和修改class文件中的字节码指令。通过使用这些访问器,可以在访问和修改字节码的过程中实现对字节码的转换,比如增加新的指令、删除或替换现有指令等操作。

4. 类加载器集成:ASM可以与Java的类加载器机制进行集成,以实现对类的动态加载和替换。通过使用ASM,在类被加载到JVM之前,可以对类的字节码进行修改和增强,从而实现对类的自定义修改和拦截。

ASM的使用可以广泛应用于许多场景,比如字节码增强、AOP(面向切面编程)、动态代理、代码生成等。它可以帮助开发者实现对Java程序的动态修改,从而实现更高级的功能和扩展性。

ASMifier class文件->java源码(ASM api)

ASMifier 是一个 ASM 自带的工具,可以将任何无法理解的 Java 字节码转化成直接使用 ASM API 生成该字节码的代码(Java 源码),是一个将字节码转化成 ASM 代码的转化器。

可以看做是将二进制代码转换为ASM代码的工具。把已有的代码的二进制码转换成ASM代码,然后在这个基础之上进行更改,或者是把ASM代码转换成对应的字节码。使用可参考如下流程:

1. 生成 class 文件
根据相关代码编写成 java 文件并编译成 class 文件,例如观察 Hello 的编译结果,使用 javac 命令进行编译:

public class Hello {  
  
    public static void main(String[] args) {  
        System.out.println("Hello, ASM!");  
    }  
}  

通过 javac 命令进行编译,我们得到了 Hello.class 文件。

2. 使用 ASMifier 工具
在此基础上,运行 ASMifier 工具将上面编译好的 Hello.class 文件转化成对应的 ASM 代码,使用如下命令即可:

java org.objectweb.asm.util.ASMifier Hello.class  

3. 查看 ASM 代码
运行以上命令后,ASMifier 工具会将 Hello.class 文件的所有字节码转换成 ASM 代码,并输出在控制台上,将这些 ASM 代码,可以用来分析源代码的局限并探索使用更多的 ASM 功能,进行更为细致的源码操作。

总的来看,ASMifier 的作用就是将字节码转换成 ASM 代码,帮助我们处理字节码、生成代码分析等,极大地增强了 ASM 的灵活性和扩展性。

TraceClassVisitor

TraceClassVisitor 是 ASM 框架中非常有用的工具之一,它可以帮助开发者深入了解类的结构和内容,并进行相关分析和处理。 TraceClassVisitor 是 ASM 框架中的一个访问者(Visitor),用于跟踪和记录对类的访问操作。它可以通过回调方法追踪类的结构和内容,并生成相应的输出。

TraceClassVisitor 的作用是在访问类的过程中,以文本形式记录下访问过程中的各种操作,例如访问类的字段、方法、注解等,并生成对应的输出结果。

使用 TraceClassVisitor,可以了解类的详细信息,包括类名、父类、接口、字段、方法、注解、属性等,并能够看到每个操作对应的字节码信息。

使用方式如下:

1. 创建 ClassVisitor 对象:

ClassVisitor visitor = new TraceClassVisitor(new PrintWriter(System.out));  

2. 将 ClassVisitor 对象传递给 ClassReader:

ClassReader reader = new ClassReader(className);  
reader.accept(visitor, 0);  

3. 查看输出结果:
运行程序后,TraceClassVisitor 会将访问类的过程输出到标准输出(或其他指定的输出流),可以通过查看输出结果来了解类的详细信息。

示例输出结果可能包括类定义、字段信息、方法信息等,可以根据需要调整 

TraceClassVisitor 的输出级别或进行其他自定义操作。
TraceClassVisitor 是 ASM 框架中非常有用的工具之一,它可以帮助开发者深入了解类的结构和内容,并进行相关分析和处理。

CheckClassAdapter

CheckClassAdapter是ASM框架中的一个适配器类,用于在访问和转换类的过程中进行检查和验证。它可以用作ClassVisitor的包装器,用于检查所访问类的有效性,并在发现错误时生成相应的错误消息。

CheckClassAdapter主要提供以下功能:

1. 类的基本验证:它会检查类的标志、访问修饰符和名称等是否符合规范,确保类的定义是有效的。

2. 字段和方法的验证:它会验证访问修饰符、类型签名、异常表等,确保字段和方法的定义是有效的。

3. 属性和注解的验证:它会验证属性和注解的格式、内容和正确性,确保它们符合规范。

4. 字节码指令的验证:它会验证字节码指令的参数是否正确,确保字节码的合法性和有效性。

CheckClassAdapter在访问和转换类的过程中,充当了一个安全屏障,帮助开发者避免常见的错误和潜在的安全漏洞。它可以在编译时或运行时帮助开发者发现和修复问题,提高代码的可靠性和安全性。

使用方式如下:

1. 创建ClassVisitor对象:

ClassVisitor visitor = new CheckClassAdapter(classWriter);  

这里的classWriter是一个实现了ClassVisitor接口的对象,用于对类进行访问和转换。

2. 使用CheckClassAdapter对象进行类的访问和转换:

classReader.accept(visitor, ClassReader.EXPAND_FRAMES);  

这里的classReader是一个ClassReader对象,用于读取类的字节码。

3. 检查验证结果:
CheckClassAdapter会在访问和转换过程中发现错误时,输出相应的错误消息。可以通过查看错误消息来定位和修复问题。

CheckClassAdapter是一个非常有用的工具,它可以帮助开发者发现并修复类的问题,提高代码的质量和可靠性。在使用ASM框架进行类的访问和转换时,建议将CheckClassAdapter作为一个必要的步骤,以确保生成的字节码符合规范并进行了正确的转换。

Type

在 ASM 框架中,Type 类是一个用于表示 Java 类型的工具类。它提供了各种静态方法,用于创建和操作不同类型的描述符。

Type 类中的一些常用方法包括:

1. getType(String className):根据类的全限定名,返回对应的 Type 对象。

2. getObjectType(String className):根据类的全限定名,返回对应的引用类型的 Type 对象。

3. getArrayType(Type elementType):根据元素类型,返回对应的数组类型的 Type 对象。

4. getMethodType(Type returnType, Type... argumentTypes):根据返回类型和参数类型,返回对应的方法类型的 Type 对象。

5. getDescriptor(Class<?> type):返回给定类型的描述符。

6. getInternalName(Class<?> type):返回给定类型的内部名称。

7. getJavaName(String internalName):返回给定内部名称的 Java 类名。

Type 类还提供了一些用于获取类型信息的实例方法,如 getSort()、getElementType()、getReturnType()、getArgumentTypes() 等。

在 ASM 框架中,Type 类的主要作用是提供一种方便的方式来表示和操作不同类型的描述符。通过使用 Type 类,可以更加灵活和准确地处理和生成类的描述符,从而实现更高级和复杂的转换和操作。

示例代码:

// 创建 Type 对象  
Type intType = Type.getType(int.class);  
Type stringType = Type.getType(String.class);  
Type arrayType = Type.getType(String[].class);  
Type methodType = Type.getMethodType(intType, stringType);  
  
// 获取类型信息  
System.out.println(intType.getSort());  
System.out.println(arrayType.getElementType());  
System.out.println(methodType.getReturnType());  
System.out.println(Arrays.toString(methodType.getArgumentTypes()));  
  
// 获取类型的描述符和内部名称  
System.out.println(Type.getDescriptor(String.class));  
System.out.println(Type.getInternalName(String.class));  

上述代码演示了使用 Type 类的一些常见操作,包括创建 Type 对象、获取类型信息和获取描述符和内部名称等。

XxxNode

在 ASM 框架中,XxxNode 表示一个特定的节点,其中的 Xxx 是节点的类型。这些节点用于表示类的不同部分,如类的字段、方法、注解、指令等。每种节点都有特定的属性和方法,用于操作和获取相应的信息。

以下是一些常见的 XxxNode 类型:

1. ClassNode:表示一个类节点。它包含了类的访问标志、父类、接口、字段、方法等信息。

2. FieldNode:表示一个字段节点。它包含了字段的访问标志、名字、类型、注解等信息。

3. MethodNode:表示一个方法节点。它包含了方法的访问标志、名字、返回类型、参数、注解等信息。

4. AnnotationNode:表示一个注解节点。它包含了注解的类型、成员值等信息。

5. ParameterNode:表示一个参数节点。它包含了参数的名字、访问标志、注解等信息。

6. InsnNode:表示一个指令节点。它包含了指令的类型、操作数等信息。

这些 XxxNode 类型都是 ASM 框架提供的类,用于表示不同的节点。可以通过创建这些节点对象,或者通过访问类的节点(例如 ClassNode)来对类的不同部分进行操作和修改。

示例代码:

ClassReader cr = new ClassReader("com.example.MyClass");  
  
// 创建一个 ClassNode 对象  
ClassNode cn = new ClassNode();  
cr.accept(cn, 0);  
  
// 遍历类的字段节点  
for (FieldNode fn : cn.fields) {  
    System.out.println("Field: " + fn.name);  
}  
  
// 遍历类的方法节点  
for (MethodNode mn : cn.methods) {  
    System.out.println("Method: " + mn.name);  
}  

上述代码演示了如何使用 XxxNode 类型来表示和操作类的不同节点。通过创建对应的节点对象,或者通过访问类的节点(例如 ClassNode),可以方便地获取和修改类的不同部分。这使得在 ASM 框架中进行字节码操作变得更加灵活和可控。

树API

ASM 中的树 API 指的就是 XxxNode 类和相关的类。这些类用于表示字节码的不同部分,并支持对字节码进行分析、修改和生成。

使用树 API,可以以树状结构的形式遍历和操作字节码的不同节点,例如类节点、字段节点、方法节点等。每个节点都有对应的属性和方法,用于获取和修改相应的信息。

树 API 为开发者提供了一种直观和灵活的方式来操作字节码,可以轻松地进行自定义的字节码分析和修改。例如,可以使用树 API 来添加新的方法、修改方法的指令、增加注解等。

使用树 API 的基本步骤如下:

1. 创建一个 ClassReader 对象,用于读取字节码数据。

2. 创建一个 ClassNode 对象,作为字节码的根节点。

3. 调用 ClassReader 的 accept() 方法,将字节码数据解析为树结构,并将结果存储在 ClassNode 中。

4. 使用 ClassNode 和其他 XxxNode 类型的对象,遍历树结构,获取和修改字节码的不同节点。

5. 可选地,将修改后的字节码重新转换为字节数组,输出或保存。

示例代码:

ClassReader cr = new ClassReader("com.example.MyClass");  
  
// 创建一个 ClassNode 对象  
ClassNode cn = new ClassNode();  
cr.accept(cn, 0);  
  
// 遍历类的方法节点  
for (MethodNode mn : cn.methods) {  
    System.out.println("Method: " + mn.name);  
  
    // 获取方法的指令列表  
    InsnList instructions = mn.instructions;  
  
    // 遍历指令列表  
    for(AbstractInsnNode insn : instructions) {  
        // 判断指令的类型  
        if(insn.getOpcode() == Opcodes.INVOKEVIRTUAL) {  
            MethodInsnNode methodInsn = (MethodInsnNode) insn;  
  
            // 修改方法调用的参数  
            methodInsn.desc = "(Ljava/lang/String;)Ljava/lang/String;";  
        }  
    }  
}  
  
// 将修改后的字节码转换为字节数组  
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);  
cn.accept(cw);  
byte[] modifiedBytes = cw.toByteArr

除了遍历和修改字节码的不同节点之外,树 API 还提供了其他一些常用的功能,包括但不限于以下几个方面:

1. 生成字节码:使用树 API 可以方便地生成新的字节码。通过创建类节点(ClassNode)以及字段节点(FieldNode)、方法节点(MethodNode)等,在这些节点上设置属性和指令,最后将节点转换为字节数组即可。

2. 访问类的属性和注解:树 API 提供了一种便捷的方式来访问类、字段和方法的属性和注解。例如,可以使用 ClassNode 的属性和方法来获取类的修饰符、父类、实现的接口等信息,以及获取字段和方法上的注解。

3. 字节码分析与优化:树 API 可以帮助开发者进行字节码的分析和优化。通过遍历树结构,可以获取方法的指令列表和异常处理器信息,从而进行性能分析、代码优化等操作。可以使用树 API 来插入新的指令、删除无用的指令、重新排序指令等。

4. 字节码转换器:树 API 还提供了一种字节码转换器(ClassTransformer)的机制,用于处理字节码的转换工作。开发者可以实现自己的 ClassTransformer,并将其注册到字节码处理器(ClassVisitor)中,以实现对字节码的自定义转换操作。这可以用于实现字节码的加密、混淆、增加日志等功能。

综上所述,树 API 提供了一系列用于处理和操作字节码的类和方法,可以帮助开发者灵活、高效地进行字节码操作。通过树 API,开发者可以更深入地理解和修改 Java 字节码,实现各种自定义的功能。

ClassTransformer

ClassTransformer 是字节码转换器的概念,它是 ASM 框架中的一个接口。ClassTransformer 提供了一种将字节码进行自定义转换的机制,可以用于实现字节码的增强、修改、优化、加密等操作。

ClassTransformer 接口定义了两个方法:

1. byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer):此方法用于对传入的字节码进行转换。它接收一个字节码数组作为输入,并返回一个转换后的字节码数组。参数中的 loader 是定义要转换的类的类加载器,className 是要转换的类的类名,classBeingRedefined 是正在被重新定义的类,protectionDomain 是类的保护域,而 classfileBuffer 则是要进行转换的字节码。

2. void setTarget(ClassVisitor target):此方法用于设置转换后的字节码的目标 ClassVisitor。ClassVisitor 是 ASM 框架中的一个接口,可以用于遍历字节码,并对其进行修改。

开发者可以根据自己的需求实现一个自定义的 ClassTransformer,实现自己的字节码转换逻辑,并将其注册到 ASM 的转换器链中处理字节码。转换器链中的每个转换器都会依次对字节码进行转换,最终生成最终的字节码。

ClassTransformer 为开发者提供了一种灵活的方式来修改字节码,可用于实现各种功能,例如字节码增强、代码注入、性能优化、混淆、加密等。

核心api

ASM 的核心 API 包括以下几个部分:

1. ClassReader 和 ClassWriter:ClassReader 用于解析二进制类文件,将其转换为内部表示的形式;ClassWriter 则用于将内部表示形式的字节码写回到二进制类文件。开发者可以使用 ClassReader 解析一个类的字节码,然后使用 ClassWriter 得到一个修改过的字节码,最后再通过 ClassLoader 加载修改过的类。

2. ClassVisitor:ClassVisitor 提供了遍历和修改类文件的方法,包括类的访问、访问类的成员、访问方法的局部变量、访问方法的异常处理器等。开发者可以实现自己的 ClassVisitor,并与 ClassReader 和 ClassWriter 结合起来处理字节码。

3. Type:ASM 中的 Type 类支持 Java 类型系统的各种信息,包括 Java 类型,方法类型,字段类型等。Type 类可以用于描述要访问的类、方法的参数和返回值等。

4. Opcode:Opcode 是 ASM 中定义了字节码的数值表示。它定义了 JVM 中所有合法的字节码指令。开发者可以使用 Opcode 在字节码中插入指令、修改指令或删除指令。

5. Label:Label 是 ASM 中定义的标签,用于标识指令流中的特定位置。Label 可以用于确定指令跳转和异常处理器的范围等。开发者可以使用 Label 实现各种高级控制流结构,例如循环、条件分支等。

ASM 的核心 API 可以让开发者对字节码进行精细的控制和操作,包括读取、修改、生成、转换等各种功能。通过组合使用这些 API,开发者可以完成高级字节码转换和增强的任务。

Opcode

Opcode(Operation Code)是代表 Java 字节码指令的数值表示。在 JVM 中,每个字节码指令都对应着一个唯一的 Opcode。

Opcode 提供了对字节码指令的访问和处理的能力。开发者可以使用 Opcode 来插入新的指令、修改现有指令或删除指令等。ASM 框架中的一些核心类,例如 ClassReader、ClassVisitor 和 MethodVisitor,都提供了方法来访问和处理字节码指令的 Opcode。

以下是一些常见的 Opcode:

1. 加载和存储指令:
iload / istore:加载和存储 int 型局部变量。
aload / astore:加载和存储引用型局部变量。
fload / fstore:加载和存储 float 型局部变量。
dload / dstore:加载和存储 double 型局部变量。
lload / lstore:加载和存储 long 型局部变量。
getfield / putfield:获取和设置实例字段值。
getstatic / putstatic:获取和设置类字段值。
arrayload / arraystore:加载和存储数组元素值。

2. 算术和逻辑指令:
iadd / isub / imul / idiv:int 型加减乘除操作。
fadd / fsub / fmul / fdiv:float 型加减乘除操作。
dadd / dsub / dmul / ddiv:double 型加减乘除操作。
ladd / lsub / lmul / ldiv:long 型加减乘除操作。
iand / ior / ixor:按位与、按位或、按位异或操作。
ifeq / ifne / iflt / ifge / ifgt / ifle:条件跳转操作。

3. 类型转换指令:
i2b / i2c / i2d / i2f / i2l / i2s:int 到 byte、char、double、float、long、short 的转换。
l2i / f2i / d2i:long、float、double 到 int 的转换。

4. 方法调用和返回指令:
invokevirtual / invokeinterface / invokestatic:调用实例方法、接口方法、类方法。
ireturn / freturn / dreturn / lreturn / areturn / return:返回 int、float、double、long、引用、void。 5. 控制和跳转指令:
goto:无条件跳转到指定位置。
ifnull / ifnonnull:判断引用是否为 null,进行跳转。
tableswitch / lookupswitch:通过表格或查找表进行跳转。
jsr / ret:子例程调用和返回。

6. 异常处理指令:
athrow:抛出异常。
trycatch:捕获异常。

7. 数组指令:
newarray:创建基本类型数组。
anewarray:创建引用类型数组。
arraylength:获取数组长度。

8. 对象指令:
new:创建新的实例。
dup / dup2:复制栈顶元素。
monitorenter / monitorexit:进入或退出同步块。
checkcast:检查引用类型的类型转换。

9. 字段指令:
getfield / putfield:获取和设置实例字段值。
getstatic / putstatic:获取和设置类字段值。

10. 方法指令:
invokevirtual / invokeinterface / invokestatic:调用实例方法、接口方法、类方法。
invokevirtual:调用虚方法。

11. 类操作指令:
new:创建新的实例。
anewarray:创建引用类型数组。
instanceof:判断对象是否为某个类的实例。
checkcast:检查引用类型的类型转换。
getstatic / putstatic:获取和设置类字段值。

以上是常见的一些 Opcode,在 Java 字节码中还存在很多其他的指令和 Opcode,开发者可以根据具体需求来选择合适的 Opcode 进行字节码操作。