ASM字节码编辑学习笔记

ASM学习笔记

前言

最近对IAST、RASP产品有兴趣,发现涉及到了ASM字节码编辑技术,于是学习一下。由于ASM内容比较多,且本人是第一次学习,内容大多为学习摘录。详细可以参考Java ASM系列 | lsieun

ASM 是什么?

简单来说,ASM 是一个操作 Java 字节码的类库。它操作的对象是字节码数据,通常以 .class 文件形式存在。ASM 处理字节码的方式是“拆分-修改-合并”,具体步骤如下:

  1. 将 .class 文件拆分成多个部分。
  2. 对某个部分的信息进行修改。
  3. 将多个部分重新组织成一个新的 .class 文件。

ASM 能够做什么

ASM 可以:

  • 修改父类
  • 添加或删除接口
  • 添加或删除字段
  • 添加或删除或修改方法

.class 文件的结构

ClassFile 结构

每个 .class 文件遵循 Java Virtual Machine Specification 中定义的 ClassFile 结构。其具体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
  • u1: 1 个字节
  • u2: 2 个字节
  • u4: 4 个字节
  • u8: 8 个字节

其中,cp_infofield_infomethod_infoattribute_info 结构由 u1、u2、u4 和 u8 组成。

field_info 结构

在 .class 文件中定义的字段遵循 field_info 结构:

1
2
3
4
5
6
7
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}

method_info 结构

在 .class 文件中定义的方法遵循 method_info 结构:

1
2
3
4
5
6
7
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}

Code 属性结构

method_info 结构中,方法体的代码存储在 Code 属性中,其结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

ASMPrint 类

在刚开始学习 ASM 的时候,编写 ASM 代码是不太容易的。 ASMPrint 类能帮助我们将 .class 文件转换为 ASM 代码,这个功能非常实用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package Study;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.util.ASMifier;
import org.objectweb.asm.util.Printer;
import org.objectweb.asm.util.Textifier;
import org.objectweb.asm.util.TraceClassVisitor;

import java.io.IOException;
import java.io.PrintWriter;

public class ASMPrint {
public static void main(String[] args) throws IOException {
// (1) 设置参数
String className = "Study.HelloWorld";
int parsingOptions = ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG;
boolean asmCode = true;

// (2) 打印结果
Printer printer = asmCode ? new ASMifier() : new Textifier();
PrintWriter printWriter = new PrintWriter(System.out, true);
TraceClassVisitor traceClassVisitor = new TraceClassVisitor(null, printer, printWriter);
new ClassReader(className).accept(traceClassVisitor, parsingOptions);
}
}
1
2
3
4
5
6
7
8
package Study;

public class HelloWorld {
public void sayHello() {
System.out.println("Hello, World!");
}
}

对于 ASMPrint 类来说,其中

  • className 值设置为类的全限定名,可以是我们自己写的类,例如 sample.HelloWorld,也可以是 JDK 自带的类,例如 java.lang.Comparable
  • asmCode 值设置为 truefalse。如果是 true,可以打印出对应的 ASM 代码;如果是 false,可以打印出方法对应的 Instruction。
  • parsingOptions 值设置为 ClassReader.SKIP_CODEClassReader.SKIP_DEBUGClassReader.SKIP_FRAMESClassReader.EXPAND_FRAMES 的组合值,也可以设置为 0,可以打印出详细程度不同的信息。

ClassVisitor 类

class info

ClassVisitor 类是一个 abstract 类,要想使用它,就必须有具体的子类来继承它。

比较常见的 ClassVisitor 子类是 ClassWriter

1
2
public class ClassWriter extends ClassVisitor {
}

fields

ClassVisitor 类定义的字段

1
2
3
4
public abstract class ClassVisitor {
protected final int api;
protected ClassVisitor cv;
}
  • api 字段:它是一个 int 类型的数据,指出了当前使用的 ASM 版本,其可取值为 Opcodes.ASM4~`Opcodes.ASM9。我们使用的 ASM 版本是 9.0,因此我们在给 api字段赋值的时候,选择Opcodes.ASM9` 就可以了。
  • cv 字段:它是一个 ClassVisitor 类型的数据,它的作用是将多个 ClassVisitor 串连起来。

img

constructors

ClassVisitor 类定义的构造方法

1
2
3
4
5
6
7
8
9
10
public abstract class ClassVisitor {
public ClassVisitor(final int api) {
this(api, null);
}

public ClassVisitor(final int api, final ClassVisitor classVisitor) {
this.api = api;
this.cv = classVisitor;
}
}

methods

ClassVisitor 类定义的方法。重点关注关注这 4 个方法:visit()visitField()visitMethod()visitEnd()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class ClassVisitor {
public void visit(
final int version,
final int access,
final String name,
final String signature,
final String superName,
final String[] interfaces);
public FieldVisitor visitField( // 访问字段
final int access,
final String name,
final String descriptor,
final String signature,
final Object value);
public MethodVisitor visitMethod( // 访问方法
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions);
public void visitEnd();
// ......
}

ClassVisitorvisit() 方法、visitField() 方法和 visitMethod() 方法中都带有 signature 参数。这个 signature 参数与“泛型”密切相关;换句话说,如果处理的是一个带有泛型信息的类、字段或方法,那么就需要给 signature 参数提供一定的值;如果处理的类、字段或方法不带有“泛型”信息,那么将 signature 参数设置为 null 就可以了。在本次课程当中,我们不去考虑“泛型”相关的内容,所以我们都将 signature 参数设置成 null 值。

如果大家对 signature 参数感兴趣,我们可以使用之前介绍的 PrintASMCodeCore 类去打印一下某个泛型类的 ASM 代码。例如,java.lang.Comparable 是一个泛型接口,我们就可以使用 PrintASMCodeCore 类来打印一下它的 ASM 代码,从来查看 signature 参数的值是什么。

方法的调用顺序

visitXxx() 方法,遵循一定的调用顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
visit
[visitSource][visitModule][visitNestHost][visitPermittedSubclass][visitOuterClass]
(
visitAnnotation |
visitTypeAnnotation |
visitAttribute
)*
(
visitNestMember |
visitInnerClass |
visitRecordComponent |
visitField |
visitMethod
)*
visitEnd
  • []: 表示最多调用一次,可以不调用,但最多调用一次。
  • ()|: 表示在多个方法之间,可以选择任意一个,并且多个方法之间不分前后顺序。
  • *: 表示方法可以调用 0 次或多次。

由于我们作为初学者,只关注 ClassVisitor 类当中的 visit() 方法、visitField() 方法、visitMethod() 方法和 visitEnd() 方法这 4 个方法,因此调用顺序是

1
2
3
4
5
6
visit
(
visitField |
visitMethod
)*
visitEnd

visit() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void visit(
final int version,
final int access,
final String name,
final String signature,
final String superName,
final String[] interfaces
);

ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
ClassVisitor 方法 参数 ClassFile
ClassVisitor.visit() version minor_versionmajor_version
access access_flags
name this_class
signature attributes的一部分信息
superName super_class
interfaces interfaces_countinterfaces
ClassVisitor.visitField() field_info
ClassVisitor.visitMethod() method_info

visitField() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public FieldVisitor visitField( // 访问字段
final int access,
final String name,
final String descriptor,
final String signature,
final Object value
);

field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
ClassVisitor 方法 参数 field_info
ClassVisitor.visitField() access access_flags
name name_index
descriptor descriptor_index
signature attributes的一部分信息
value attributes的一部分信息

visitMethod() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public MethodVisitor visitMethod( // 访问方法
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions
);

method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
ClassVisitor 方法 参数 method_info
ClassVisitor.visitMethod() access access_flags
name name_index
descriptor descriptor_index
signature attributes的一部分信息
exceptions attributes的一部分信息

visitEnd() 方法

visitEnd() 方法,它是这些 visitXxx() 方法当中最后一个调用的方法。

为什么 visitEnd() 方法是“最后一个调用的方法”呢?是因为在 ClassVisitor 当中,定义了多个 visitXxx() 方法,这些个 visitXxx() 方法之间要遵循一个先后调用的顺序,而 visitEnd() 方法是最后才去调用的。

等到 visitEnd() 方法调用之后,就表示说再也不去调用其它的 visitXxx() 方法了,所有的“工作”已经做完了,到了要结束的时候了。

1
2
3
4
5
6
7
8
9
10
/*
* Visits the end of the class.
* This method, which is the last one to be called,
* is used to inform the visitor that all the fields and methods of the class have been visited.
*/
public void visitEnd() {
if (cv != null) {
cv.visitEnd();
}
}

ClassWriter 类

class info

ClassWriter 的父类是 ClassVisitor,因此 ClassWriter 类继承了 visit()visitField()visitMethod()visitEnd() 等方法。

1
2
public class ClassWriter extends ClassVisitor {
}

fields

ClassWriter 定义的字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ClassWriter extends ClassVisitor {
private int version;
private final SymbolTable symbolTable;

private int accessFlags;
private int thisClass;
private int superClass;
private int interfaceCount;
private int[] interfaces;

private FieldWriter firstField;
private FieldWriter lastField;

private MethodWriter firstMethod;
private MethodWriter lastMethod;

private Attribute firstAttribute;

//......
}

这些字段与 ClassFile 结构密切相关:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

constructors

ClassWriter 定义的构造方法

ClassWriter 定义的构造方法有两个,这里只关注其中一个,也就是只接收一个 int 类型参数的构造方法。在使用 new 关键字创建 ClassWriter 对象时,推荐使用 COMPUTE_FRAMES 参数。

1
2
3
4
5
6
7
8
9
10
11
12
public class ClassWriter extends ClassVisitor {
/* A flag to automatically compute the maximum stack size and the maximum number of local variables of methods. */
public static final int COMPUTE_MAXS = 1;
/* A flag to automatically compute the stack map frames of methods from scratch. */
public static final int COMPUTE_FRAMES = 2;

// flags option can be used to modify the default behavior of this class.
// Must be zero or more of COMPUTE_MAXS and COMPUTE_FRAMES.
public ClassWriter(final int flags) {
this(null, flags);
}
}
  • COMPUTE_MAXS

    • 标志作用:这个标志用于自动计算方法的最大栈大小(maximum stack size)和最大局部变量数量(maximum number of local variables)。
    • 使用方式:当设置这个标志时,你不需要手动传递最大栈大小和最大局部变量数量给MethodVisitor接口的visitMaxs方法。相反,这些值将会根据方法的签名和字节码自动计算出来。
    • 效果:这意味着,当你在visitMethod方法中返回MethodVisitor时,不需要关心传递给visitMaxs方法的参数,因为它们会被忽略。

    COMPUTE_FRAMES

    • 标志作用:这个标志用于从零开始自动计算方法的栈映射帧(stack map frames)。
    • 使用方式:如果你设置了COMPUTE_FRAMES标志,那么MethodVisitor.visitFrame方法的调用将会被忽略,框架会重新根据方法的字节码来计算栈映射帧。
    • 效果:同样,这也意味着传递给visitMaxs方法的参数会被忽略,并且将会根据字节码重新计算。换句话说,COMPUTE_FRAMES标志隐含了COMPUTE_MAXS标志的设置,因为你不能在不计算最大栈大小的情况下计算栈映射帧。

    简而言之,这两个标志是为了简化字节码操作过程中的一些繁琐计算。COMPUTE_MAXS可以自动确定方法执行时所需的栈大小和局部变量表的大小,而COMPUTE_FRAMES会进一步计算方法在执行期间的每个指令点的栈状态。使用这些标志,开发者不需要手动管理这些信息,字节码框架会根据提供的字节码自动完成这些计算。这在处理复杂字节码时特别有用,它可以减少出错的机会,并简化代码的编写。

创建 ClassWriter 对象

在创建ClassWriter对象时,可以指定一个flags参数来决定ASM框架在生成类文件时是否自动计算某些字节码相关的信息。

  • ClassWriterflags参数有三个可选值:
    1. 0:ASM不会自动计算最大栈大小(max stacks)和最大局部变量数量(max locals),也不会自动计算栈映射帧(stack map frames)。
    2. ClassWriter.COMPUTE_MAXS:ASM会自动计算最大栈大小和最大局部变量数量,但不会自动计算栈映射帧。
    3. ClassWriter.COMPUTE_FRAMES(推荐使用):ASM会自动计算最大栈大小、最大局部变量数量,并且会自动计算栈映射帧。

下表展示了不同flags值对应的行为:

Flags Max Stacks and Max Locals Stack Map Frames
0 NO NO
ClassWriter.COMPUTE_MAXS YES NO
ClassWriter.COMPUTE_FRAMES YES YES

示例代码ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);创建了一个ClassWriter实例,并指定了COMPUTE_FRAMES标志,这意味着ASM将自动计算最大栈大小、最大局部变量数量,并且会计算栈映射帧,从而简化了字节码生成过程,并减少了手动错误的可能性。

使用 ClassWriter 类

使用 ClassWriter 生成一个 Class 文件,可以大致分成三个步骤:

  • 第一步,创建 ClassWriter 对象。
  • 第二步,调用 ClassWriter 对象的 visitXxx() 方法。
  • 第三步,调用 ClassWriter 对象的 toByteArray() 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HelloWorldGenerateCore {
public static byte[] dump () throws Exception {
// (1) 创建 ClassWriter 对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

// (2) 调用 visitXxx() 方法
cw.visit(...);
cw.visitField(...);
cw.visitMethod(...);
cw.visitEnd(...); // 注意,最后要调用 visitEnd() 方法

// (3) 调用 toByteArray() 方法
byte[] bytes = cw.toByteArray();
return bytes;
}
}

示例一:生成接口

生成 HelloWorld 接口。

1
2
public interface HelloWorld {
}

编码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package Study.ClassWriter;

import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassWriter;

import static org.objectweb.asm.Opcodes.*;

public class HelloWorldGenerateCore {
public static void main(String[] args) throws Exception {
String relative_path = "ClassFilePath/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);

// (1) 生成 byte[] 内容
byte[] bytes = dump();

// (2) 保存 byte[] 到文件
FileUtils.writeBytes(filepath, bytes);
}

public static byte[] dump() throws Exception {
// (1) 创建 ClassWriter 对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

// (2) 调用 visitXxx() 方法
cw.visit(
V1_8, // version
ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, // access
"sample/HelloWorld", // name
null, // signature
"java/lang/Object", // superName
null // interfaces
);

cw.visitEnd();

// (3) 调用 toByteArray() 方法
return cw.toByteArray();
}
}

调用了 visit() 方法、visitEnd() 方法和 toByteArray() 方法。

由于 sample.HelloWorld 这个接口中,并没有定义任何的字段和方法,因此,在上述代码中没有调用 visitField() 方法和 visitMethod() 方法。

  • version: 表示当前类的版本信息。在上述示例代码中,其取值为 Opcodes.V1_8,表示使用 Java 8 版本。
  • access: 表示当前类的访问标识(access flag)信息。在上面的示例中,access 的取值是 ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,也可以写成 ACC_PUBLIC | ACC_ABSTRACT | ACC_INTERFACE。如果想进一步了解这些标识的含义,可以参考 Java Virtual Machine Specification 的 Chapter 4. The class File Format 部分。
  • name: 表示当前类的名字,它采用的格式是 Internal Name 的形式。
  • signature: 表示当前类的泛型信息。因为在这个接口当中不包含任何的泛型信息,因此它的值为 null
  • superName: 表示当前类的父类信息,它采用的格式是 Internal Name 的形式。
  • interfaces: 表示当前类实现了哪些接口信息。

示例二:生成接口 + 字段 + 方法

生成 HelloWorld 接口。

1
2
3
4
5
6
public interface HelloWorld extends Cloneable {
int LESS = -1;
int EQUAL = 0;
int GREATER = 1;
int compareTo(Object o);
}

编码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package Study.ClassWriter;

import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;

import static org.objectweb.asm.Opcodes.*;

public class HelloWorldGenerateCore {
public static void main(String[] args) throws Exception {
String relative_path = "ClassFilePath/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);

// (1) 生成 byte[] 内容
byte[] bytes = dump();

// (2) 保存 byte[] 到文件
FileUtils.writeBytes(filepath, bytes);
}

public static byte[] dump() throws Exception {
// (1) 创建 ClassWriter 对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

// (2) 调用 visitXxx() 方法
cw.visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, "sample/HelloWorld",
null, "java/lang/Object", new String[]{"java/lang/Cloneable"});

{
FieldVisitor fv1 = cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I", null, -1);
fv1.visitEnd();
}

{
FieldVisitor fv2 = cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I", null, 0);
fv2.visitEnd();
}

{
FieldVisitor fv3 = cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I", null, 1);
fv3.visitEnd();
}

{
MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I", null, null);
mv1.visitEnd();
}


cw.visitEnd();

// (3) 调用 toByteArray() 方法
return cw.toByteArray();
}
}

visitField() 和 visitMethod() 方法

  • visitField (access, name, descriptor, signature, value)
  • visitMethod(access, name, descriptor, signature, exceptions)

这两个方法的前 4 个参数是相同的,第 5 个参数不同

  • access: 用于指定字段或方法的访问标识,如 ACC_PUBLIC(公共)、ACC_STATIC(静态)、ACC_FINAL(最终)等,这些标识符可以组合使用。

  • name: 字符串参数,表示字段或方法的名称。

  • descriptor: 字符串参数,表示字段或方法的描述符,这是一个特殊的字符串,用来描述字段或方法的类型和参数类型,它与Java语言中使用的类型表示法不同,例如,()V 表示无参无返回值的方法,I 表示 int 类型。

  • signature: 可以为 null 或具体的泛型签名字符串,用于表示字段或方法是否包含泛型信息。如果包含泛型,则需要提供具体的泛型签名;如果不包含,则提供 null。

  • value: 仅用于 visitField() 方法,表示字段的初始值。如果字段是一个常量,则需要提供这个常量的值;如果字段不是常量,则使用 null。

  • exceptions: 仅用于 visitMethod() 方法,表示方法可能抛出的异常列表。如果方法有 throws 声明,则提供异常类的完全限定名列表;如果没有,则使用 null。

描述符(descriptor)

  • 对于字段(field)来说,描述符(descriptor)就是对字段本身的类型进行简单化描述。
  • 对于方法(method)来说,描述符(descriptor)就是对方法的接收参数的类型返回值的类型进行简单化描述。
Java 类型 ClassFile 描述符
boolean Z(Z 表示 Zero,零表示 false,非零表示 true
byte B
char C
double D
float F
int I
long J
short S
void V
non-array reference L<InternalName>;
array reference [

对字段描述符的举例:

  • boolean flag: Z
  • byte byteValue: B
  • int intValue: I
  • float floatValue: F
  • double doubleValue: D
  • String strValue: Ljava/lang/String;
  • Object objValue: Ljava/lang/Object;
  • byte[] bytes: [B
  • String[] array: [Ljava/lang/String;
  • Object[][] twoDimArray: [[Ljava/lang/Object;

对方法描述符的举例:

  • int add(int a, int b): (II)I
  • void test(int a, int b): (II)V
  • boolean compare(Object obj): (Ljava/lang/Object;)Z
  • void main(String[] args): ([Ljava/lang/String;)V

示例三:生成类

生成 HelloWorld 类。

1
2
public class HelloWorld {
}

编码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package Study.ClassWriter;

import cmisl.utils.FileUtils;
import org.objectweb.asm.*;

import static org.objectweb.asm.Opcodes.*;

public class HelloWorldGenerateCore {
public static void main(String[] args) throws Exception {
String relative_path = "ClassFilePath/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);

// (1) 生成 byte[] 内容
byte[] bytes = dump();

// (2) 保存 byte[] 到文件
FileUtils.writeBytes(filepath, bytes);
}

public static byte[] dump() throws Exception {
// (1) 创建 ClassWriter 对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

// (2) 调用 visitXxx() 方法
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld",
null, "java/lang/Object", null);

MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();

cw.visitEnd();

// (3) 调用 toByteArray() 方法
return cw.toByteArray();
}
}

<init>() 和 <clinit>() 方法

对于一个类(Class)来说,如果没有提供任何构造方法,Java 编译器会自动生成一个默认构造方法。在所有的 .class 文件中,构造方法的名字是 <init>()

另外,如果在 .class 文件中包含静态代码块,那么就会有一个 <clinit>() 方法。

1
2
3
4
5
6
7
package sample;

public class HelloWorld {
static {
System.out.println("static code block");
}
}

上面的静态代码码,对应于 visitMethod(ACC_STATIC, "<clinit>", "()V", null, null) 的调用。

.class 文件中,构造方法的名字是 <init>(),表示 instance initialization method;静态代码块的名字是 <clinit>(),表示 class initialization method。

FieldVisitor 类

通过调用 ClassVisitor 类的 visitField() 方法,会返回一个 FieldVisitor 类型的对象。

1
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value);

class info

FieldVisitor 类是一个 abstract 类。

1
2
public abstract class FieldVisitor {
}

fields

FieldVisitor 类定义的字段。

1
2
3
4
public abstract class FieldVisitor {
protected final int api;
protected FieldVisitor fv;
}

constructors

FieldVisitor 类定义的构造方法。

1
2
3
4
5
6
7
8
9
10
public abstract class FieldVisitor {
public FieldVisitor(final int api) {
this(api, null);
}

public FieldVisitor(final int api, final FieldVisitor fieldVisitor) {
this.api = api;
this.fv = fieldVisitor;
}
}

methods

FieldVisitor 类定义的方法。

FieldVisitor 类当中,一共定义了 4 个 visitXxx() 方法,对于初学者的我们只需要关注其中的 visitEnd() 方法就可以了。

1
2
3
4
5
6
7
8
9
public abstract class FieldVisitor {
// ......

public void visitEnd() {
if (fv != null) {
fv.visitEnd();
}
}
}

多个 visitXxx() 方法遵循的调用顺序

1
2
3
4
5
6
(
visitAnnotation |
visitTypeAnnotation |
visitAttribute
)*
visitEnd

由于我们只关注 visitEnd() 方法,那么,这个调用顺序就变成如下这样:

1
visitEnd

示例一:字段常量

预期目标

1
2
3
4
public interface HelloWorld {
int intValue = 100;
String strValue = "ABC";
}

编码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;

import static org.objectweb.asm.Opcodes.*;

public class HelloWorldGenerateCore {
public static void main(String[] args) throws Exception {
String relative_path = "ClassFilePath/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);

// (1) 生成 byte[] 内容
byte[] bytes = dump();

// (2) 保存 byte[] 到文件
FileUtils.writeBytes(filepath, bytes);
}

public static byte[] dump() throws Exception {
// (1) 创建 ClassWriter 对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

// (2) 调用 visitXxx() 方法
cw.visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, "sample/HelloWorld", null, "java/lang/Object", null);

{
// 多个 ACC_XXX 之间用 | 或 + 的效果是一样的
FieldVisitor fv1 = cw.visitField(ACC_PUBLIC | ACC_FINAL | ACC_STATIC, "intValue", "I", null, 100);
fv1.visitEnd();
}

{
// 多个 ACC_XXX 之间用 | 或 + 的效果是一样的
FieldVisitor fv2 = cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "strValue", "Ljava/lang/String;", null, "ABC");
fv2.visitEnd();
}

cw.visitEnd();

// (3) 调用 toByteArray() 方法
return cw.toByteArray();
}
}

FieldWriter 类

class info

FieldWriter 类的父类是 FieldVisitor 类。FieldWriter 类并不带有 public 修饰,因此它的有效访问范围只局限于它所处的 package 当中,不能像其它的 public 类一样被外部所使用。

1
2
final class FieldWriter extends FieldVisitor {
}

fields

FieldWriter 类定义的字段。

1
2
3
4
5
6
final class FieldWriter extends FieldVisitor {
private final int accessFlags;
private final int nameIndex;
private final int descriptorIndex;
private Attribute firstAttribute;
}

这些字段与 ClassFile 当中的 field_info 是对应的:

1
2
3
4
5
6
7
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}

constructors

FieldWriter 类定义的构造方法。在 FieldWriter 类当中,只定义了一个构造方法;同时,它也不带有 public 标识,只能在 package 内使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final class FieldWriter extends FieldVisitor {
FieldWriter(SymbolTable symbolTable, int access, String name, String descriptor, String signature, Object constantValue) {
super(Opcodes.ASM9);
this.symbolTable = symbolTable;
this.accessFlags = access;
this.nameIndex = symbolTable.addConstantUtf8(name);
this.descriptorIndex = symbolTable.addConstantUtf8(descriptor);
if (signature != null) {
this.signatureIndex = symbolTable.addConstantUtf8(signature);
}
if (constantValue != null) {
this.constantValueIndex = symbolTable.addConstant(constantValue).index;
}
}
}

methods

FieldWriter 类定义的方法。在 FieldWriter 类当中,有两个重要的方法:computeFieldInfoSize()putFieldInfo() 方法。这两个方法会在 ClassWriter 类的 toByteArray() 方法内使用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
final class FieldWriter extends FieldVisitor {
int computeFieldInfoSize() {
// The access_flags, name_index, descriptor_index and attributes_count fields use 8 bytes.
int size = 8;
// For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS.
if (constantValueIndex != 0) {
// ConstantValue attributes always use 8 bytes.
symbolTable.addConstantUtf8(Constants.CONSTANT_VALUE);
size += 8;
}
// ......
return size;
}

void putFieldInfo(final ByteVector output) {
boolean useSyntheticAttribute = symbolTable.getMajorVersion() < Opcodes.V1_5;
// Put the access_flags, name_index and descriptor_index fields.
int mask = useSyntheticAttribute ? Opcodes.ACC_SYNTHETIC : 0;
output.putShort(accessFlags & ~mask).putShort(nameIndex).putShort(descriptorIndex);
// Compute and put the attributes_count field.
// For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS.
int attributesCount = 0;
if (constantValueIndex != 0) {
++attributesCount;
}
// ......
output.putShort(attributesCount);
// Put the field_info attributes.
// For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS.
if (constantValueIndex != 0) {
output
.putShort(symbolTable.addConstantUtf8(Constants.CONSTANT_VALUE))
.putInt(2)
.putShort(constantValueIndex);
}
// ......
}
}

关于 FieldWriter 类的使用,它主要出现在 ClassWriter 类当中的 visitField()toByteArray() 方法内。

关于Frame

1.static 方法

方法定义

1
2
3
4
5
public class HelloWorld {
public static int add(int a, int b) {
return a + b;
}
}

这是一个简单的静态方法add,它接收两个整数参数并返回它们的和。

初始 Frame

1
[int, int] []

这个表示method frame的初始状态,其中:

  • 第一个[][int, int])表示局部变量表(locals),包含两个整数参数ab
  • 第二个[]表示操作数栈(operand stack),初始为空。

HelloWorldFrameTree 类输出的内容

1
2
3
4
5
6
7
8
9
10
11
add:(II)I
┌───────────────────────────────────────────────────────────┐
│ │ locals: 2
│ operand stack │ stacks: 0
│ ┌───────────────────────────┐ │
│ │ │ local variable │ 0: I
│ ├───────────────────────────┤ ┌─────┬─────┐ │ 1: I
│ │ │ │ 0 │ 1 │ │
│ └───────────────────────────┘ └─────┴─────┘ │
│ │
└───────────────────────────────────────────────────────────┘

这个图示表示当前方法的Frame结构:

  • locals: 2:表示局部变量表包含2个变量。
  • stacks: 0:表示操作数栈当前为空。
  • local variable:显示局部变量表的内容,其中01对应于方法参数ab

字节码指令

使用命令 javap -c sample.HelloWorld 可以看到方法的字节码指令:

1
2
3
4
5
6
public static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturn

这些指令分别表示:

  • 0: iload_0:将局部变量表中索引为0的整数(即参数a)加载到操作数栈上。
  • 1: iload_1:将局部变量表中索引为1的整数(即参数b)加载到操作数栈上。
  • 2: iadd:弹出操作数栈顶部的两个整数并将它们相加,然后将结果压回操作数栈。
  • 3: ireturn:从当前方法返回栈顶的整数值。

Frame 变化

1
2
3
4
5
6
add(II)I
[int, int] []
[int, int] [int]
[int, int] [int, int]
[int, int] [int]
[] []

这个表示方法执行过程中Frame的变化:

  1. 初始状态:[int, int] [](局部变量表有ab,操作数栈为空)。
  2. iload_0[int, int] [int]a被加载到操作数栈)。
  3. iload_1[int, int] [int, int]b被加载到操作数栈)。
  4. iadd[int, int] [int](栈顶的两个整数相加,结果压回栈)。
  5. ireturn[] [](方法返回,Frame被清空)。

或者:

1
2
3
4
5
// {int, int} | {}
0000: iload_0 // {int, int} | {int}
0001: iload_1 // {int, int} | {int, int}
0002: iadd // {int, int} | {int}
0003: ireturn // {} | {}

这也是对Frame变化的另一种表示方式,注释中|左侧表示局部变量表的内容,右侧表示操作数栈的内容。

2.non-static 方法

方法定义

1
2
3
4
5
public class HelloWorld {
public int add(int a, int b) {
return a + b;
}
}

这是一个非静态方法add,它接收两个整数参数并返回它们的和。

初始 Frame

1
[sample/HelloWorld, int, int] []

表示方法的初始Frame,其中:

  • 第一个[][sample/HelloWorld, int, int])表示局部变量表(locals),包含:
    • this引用(sample/HelloWorld
    • 第一个int参数(a
    • 第二个int参数(b
  • 第二个[]表示操作数栈(operand stack),初始为空。

HelloWorldFrameTree 类输出的内容

1
2
3
4
5
6
7
8
9
10
11
add:(II)I
┌─────────────────────────────────────────────────────────────┐
│ │ locals: 3
│ operand stack │ stacks: 0
│ ┌───────────────────────────┐ │
│ │ │ local variable │ 0: HelloWorld
│ ├───────────────────────────┤ ┌─────┬─────┬─────┐ │ 1: I
│ │ │ │ 0 │ 1 │ 2 │ │ 2: I
│ └───────────────────────────┘ └─────┴─────┴─────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

这个图示表示方法的Frame结构:

  • locals: 3:表示局部变量表包含3个位置。
  • stacks: 0:表示操作数栈当前为空。
  • local variable:显示局部变量表的内容,其中0this引用,1是第一个int参数a2是第二个int参数b

字节码指令

方法的字节码指令如下:

1
2
3
4
5
6
public int add(int, int);
Code:
0: iload_1
1: iload_2
2: iadd
3: ireturn

这些指令分别表示:

  • 0: iload_1:将局部变量表中索引为1的位置(即第一个int参数a)加载到操作数栈上。
  • 1: iload_2:将局部变量表中索引为2的位置(即第二个int参数b)加载到操作数栈上。
  • 2: iadd:弹出操作数栈顶部的两个整数并将它们相加,然后将结果压回操作数栈。
  • 3: ireturn:从当前方法返回栈顶的整数值。

Frame 变化

1
2
3
4
5
6
add(II)I
[sample/HelloWorld, int, int] []
[sample/HelloWorld, int, int] [int]
[sample/HelloWorld, int, int] [int, int]
[sample/HelloWorld, int, int] [int]
[] []

这个表示方法执行过程中Frame的变化:

  1. 初始状态:[sample/HelloWorld, int, int] [](局部变量表有thisab,操作数栈为空)。
  2. iload_1[sample/HelloWorld, int, int] [int]a被加载到操作数栈)。
  3. iload_2[sample/HelloWorld, int, int] [int, int]b被加载到操作数栈)。
  4. iadd[sample/HelloWorld, int, int] [int](栈顶的两个整数相加,结果压回栈)。
  5. ireturn[] [](方法返回,Frame被清空)。

或者:

1
2
3
4
5
// {this, int, int} | {}
0000: iload_1 // {this, int, int} | {int}
0001: iload_2 // {this, int, int} | {int, int}
0002: iadd // {this, int, int} | {int}
0003: ireturn // {} | {}

这也是对Frame变化的另一种表示方式,注释中|左侧表示局部变量表的内容,右侧表示操作数栈的内容。

3.long 和 double 类型

这段内容描述了一个Java非静态方法add(long, long)的执行过程及其字节码指令对应的局部变量表(locals)和操作数栈(operand stack)的变化。下面是对这段内容的详细解释:

方法定义

1
2
3
4
5
public class HelloWorld {
public long add(long a, long b) {
return a + b;
}
}

这是一个非静态方法add,它接收两个长整型参数并返回它们的和。

初始 Frame

1
[sample/HelloWorld, long, top, long, top] []

表示方法的初始Frame,其中:

  • 第一个[][sample/HelloWorld, long, top, long, top])表示局部变量表(locals),包含:
    • this引用(sample/HelloWorld
    • 第一个long参数(占用两个位置,longtop
    • 第二个long参数(占用两个位置,longtop
  • 第二个[]表示操作数栈(operand stack),初始为空。

HelloWorldFrameTree 类输出的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
add:(JJ)J
┌───────────────────────────────────────────────────────────────────────────┐
│ │ locals: 5
│ operand stack │ stacks: 0
│ ┌───────────────────────────┐ │
│ │ │ │ 0: HelloWorld
│ ├───────────────────────────┤ │ 1: J
│ │ │ │ 2: .
│ ├───────────────────────────┤ │ 3: J
│ │ │ local variable │ 4: .
│ ├───────────────────────────┤ ┌─────┬─────┬─────┬─────┬─────┐ │
│ │ │ │ 0 │ 1 │ 2 │ 3 │ 4 │ │
│ └───────────────────────────┘ └─────┴─────┴─────┴─────┴─────┘ │
│ │
└───────────────────────────────────────────────────────────────────────────┘

这个图示表示方法的Frame结构:

  • locals: 5:表示局部变量表包含5个位置。
  • stacks: 0:表示操作数栈当前为空。
  • local variable:显示局部变量表的内容,其中0this引用,12是第一个long参数,34是第二个long参数。

字节码指令

方法的字节码指令如下:

1
2
3
4
5
6
public long add(long, long);
Code:
0: lload_1
1: lload_3
2: ladd
3: lreturn

这些指令分别表示:

  • 0: lload_1:将局部变量表中索引为12的位置(即第一个long参数)加载到操作数栈上。
  • 1: lload_3:将局部变量表中索引为34的位置(即第二个long参数)加载到操作数栈上。
  • 2: ladd:弹出操作数栈顶部的两个long值并将它们相加,然后将结果压回操作数栈。
  • 3: lreturn:从当前方法返回栈顶的long值。

Frame 变化

1
2
3
4
5
6
add(JJ)J
[sample/HelloWorld, long, top, long, top] []
[sample/HelloWorld, long, top, long, top] [long, top]
[sample/HelloWorld, long, top, long, top] [long, top, long, top]
[sample/HelloWorld, long, top, long, top] [long, top]
[] []

这个表示方法执行过程中Frame的变化:

  1. 初始状态:[sample/HelloWorld, long, top, long, top] [](局部变量表有thisab,操作数栈为空)。
  2. lload_1[sample/HelloWorld, long, top, long, top] [long, top](第一个long参数被加载到操作数栈)。
  3. lload_3[sample/HelloWorld, long, top, long, top] [long, top, long, top](第二个long参数被加载到操作数栈)。
  4. ladd[sample/HelloWorld, long, top, long, top] [long, top](栈顶的两个long值相加,结果压回栈)。
  5. lreturn[] [](方法返回,Frame被清空)。

或者:

1
2
3
4
5
// {this, long, top, long, top} | {}
0000: lload_1 // {this, long, top, long, top} | {long, top}
0001: lload_3 // {this, long, top, long, top} | {long, top, long, top}
0002: ladd // {this, long, top, long, top} | {long, top}
0003: lreturn // {} | {}

这也是对Frame变化的另一种表示方式,注释中|左侧表示局部变量表的内容,右侧表示操作数栈的内容。

MethodVisitor 类

class info

MethodVisitor 类是一个 abstract 类。

1
2
public abstract class MethodVisitor {
}

fields

MethodVisitor 类定义的字段。

1
2
3
4
public abstract class MethodVisitor {
protected final int api;
protected MethodVisitor mv;
}

constructors

MethodVisitor 类定义的构造方法。

1
2
3
4
5
6
7
8
9
10
public abstract class MethodVisitor {
public MethodVisitor(final int api) {
this(api, null);
}

public MethodVisitor(final int api, final MethodVisitor methodVisitor) {
this.api = api;
this.mv = methodVisitor;
}
}

methods

MethodVisitor 类定义的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public abstract class MethodVisitor {
public void visitCode();

public void visitInsn(final int opcode);
public void visitIntInsn(final int opcode, final int operand);
public void visitVarInsn(final int opcode, final int var);
public void visitTypeInsn(final int opcode, final String type);
public void visitFieldInsn(final int opcode, final String owner, final String name, final String descriptor);
public void visitMethodInsn(final int opcode, final String owner, final String name, final String descriptor,
final boolean isInterface);
public void visitInvokeDynamicInsn(final String name, final String descriptor, final Handle bootstrapMethodHandle,
final Object... bootstrapMethodArguments);
public void visitJumpInsn(final int opcode, final Label label);
public void visitLabel(final Label label);
public void visitLdcInsn(final Object value);
public void visitIincInsn(final int var, final int increment);
public void visitTableSwitchInsn(final int min, final int max, final Label dflt, final Label... labels);
public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels);
public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions);

public void visitTryCatchBlock(final Label start, final Label end, final Label handler, final String type);

public void visitMaxs(final int maxStack, final int maxLocals);
public void visitEnd();

// ......
}

对于这些 visitXxx() 方法,可以从以下资料参阅:

MethodVisitor 类当中,定义了许多的 visitXxx() 方法,这些方法的调用,也要遵循一定的顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(visitParameter)*
[visitAnnotationDefault]
(visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation | visitTypeAnnotation | visitAttribute)*
[
visitCode
(
visitFrame |
visitXxxInsn |
visitLabel |
visitInsnAnnotation |
visitTryCatchBlock |
visitTryCatchAnnotation |
visitLocalVariable |
visitLocalVariableAnnotation |
visitLineNumber
)*
visitMaxs
]
visitEnd

我们可以把这些 visitXxx() 方法分成三组:

  • 第一组,在 visitCode() 方法之前的方法。这一组的方法,主要负责 parameter、annotation 和 attributes 等内容,这些内容并不是方法当中“必不可少”的一部分。
  • 第二组,在 visitCode() 方法和 visitMaxs() 方法之间的方法。这一组的方法,主要负责当前方法的“方法体”内的 opcode 内容。其中,visitCode() 方法,标志着方法体的开始,而 visitMaxs() 方法,标志着方法体的结束。
  • 第三组,是 visitEnd() 方法。这个 visitEnd() 方法,是最后一个进行调用的方法。

对这些 visitXxx() 方法进行精简之后,内容如下:

1
2
3
4
5
6
7
8
9
10
11
[
visitCode
(
visitFrame |
visitXxxInsn |
visitLabel |
visitTryCatchBlock
)*
visitMaxs
]
visitEnd

这些方法的调用顺序,可以记忆如下:

  • 第一步,调用 visitCode() 方法,调用一次。
  • 第二步,调用 visitXxxInsn() 方法,可以调用多次。对这些方法的调用,就是在构建方法的“方法体”。
  • 第三步,调用 visitMaxs() 方法,调用一次。
  • 第四步,调用 visitEnd() 方法,调用一次。
1
2
3
4
5
6
7
8
9
10
11
12
13
                 ┌─── visitCode()

│ ┌─── visitXxxInsn()
│ │
│ ├─── visitLabel()
├─── visitXxxInsn() ───┤
MethodVisitor ───┤ ├─── visitFrame()
│ │
│ └─── visitTryCatchBlock()

├─── visitMaxs()

└─── visitEnd()

示例一:<init>() 方法

.class 文件中,构造方法的名字是 <init>,它表示 instance initialization method 的缩写。

编写代码实现以下.class文件

1
2
public class HelloWorld {
}

或者:

1
2
3
4
5
public class HelloWorld {
public HelloWorld() {
super();
}
}

编码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package Study.MethodVisitor;

import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;

import static org.objectweb.asm.Opcodes.*;

public class HelloWorldGenerateCore {
public static void main(String[] args) throws Exception {
String relative_path = "ClassFilePath/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);

// (1) 生成 byte[] 内容
byte[] bytes = dump();

// (2) 保存 byte[] 到文件
FileUtils.writeBytes(filepath, bytes);
}

public static byte[] dump() throws Exception {
// (1) 创建 ClassWriter 对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

// (2) 调用 visitXxx() 方法
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld", null, "java/lang/Object", null);

{
MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv1.visitCode();
mv1.visitVarInsn(ALOAD, 0);
mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv1.visitInsn(RETURN);
mv1.visitMaxs(1, 1);
mv1.visitEnd();
}

cw.visitEnd();

// (3) 调用 toByteArray() 方法
return cw.toByteArray();
}
}

示例二:<clinit> 方法

.class 文件中,静态初始化方法的名字是 <clinit>,它表示class initialization method 的缩写。它的方法描述符是 ()V

1
2
3
4
5
public class HelloWorld {
static {
System.out.println("class initialization method");
}
}

编码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package Study.MethodVisitor;

import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;

import static org.objectweb.asm.Opcodes.*;

public class HelloWorldGenerateCore {
public static void main(String[] args) throws Exception {
String relative_path = "ClassFilePath/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);

// (1) 生成 byte[] 内容
byte[] bytes = dump();

// (2) 保存 byte[] 到文件
FileUtils.writeBytes(filepath, bytes);
}

public static byte[] dump() throws Exception {
// (1) 创建 ClassWriter 对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

// (2) 调用 visitXxx() 方法
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld", null, "java/lang/Object", null);

{
MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv1.visitCode();
mv1.visitVarInsn(ALOAD, 0);
mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv1.visitInsn(RETURN);
mv1.visitMaxs(1, 1);
mv1.visitEnd();
}

{
MethodVisitor mv2 = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
mv2.visitCode();
mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv2.visitLdcInsn("class initialization method");
mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv2.visitInsn(RETURN);
mv2.visitMaxs(2, 0);
mv2.visitEnd();
}

cw.visitEnd();

// (3) 调用 toByteArray() 方法
return cw.toByteArray();
}
}

示例三:创建对象

假如有一个 GoodChild 类,内容如下:

1
2
3
4
5
6
7
8
9
public class GoodChild {
public String name;
public int age;

public GoodChild(String name, int age) {
this.name = name;
this.age = age;
}
}

我们的预期目标是生成一个 HelloWorld 类:

1
2
3
4
5
public class HelloWorld {
public void test() {
GoodChild child = new GoodChild("Lucy", 8);
}
}

编码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package Study.MethodVisitor;

import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;

import static org.objectweb.asm.Opcodes.*;

public class HelloWorldGenerateCore {
public static void main(String[] args) throws Exception {
String relative_path = "ClassFilePath/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);

// (1) 生成 byte[] 内容
byte[] bytes = dump();

// (2) 保存 byte[] 到文件
FileUtils.writeBytes(filepath, bytes);
}

public static byte[] dump() throws Exception {
// (1) 创建 ClassWriter 对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

// (2) 调用 visitXxx() 方法
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld", null, "java/lang/Object", null);

{
MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv1.visitCode();
mv1.visitVarInsn(ALOAD, 0);
mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv1.visitInsn(RETURN);
mv1.visitMaxs(1, 1);
mv1.visitEnd();
}

{
MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
mv2.visitCode();
mv2.visitTypeInsn(NEW, "sample/GoodChild");
mv2.visitInsn(DUP);
mv2.visitLdcInsn("Lucy");
mv2.visitIntInsn(BIPUSH, 8);
mv2.visitMethodInsn(INVOKESPECIAL, "sample/GoodChild", "<init>", "(Ljava/lang/String;I)V", false);
mv2.visitVarInsn(ASTORE, 1);
mv2.visitInsn(RETURN);
mv2.visitMaxs(4, 2);
mv2.visitEnd();
}

cw.visitEnd();

// (3) 调用 toByteArray() 方法
return cw.toByteArray();
}
}

示例四:调用方法

1
2
3
4
5
6
public class HelloWorld {
public void test(int a, int b) {
int val = Math.max(a, b); // 对 static 方法进行调用
System.out.println(val); // 对 non-static 方法进行调用
}
}

编码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package Study.MethodVisitor;

import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;

import static org.objectweb.asm.Opcodes.*;

public class HelloWorldGenerateCore {
public static void main(String[] args) throws Exception {
String relative_path = "ClassFilePath/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);

// (1) 生成 byte[] 内容
byte[] bytes = dump();

// (2) 保存 byte[] 到文件
FileUtils.writeBytes(filepath, bytes);
}

public static byte[] dump() throws Exception {
// (1) 创建 ClassWriter 对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

// (2) 调用 visitXxx() 方法
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld", null, "java/lang/Object", null);

{
MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv1.visitCode();
mv1.visitVarInsn(ALOAD, 0);
mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv1.visitInsn(RETURN);
mv1.visitMaxs(1, 1);
mv1.visitEnd();
}

{
MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "(II)V", null, null);
mv2.visitCode();
mv2.visitVarInsn(ILOAD, 1);
mv2.visitVarInsn(ILOAD, 2);
mv2.visitMethodInsn(INVOKESTATIC, "java/lang/Math", "max", "(II)I", false);
mv2.visitVarInsn(ISTORE, 3);
mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv2.visitVarInsn(ILOAD, 3);
mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
mv2.visitInsn(RETURN);
mv2.visitMaxs(2, 4);
mv2.visitEnd();
}

cw.visitEnd();

// (3) 调用 toByteArray() 方法
return cw.toByteArray();
}
}

上面代码如果看不懂,以下是ai解释

这段代码利用ASM库生成了一个Java类,并为该类定义了两个方法:一个构造方法和一个名为test的方法。下面我们逐步解释每一部分的代码。

1. 构造方法

1
2
3
4
5
6
7
8
9
>{
>MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
>mv1.visitCode();
>mv1.visitVarInsn(ALOAD, 0);
>mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
>mv1.visitInsn(RETURN);
>mv1.visitMaxs(1, 1);
>mv1.visitEnd();
>}

1.1 创建构造方法

1
>MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
  • 创建一个方法访问器,用来生成新的构造方法。
  • ACC_PUBLIC: 构造方法的访问权限是public
  • "<init>": 构造方法的名称。
  • "()V": 构造方法的描述符,表示无参数且返回类型为void.
  • null, null: 不设置方法的签名和异常。

1.2 生成方法代码

1
>mv1.visitCode();
  • 开始生成方法体代码。

1.3 调用父类构造方法

1
2
>mv1.visitVarInsn(ALOAD, 0);
>mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
  • ALOAD 0: 将this引用加载到操作数栈上(局部变量索引0)。
  • INVOKESPECIAL: 调用父类的实例初始化方法。
  • "java/lang/Object": 类是java.lang.Object
  • "<init>": 方法名是<init>
  • "()V": 方法描述符,无参数且返回类型为void
  • false: 不是接口方法。

1.4 返回

1
>mv1.visitInsn(RETURN);
  • RETURN: 从方法返回。

1.5 设置最大堆栈深度和局部变量表大小

1
>mv1.visitMaxs(1, 1);
  • 1: 方法执行期间操作数栈的最大深度是1。
  • 1: 方法的局部变量表大小是1(this引用)。

1.6 结束方法生成

1
>mv1.visitEnd();
  • 结束方法生成。

2. test 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>{
MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "(II)V", null, null);
mv2.visitCode();
mv2.visitVarInsn(ILOAD, 1);
mv2.visitVarInsn(ILOAD, 2);
mv2.visitMethodInsn(INVOKESTATIC, "java/lang/Math", "max", "(II)I", false);
mv2.visitVarInsn(ISTORE, 3);
mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv2.visitVarInsn(ILOAD, 3);
mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
mv2.visitInsn(RETURN);
mv2.visitMaxs(2, 4);
mv2.visitEnd();
>}

2.1 创建test方法

1
>MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "(II)V", null, null);
  • 创建一个方法访问器,用来生成新的test方法。
  • ACC_PUBLIC: 方法的访问权限是public
  • "test": 方法名是test
  • "(II)V": 方法描述符,表示接受两个int参数且返回类型为void
  • null, null: 不设置方法的签名和异常。

2.2 生成方法代码

1
>mv2.visitCode();
  • 开始生成方法体代码。

2.3 加载方法参数

1
2
>mv2.visitVarInsn(ILOAD, 1);
>mv2.visitVarInsn(ILOAD, 2);
  • ILOAD 1: 将第一个参数(局部变量索引1)加载到操作数栈。
  • ILOAD 2: 将第二个参数(局部变量索引2)加载到操作数栈。

2.4 调用Math.max方法

1
>mv2.visitMethodInsn(INVOKESTATIC, "java/lang/Math", "max", "(II)I", false);
  • INVOKESTATIC: 调用静态方法。
  • "java/lang/Math": 类是java.lang.Math
  • "max": 方法名是max
  • "(II)I": 方法描述符,接受两个int参数且返回一个int
  • false: 不是接口方法。
  • 将两个整数从操作数栈中弹出,计算它们的最大值,并将结果压回操作数栈。

2.5 存储结果

1
>mv2.visitVarInsn(ISTORE, 3);
  • ISTORE 3: 将操作数栈顶部的值存储到局部变量表索引3的位置。

2.6 获取System.out输出流

1
>mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
  • GETSTATIC: 获取静态字段的值。
  • "java/lang/System": 类是java.lang.System
  • "out": 字段名是out
  • "Ljava/io/PrintStream;": 字段类型是java.io.PrintStream
  • System.out压入操作数栈。

2.7 加载结果

1
>mv2.visitVarInsn(ILOAD, 3);
  • ILOAD 3: 将局部变量表索引3的值加载到操作数栈。

2.8 调用println方法

1
>mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
  • INVOKEVIRTUAL: 调用实例方法。
  • "java/io/PrintStream": 类是java.io.PrintStream
  • "println": 方法名是println
  • "(I)V": 方法描述符,接受一个int参数且返回void
  • false: 不是接口方法。
  • 从操作数栈中弹出值并调用println方法,打印最大值。

2.9 返回

1
>mv2.visitInsn(RETURN);
  • RETURN: 从方法返回。

2.10 设置最大堆栈深度和局部变量表大小

1
>mv2.visitMaxs(2, 4);
  • 2: 方法执行期间操作数栈的最大深度是2。
  • 4: 方法的局部变量表大小是4(this、两个参数和一个局部变量)。

2.11 结束方法生成

1
>mv2.visitEnd();
  • 结束方法生成。

总结

这段代码利用ASM库生成了一个类,并为该类定义了以下两个方法:

  1. 构造方法
  • 访问权限:public
  • 名称:<init>
  • 描述符:()V(无参数且返回类型为void
  • 功能:调用父类java.lang.Object的构造方法。
  1. test 方法
  • 访问权限:public
  • 名称:test
  • 描述符:(II)V(接受两个int参数且返回类型为void
  • 功能:计算两个整数中的最大值,并将结果打印到控制台。

通过设置最大操作数栈深度和局部变量表大小,确保生成的字节码能够被JVM正确加载和执行。

更细节的如下

当然可以!我们可以通过模拟test方法的执行过程,来帮助你理解每一步指令的作用。这是一个很好的方式来深入理解如何通过ASM生成字节码。

test 方法回顾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>{
MethodVisitor mv2 = cw.visitMethod(Opcodes.ACC_PUBLIC, "test", "(II)V", null, null);
mv2.visitCode();
mv2.visitVarInsn(Opcodes.ILOAD, 1);
mv2.visitVarInsn(Opcodes.ILOAD, 2);
mv2.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Math", "max", "(II)I", false);
mv2.visitVarInsn(Opcodes.ISTORE, 3);
mv2.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv2.visitVarInsn(Opcodes.ILOAD, 3);
mv2.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
mv2.visitInsn(Opcodes.RETURN);
mv2.visitMaxs(2, 4);
mv2.visitEnd();
>}

我们将逐行分析这段方法,并模拟其执行过程。

初始状态

在方法调用开始时,局部变量表和操作数栈的状态如下:

局部变量表

索引 变量
0 this
1 int a
2 int b
3 未使用

操作数栈

为空。

步骤 1:加载第一个参数(a)

1
>mv2.visitVarInsn(Opcodes.ILOAD, 1);

局部变量表

索引 变量
0 this
1 int a
2 int b
3 未使用

操作数栈

栈顶
int a

步骤 2:加载第二个参数(b)

1
>mv2.visitVarInsn(Opcodes.ILOAD, 2);

局部变量表

索引 变量
0 this
1 int a
2 int b
3 未使用

操作数栈

栈顶
int b
int a

步骤 3:调用Math.max方法

1
>mv2.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Math", "max", "(II)I", false);
  • 该指令从操作数栈中弹出两个整数ab
  • 计算它们的最大值。
  • 将结果压回操作数栈。

假设a = 5b = 3,则最大值是5

局部变量表

索引 变量
0 this
1 int a
2 int b
3 未使用

操作数栈

栈顶
int 5

步骤 4:将结果存储到局部变量

1
>mv2.visitVarInsn(Opcodes.ISTORE, 3);
  • 将操作数栈顶部的值存储到局部变量索引3的位置。

局部变量表

索引 变量
0 this
1 int a
2 int b
3 int 5

操作数栈

为空。

步骤 5:获取System.out输出流

1
>mv2.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
  • 获取System.out的静态字段值,并将其压入操作数栈。

局部变量表

索引 变量
0 this
1 int a
2 int b
3 int 5

操作数栈

栈顶
PrintStream

步骤 6:加载局部变量中的结果

1
>mv2.visitVarInsn(Opcodes.ILOAD, 3);
  • 将局部变量索引3的值加载到操作数栈。

局部变量表

索引 变量
0 this
1 int a
2 int b
3 int 5

操作数栈

栈顶
int 5
PrintStream

步骤 7:调用println方法

1
>mv2.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
  • 从操作数栈中弹出两个操作数:一个PrintStream对象和一个int
  • 调用PrintStream对象的println方法,打印整数值。

局部变量表

索引 变量
0 this
1 int a
2 int b
3 int 5

操作数栈

为空。

步骤 8:方法返回

1
>mv2.visitInsn(Opcodes.RETURN);
  • 执行返回指令,方法结束。

步骤 9:设置最大堆栈深度和局部变量表大小

1
>mv2.visitMaxs(2, 4);
  • 2: 方法执行期间操作数栈的最大深度是2。
  • 4: 方法的局部变量表大小是4(this、两个参数和一个局部变量)。

步骤 10:结束方法生成

1
>mv2.visitEnd();
  • 结束方法生成。

总结

通过逐步模拟test方法的执行,我们可以看到每一条字节码指令如何操作局部变量表和操作数栈。希望这个详细的模拟过程能帮助你更好地理解字节码生成的过程。如果你有任何问题,随时提问!

示例五:不调用 visitMaxs() 方法

在创建 ClassWriter 对象时,使用了 ClassWriter.COMPUTE_FRAMES 选项。

1
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

使用 ClassWriter.COMPUTE_FRAMES 后,ASM 会自动计算 max stacks、max locals 和 stack map frames 的具体值。 从代码的角度来说,使用 ClassWriter.COMPUTE_FRAMES,会忽略我们在代码中 visitMaxs() 方法和 visitFrame() 方法传入的具体参数值。 换句话说,无论我们传入的参数值是否正确,ASM 会帮助我们从新计算一个正确的值,代替我们在代码中传入的参数。

  • 第 1 种情况,在创建 ClassWriter 对象时,flags 参数使用 ClassWriter.COMPUTE_FRAMES 值,在调用 mv.visitMaxs(0, 0) 方法之后,仍然能得到一个正确的 .class 文件。
  • 第 2 种情况,在创建 ClassWriter 对象时,flags 参数使用 0 值,在调用 mv.visitMaxs(0, 0) 方法之后,得到的 .class 文件就不能正确运行。

需要注意的是,在创建 ClassWriter 对象时,flags 参数使用 ClassWriter.COMPUTE_FRAMES 值,我们可以给 visitMaxs() 方法传入一个错误的值,但是不能省略对于 visitMaxs() 方法的调用。 如果我们省略掉 visitCode()visitEnd() 方法,生成的 .class 文件也不会出错;当然,并不建议这么做。但是,如果我们省略掉对于 visitMaxs() 方法的调用,生成的 .class 文件就会出错。

如果省略掉对于 visitMaxs() 方法的调用,会出现如下错误:

1
Exception in thread "main" java.lang.VerifyError: Operand stack overflow

示例六:不同的 MethodVisitor 交叉使用

假如我们有两个 MethodVisitor 对象 mv1mv2,如下所示:

1
2
MethodVisitor mv1 = cw.visitMethod(...);
MethodVisitor mv2 = cw.visitMethod(...);

同时,我们也知道 MethodVisitor 类里的 visitXxx() 方法需要遵循一定的调用顺序:

  • 第一步,调用 visitCode() 方法,调用一次
  • 第二步,调用 visitXxxInsn() 方法,可以调用多次
  • 第三步,调用 visitMaxs() 方法,调用一次
  • 第四步,调用 visitEnd() 方法,调用一次

对于 mv1mv2 这两个对象来说,它们的 visitXxx() 方法的调用顺序是彼此独立的、不会相互干扰。

一般情况下,我们可以如下写代码,这样逻辑比较清晰:

1
2
3
4
5
6
7
8
9
10
11
MethodVisitor mv1 = cw.visitMethod(...);
mv1.visitCode(...);
mv1.visitXxxInsn(...)
mv1.visitMaxs(...);
mv1.visitEnd();

MethodVisitor mv2 = cw.visitMethod(...);
mv2.visitCode(...);
mv2.visitXxxInsn(...)
mv2.visitMaxs(...);
mv2.visitEnd();

但是,我们也可以这样来写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
MethodVisitor mv1 = cw.visitMethod(...);
MethodVisitor mv2 = cw.visitMethod(...);

mv1.visitCode(...);
mv2.visitCode(...);

mv2.visitXxxInsn(...)
mv1.visitXxxInsn(...)

mv1.visitMaxs(...);
mv1.visitEnd();
mv2.visitMaxs(...);
mv2.visitEnd();

在上面的代码中,mv1mv2 这两个对象的 visitXxx() 方法交叉调用,这是可以的。 换句话说,只要每一个 MethodVisitor 对象在调用 visitXxx() 方法时,遵循了调用顺序,那结果就是正确的; 不同的 MethodVisitor 对象,是相互独立的、不会彼此影响。

那么,可能有的同学会问:MethodVisitor 对象交叉使用有什么作用呢?有没有什么场景下的应用呢?回答是“有的”。 在 ASM 当中,有一个 org.objectweb.asm.commons.StaticInitMerger 类,其中有一个 MethodVisitor mergedClinitVisitor 字段,它就是一个很好的示例,在后续内容中,我们会介绍到这个类。

预期目标

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.Date;

public class HelloWorld {
public void test() {
System.out.println("This is a test method.");
}

public void printDate() {
Date now = new Date();
System.out.println(now);
}
}

编码实现(第一种方式,顺序)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;

import static org.objectweb.asm.Opcodes.*;

public class HelloWorldGenerateCore {
public static void main(String[] args) throws Exception {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);

// (1) 生成 byte[] 内容
byte[] bytes = dump();

// (2) 保存 byte[] 到文件
FileUtils.writeBytes(filepath, bytes);
}

public static byte[] dump() throws Exception {
// (1) 创建 ClassWriter 对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

// (2) 调用 visitXxx() 方法
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld",
null, "java/lang/Object", null);

{
MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv1.visitCode();
mv1.visitVarInsn(ALOAD, 0);
mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv1.visitInsn(RETURN);
mv1.visitMaxs(1, 1);
mv1.visitEnd();
}

{
MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
mv2.visitCode();
mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv2.visitLdcInsn("This is a test method.");
mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv2.visitInsn(RETURN);
mv2.visitMaxs(2, 1);
mv2.visitEnd();
}

{
MethodVisitor mv3 = cw.visitMethod(ACC_PUBLIC, "printDate", "()V", null, null);
mv3.visitCode();
mv3.visitTypeInsn(NEW, "java/util/Date");
mv3.visitInsn(DUP);
mv3.visitMethodInsn(INVOKESPECIAL, "java/util/Date", "<init>", "()V", false);
mv3.visitVarInsn(ASTORE, 1);
mv3.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv3.visitVarInsn(ALOAD, 1);
mv3.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V", false);
mv3.visitInsn(RETURN);
mv3.visitMaxs(2, 2);
mv3.visitEnd();
}

cw.visitEnd();

// (3) 调用 toByteArray() 方法
return cw.toByteArray();
}
}

编码实现(第二种方式,交叉)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;

import static org.objectweb.asm.Opcodes.*;

public class HelloWorldGenerateCore {
public static void main(String[] args) throws Exception {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);

// (1) 生成 byte[] 内容
byte[] bytes = dump();

// (2) 保存 byte[] 到文件
FileUtils.writeBytes(filepath, bytes);
}

public static byte[] dump() throws Exception {
// (1) 创建 ClassWriter 对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

// (2) 调用 visitXxx() 方法
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "sample/HelloWorld",
null, "java/lang/Object", null);

{
MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv1.visitCode();
mv1.visitVarInsn(ALOAD, 0);
mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv1.visitInsn(RETURN);
mv1.visitMaxs(1, 1);
mv1.visitEnd();
}

{
// 第 1 部分,mv2
MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "test", "()V", null, null);

// 第 2 部分,mv3
MethodVisitor mv3 = cw.visitMethod(ACC_PUBLIC, "printDate", "()V", null, null);
mv3.visitCode();
mv3.visitTypeInsn(NEW, "java/util/Date");
mv3.visitInsn(DUP);
mv3.visitMethodInsn(INVOKESPECIAL, "java/util/Date", "<init>", "()V", false);

// 第 3 部分,mv2
mv2.visitCode();
mv2.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv2.visitLdcInsn("This is a test method.");
mv2.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

// 第 4 部分,mv3
mv3.visitVarInsn(ASTORE, 1);
mv3.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv3.visitVarInsn(ALOAD, 1);
mv3.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V", false);

// 第 5 部分,mv2
mv2.visitInsn(RETURN);
mv2.visitMaxs(2, 1);
mv2.visitEnd();

// 第 6 部分,mv3
mv3.visitInsn(RETURN);
mv3.visitMaxs(2, 2);
mv3.visitEnd();
}

cw.visitEnd();

// (3) 调用 toByteArray() 方法
return cw.toByteArray();
}
}

MethodWriter 类

class info

MethodWriter 类的父类是 MethodVisitor 类。MethodWriter 类并不带有 public 修饰,因此它的有效访问范围只局限于它所处的 package 当中,不能像其它的 public 类一样被外部所使用。

1
2
final class MethodWriter extends MethodVisitor {
}

fields

MethodWriter 类定义的字段。

1
2
3
4
5
6
7
8
final class MethodWriter extends MethodVisitor {
private final int accessFlags;
private final int nameIndex;
private final String name;
private final int descriptorIndex;
private final String descriptor;
private Attribute firstAttribute;
}

这些字段与 ClassFile 当中的 method_info 也是对应的:

1
2
3
4
5
6
7
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}

下面的几个字段,是与“方法体”直接相关的几个字段:

1
2
3
4
5
6
7
8
9
10
final class MethodWriter extends MethodVisitor {
private int maxStack;
private int maxLocals;
private final ByteVector code = new ByteVector();
private Handler firstHandler;
private Handler lastHandler;
private final int numberOfExceptions;
private final int[] exceptionIndexTable;
private Attribute firstCodeAttribute;
}

这些字段对应于 Code 属性结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

constructors

MethodWriter 类定义的构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
final class MethodWriter extends MethodVisitor {
MethodWriter(SymbolTable symbolTable, int access, String name, String descriptor, String signature, String[] exceptions, int compute) {
super(Opcodes.ASM9);
this.symbolTable = symbolTable;
this.accessFlags = "<init>".equals(name) ? access | Constants.ACC_CONSTRUCTOR : access;
this.nameIndex = symbolTable.addConstantUtf8(name);
this.name = name;
this.descriptorIndex = symbolTable.addConstantUtf8(descriptor);
this.descriptor = descriptor;
this.signatureIndex = signature == null ? 0 : symbolTable.addConstantUtf8(signature);
if (exceptions != null && exceptions.length > 0) {
numberOfExceptions = exceptions.length;
this.exceptionIndexTable = new int[numberOfExceptions];
for (int i = 0; i < numberOfExceptions; ++i) {
this.exceptionIndexTable[i] = symbolTable.addConstantClass(exceptions[i]).index;
}
} else {
numberOfExceptions = 0;
this.exceptionIndexTable = null;
}
this.compute = compute;
if (compute != COMPUTE_NOTHING) {
// Update maxLocals and currentLocals.
int argumentsSize = Type.getArgumentsAndReturnSizes(descriptor) >> 2;
if ((access & Opcodes.ACC_STATIC) != 0) {
--argumentsSize;
}
maxLocals = argumentsSize;
currentLocals = argumentsSize;
// Create and visit the label for the first basic block.
firstBasicBlock = new Label();
visitLabel(firstBasicBlock);
}
}
}

methods

MethodWriter 类定义的方法。

MethodWriter 类当中,也有两个重要的方法:computeMethodInfoSize()putMethodInfo() 方法。这两个方法也是在 ClassWriter 类的 toByteArray() 方法内使用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
final class MethodWriter extends MethodVisitor {
int computeMethodInfoSize() {
// ......
// 2 bytes each for access_flags, name_index, descriptor_index and attributes_count.
int size = 8;
// For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS.
if (code.length > 0) {
if (code.length > 65535) {
throw new MethodTooLargeException(symbolTable.getClassName(), name, descriptor, code.length);
}
symbolTable.addConstantUtf8(Constants.CODE);
// The Code attribute has 6 header bytes, plus 2, 2, 4 and 2 bytes respectively for max_stack,
// max_locals, code_length and attributes_count, plus the ByteCode and the exception table.
size += 16 + code.length + Handler.getExceptionTableSize(firstHandler);
if (stackMapTableEntries != null) {
boolean useStackMapTable = symbolTable.getMajorVersion() >= Opcodes.V1_6;
symbolTable.addConstantUtf8(useStackMapTable ? Constants.STACK_MAP_TABLE : "StackMap");
// 6 header bytes and 2 bytes for number_of_entries.
size += 8 + stackMapTableEntries.length;
}
// ......
}
if (numberOfExceptions > 0) {
symbolTable.addConstantUtf8(Constants.EXCEPTIONS);
size += 8 + 2 * numberOfExceptions;
}
//......
return size;
}

void putMethodInfo(final ByteVector output) {
boolean useSyntheticAttribute = symbolTable.getMajorVersion() < Opcodes.V1_5;
int mask = useSyntheticAttribute ? Opcodes.ACC_SYNTHETIC : 0;
output.putShort(accessFlags & ~mask).putShort(nameIndex).putShort(descriptorIndex);
// ......
// For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS.
int attributeCount = 0;
if (code.length > 0) {
++attributeCount;
}
if (numberOfExceptions > 0) {
++attributeCount;
}
// ......
// For ease of reference, we use here the same attribute order as in Section 4.7 of the JVMS.
output.putShort(attributeCount);
if (code.length > 0) {
// 2, 2, 4 and 2 bytes respectively for max_stack, max_locals, code_length and
// attributes_count, plus the ByteCode and the exception table.
int size = 10 + code.length + Handler.getExceptionTableSize(firstHandler);
int codeAttributeCount = 0;
if (stackMapTableEntries != null) {
// 6 header bytes and 2 bytes for number_of_entries.
size += 8 + stackMapTableEntries.length;
++codeAttributeCount;
}
// ......
output
.putShort(symbolTable.addConstantUtf8(Constants.CODE))
.putInt(size)
.putShort(maxStack)
.putShort(maxLocals)
.putInt(code.length)
.putByteArray(code.data, 0, code.length);
Handler.putExceptionTable(firstHandler, output);
output.putShort(codeAttributeCount);
// ......
}
if (numberOfExceptions > 0) {
output
.putShort(symbolTable.addConstantUtf8(Constants.EXCEPTIONS))
.putInt(2 + 2 * numberOfExceptions)
.putShort(numberOfExceptions);
for (int exceptionIndex : exceptionIndexTable) {
output.putShort(exceptionIndex);
}
}
// ......
}
}

关于 MethodWriter 类的使用,在 ClassWriter 类当中,visitMethod() 方法代码如下:

1
2
3
4
5
6
7
8
9
10
11
public class ClassWriter extends ClassVisitor {
public final MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodWriter methodWriter = new MethodWriter(symbolTable, access, name, descriptor, signature, exceptions, compute);
if (firstMethod == null) {
firstMethod = methodWriter;
} else {
lastMethod.mv = methodWriter;
}
return lastMethod = methodWriter;
}
}

toByteArray 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class ClassWriter extends ClassVisitor {
public byte[] toByteArray() {

// First step: compute the size in bytes of the ClassFile structure.
// The magic field uses 4 bytes, 10 mandatory fields (minor_version, major_version,
// constant_pool_count, access_flags, this_class, super_class, interfaces_count, fields_count,
// methods_count and attributes_count) use 2 bytes each, and each interface uses 2 bytes too.
int size = 24 + 2 * interfaceCount;
// ......
int methodsCount = 0;
MethodWriter methodWriter = firstMethod;
while (methodWriter != null) {
++methodsCount;
size += methodWriter.computeMethodInfoSize(); // 这里是对 MethodWriter.computeMethodInfoSize() 方法的调用
methodWriter = (MethodWriter) methodWriter.mv;
}
// ......

// Second step: allocate a ByteVector of the correct size (in order to avoid any array copy in
// dynamic resizes) and fill it with the ClassFile content.
ByteVector result = new ByteVector(size);
result.putInt(0xCAFEBABE).putInt(version);
symbolTable.putConstantPool(result);
int mask = (version & 0xFFFF) < Opcodes.V1_5 ? Opcodes.ACC_SYNTHETIC : 0;
result.putShort(accessFlags & ~mask).putShort(thisClass).putShort(superClass);
result.putShort(interfaceCount);
for (int i = 0; i < interfaceCount; ++i) {
result.putShort(interfaces[i]);
}
// ......
result.putShort(methodsCount);
boolean hasFrames = false;
boolean hasAsmInstructions = false;
methodWriter = firstMethod;
while (methodWriter != null) {
hasFrames |= methodWriter.hasFrames();
hasAsmInstructions |= methodWriter.hasAsmInstructions();
methodWriter.putMethodInfo(result); // 这里是对 MethodWriter.putMethodInfo() 方法的调用
methodWriter = (MethodWriter) methodWriter.mv;
}
// ......

// Third step: replace the ASM specific instructions, if any.
if (hasAsmInstructions) {
return replaceAsmInstructions(result.data, hasFrames);
} else {
return result.data;
}
}
}

关于 MethodWriter 类的使用,它主要出现在 ClassWriter 类当中的 visitMethod()toByteArray() 方法内。

ClassReader 类

ClassReader 类和 ClassWriter 类,从功能角度来说,是完全相反的两个类,一个用于读取 .class 文件,另一个用于生成 .class 文件。

class info

ClassWriter 类不同的是,ClassReader 类并没有继承自 ClassVisitor 类。

1
2
public class ClassReader {
}

ClassWriter 类的定义如下:

1
2
public class ClassWriter extends ClassVisitor {
}

fields

ClassReader 类定义的字段有哪些。主要是以下三个片段: classFileBuffer 字段、cpInfoOffsets 字段和 header 字段。

1
2
3
4
5
6
7
8
public class ClassReader {
// 第 1 组,真实的数据部分
final byte[] classFileBuffer;

// 第 2 组,数据的索引信息
private final int[] cpInfoOffsets;
public final int header;
}

这 3 个字段能够体现出 ClassReader 类处理 .class 文件的整体思路:

  • 第 1 组,classFileBuffer 字段:它里面包含的信息,就是从 .class 文件中读取出来的字节码数据。
  • 第 2 组,cpInfoOffsets 字段和 header 字段:它们分别标识了 classFileBuffer 中数据里包含的常量池(constant pool)和访问标识(access flag)的位置信息。

我们拿到 classFileBuffer 字段后,一个主要目的就是对它的内容进行修改,来实现一个新的功能。它处理的大体思路是这样的:

1
.class 文件 --> ClassReader --> byte[] --> 经过各种转换 --> ClassWriter --> byte[] --> .class 文件

constructors

ClassReader 类定义的构造方法。在 ClassReader 类当中定义了 5 个构造方法。但是,从本质上来说,这 5 个构造方法本质上是同一个构造方法的不同表现形式。其中,最常用的构造方法有两个:

  • 第一个是 ClassReader cr = new ClassReader("sample.HelloWorld");
  • 第二个是 ClassReader cr = new ClassReader(bytes);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ClassReader {

public ClassReader(final String className) throws IOException { // 第一个构造方法(常用)
this(
readStream(ClassLoader.getSystemResourceAsStream(className.replace('.', '/') + ".class"), true)
);
}

public ClassReader(final byte[] classFile) { // 第二个构造方法(常用)
this(classFile, 0, classFile.length);
}

public ClassReader(final byte[] classFileBuffer, final int classFileOffset, final int classFileLength) {
this(classFileBuffer, classFileOffset, true);
}

ClassReader( // 这是最根本、最本质的构造方法
final byte[] classFileBuffer,
final int classFileOffset,
final boolean checkClassVersion) {
// ......
}

public ClassReader(InputStream inputStream) throws IOException {
this(readStream(inputStream, false));
}
}

methods

ClassReader 类定义的方法。

getXxx() 方法

这里介绍的几个 getXxx() 方法,都是在 header 字段的基础上获得的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ClassReader {
public int getAccess() {
return readUnsignedShort(header);
}

public String getClassName() {
// this_class is just after the access_flags field (using 2 bytes).
return readClass(header + 2, new char[maxStringLength]);
}

public String getSuperName() {
// super_class is after the access_flags and this_class fields (2 bytes each).
return readClass(header + 4, new char[maxStringLength]);
}

public String[] getInterfaces() {
// interfaces_count is after the access_flags, this_class and super_class fields (2 bytes each).
int currentOffset = header + 6;
int interfacesCount = readUnsignedShort(currentOffset);
String[] interfaces = new String[interfacesCount];
if (interfacesCount > 0) {
char[] charBuffer = new char[maxStringLength];
for (int i = 0; i < interfacesCount; ++i) {
currentOffset += 2;
interfaces[i] = readClass(currentOffset, charBuffer);
}
}
return interfaces;
}
}

上面的几个 getXxx() 方法也需要参考 ClassFile 结构来理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

假如,有如下一个类:

1
2
3
4
5
import java.io.Serializable;

public class HelloWorld extends Exception implements Serializable, Cloneable {

}

我们可以使用 ClassReader 类中的 getXxx() 方法来获取相应的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassReader;

import java.util.Arrays;

public class HelloWorldRun {
public static void main(String[] args) throws Exception {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
byte[] bytes = FileUtils.readBytes(filepath);

//(1)构建 ClassReader
ClassReader cr = new ClassReader(bytes);

// (2) 调用 getXxx() 方法
int access = cr.getAccess();
System.out.println("access: " + access);

String className = cr.getClassName();
System.out.println("className: " + className);

String superName = cr.getSuperName();
System.out.println("superName: " + superName);

String[] interfaces = cr.getInterfaces();
System.out.println("interfaces: " + Arrays.toString(interfaces));
}
}

输出结果:

1
2
3
4
access: 33
className: sample/HelloWorld
superName: java/lang/Exception
interfaces: [java/io/Serializable, java/lang/Cloneable]

accept() 方法

ClassReader 类当中,有一个 accept() 方法,这个方法接收一个 ClassVisitor 类型的参数,因此 accept() 方法是将 ClassReaderClassVisitor 进行连接的“桥梁”。accept() 方法的代码逻辑就是按照一定的顺序来调用 ClassVisitor 当中的 visitXxx() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class ClassReader {
// A flag to skip the Code attributes.
public static final int SKIP_CODE = 1;

// A flag to skip the SourceFile, SourceDebugExtension,
// LocalVariableTable, LocalVariableTypeTable,
// LineNumberTable and MethodParameters attributes.
public static final int SKIP_DEBUG = 2;

// A flag to skip the StackMap and StackMapTable attributes.
public static final int SKIP_FRAMES = 4;

// A flag to expand the stack map frames.
public static final int EXPAND_FRAMES = 8;


public void accept(final ClassVisitor classVisitor, final int parsingOptions) {
accept(classVisitor, new Attribute[0], parsingOptions);
}

public void accept(
final ClassVisitor classVisitor,
final Attribute[] attributePrototypes,
final int parsingOptions) {
Context context = new Context();
context.attributePrototypes = attributePrototypes;
context.parsingOptions = parsingOptions;
context.charBuffer = new char[maxStringLength];

// Read the access_flags, this_class, super_class, interface_count and interfaces fields.
char[] charBuffer = context.charBuffer;
int currentOffset = header;
int accessFlags = readUnsignedShort(currentOffset);
String thisClass = readClass(currentOffset + 2, charBuffer);
String superClass = readClass(currentOffset + 4, charBuffer);
String[] interfaces = new String[readUnsignedShort(currentOffset + 6)];
currentOffset += 8;
for (int i = 0; i < interfaces.length; ++i) {
interfaces[i] = readClass(currentOffset, charBuffer);
currentOffset += 2;
}

// ......

// Visit the class declaration. The minor_version and major_version fields start 6 bytes before
// the first constant pool entry, which itself starts at cpInfoOffsets[1] - 1 (by definition).
classVisitor.visit(readInt(cpInfoOffsets[1] - 7), accessFlags, thisClass, signature, superClass, interfaces);

// ......

// Visit the fields and methods.
int fieldsCount = readUnsignedShort(currentOffset);
currentOffset += 2;
while (fieldsCount-- > 0) {
currentOffset = readField(classVisitor, context, currentOffset);
}
int methodsCount = readUnsignedShort(currentOffset);
currentOffset += 2;
while (methodsCount-- > 0) {
currentOffset = readMethod(classVisitor, context, currentOffset);
}

// Visit the end of the class.
classVisitor.visitEnd();
}

}

ClassVisitor 类中 visitXxx() 方法的调用顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
visit
[visitSource][visitModule][visitNestHost][visitPermittedSubclass][visitOuterClass]
(
visitAnnotation |
visitTypeAnnotation |
visitAttribute
)*
(
visitNestMember |
visitInnerClass |
visitRecordComponent |
visitField |
visitMethod
)*
visitEnd

使用 ClassReader 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

public class HelloWorldTransformCore {
public static void main(String[] args) {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
byte[] bytes1 = FileUtils.readBytes(filepath);

//(1)构建 ClassReader
ClassReader cr = new ClassReader(bytes1);

//(2)构建 ClassWriter
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

//(3)串连 ClassVisitor
int api = Opcodes.ASM9;
ClassVisitor cv = new ClassVisitor(api, cw) { /**/ };

//(4)结合 ClassReader 和 ClassVisitor
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);

//(5)生成 byte[]
byte[] bytes2 = cw.toByteArray();

FileUtils.writeBytes(filepath, bytes2);
}
}

代码的整体处理流程是如下这样的:

1
.class --> ClassReader --> ClassVisitor1 ... --> ClassVisitorN --> ClassWriter --> .class 文件

我们可以将整体的处理流程想像成一条河流,那么

  • 第一步,构建 ClassReader。生成的 ClassReader 对象,它是这条“河流”的“源头”。
  • 第二步,构建 ClassWriter。生成的 ClassWriter 对象,它是这条“河流”的“归处”,它可以想像成是“百川东到海”中的“大海”。
  • 第三步,串连 ClassVisitor。生成的 ClassVisitor 对象,它是这条“河流”上的重要节点,可以想像成一个“水库”;可以有多个 ClassVisitor 对象,也就是在这条“河流”上存在多个“水库”,这些“水库”可以对“河水”进行一些处理,最终会这些“水库”的水会流向“大海”;也就是说多个 ClassVisitor 对象最终会连接到 ClassWriter 对象上。
  • 第四步,结合 ClassReaderClassVisitor。在 ClassReader 类上,有一个 accept() 方法,它接收一个 ClassVisitor 类型的对象;换句话说,就是将“河流”的“源头”和后续的“水库”连接起来。
  • 第五步,生成 byte[]。到这一步,就是所有的“河水”都流入 ClassWriter 这个“大海”当中,这个时候我们调用 ClassWriter.toByteArray() 方法,就能够得到 byte[] 内容。

img

parsingOptions 参数

ClassReader 类当中,accept() 方法接收一个 int 类型的 parsingOptions 参数。

1
public void accept(final ClassVisitor classVisitor, final int parsingOptions)

parsingOptions 参数可以选取的值有以下 5 个:

  • 0
  • ClassReader.SKIP_CODE
  • ClassReader.SKIP_DEBUG
  • ClassReader.SKIP_FRAMES
  • ClassReader.EXPAND_FRAMES

推荐使用:

  • 在调用 ClassReader.accept() 方法时,其中的 parsingOptions 参数,推荐使用 ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES
  • 在创建 ClassWriter 对象时,其中的 flags 参数,推荐使用 ClassWriter.COMPUTE_FRAMES

示例代码如下:

1
2
3
4
5
ClassReader cr = new ClassReader(bytes);
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

为什么我们推荐使用 ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES 呢?因为使用这样的一个值,可以生成最少的 ASM 代码,但是又能实现完整的功能。

  • 0:会生成所有的 ASM 代码,包括调试信息、frame 信息和代码信息。
  • ClassReader.SKIP_CODE:会忽略代码信息,例如,会忽略对于 MethodVisitor.visitXxxInsn() 方法的调用。
  • ClassReader.SKIP_DEBUG:会忽略调试信息,例如,会忽略对于 MethodVisitor.visitParameter()MethodVisitor.visitLineNumber()MethodVisitor.visitLocalVariable() 等方法的调用。
  • ClassReader.SKIP_FRAMES:会忽略 frame 信息,例如,会忽略对于 MethodVisitor.visitFrame() 方法的调用。
  • ClassReader.EXPAND_FRAMES:会对 frame 信息进行扩展,例如,会对 MethodVisitor.visitFrame() 方法的参数有影响。

简而言之,使用 ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES 的目的是功能完整、代码少、复杂度低,

  • 不使用 ClassReader.SKIP_CODE,使代码的功能保持完整。
  • 使用 ClassReader.SKIP_DEBUG,减少不必要的调试信息,会使代码量减少。
  • 使用 ClassReader.SKIP_FRAMES,降低代码的复杂度
  • 不使用 ClassReader.EXPAND_FRAMES,降低代码的复杂度

我们使用 ClassReader.SKIP_DEBUG 的时候,就不会生成调试信息。因为这些调试信息主要是记录某一条 instruction 在代码当中的行数,以及变量的名字等信息;如果没有这些调试信息,也不会影响程序的正常运行,也就是说功能不受影响,因此省略这些信息,就会让 ASM 代码尽可能的简洁。

我们使用 ClassReader.SKIP_FRAMES 的时候,就会忽略 frame 的信息。为什么要忽略这些 frame 信息呢?因为 frame 计算的细节会很繁琐,需要处理的情况也有很多,总的来说,就是比较麻烦。我们解决这个麻烦的方式,就是让 ASM 帮助我们来计算 frame 的情况,也就是在创建 ClassWriter 对象的时候使用 ClassWriter.COMPUTE_FRAMES 选项。

示例一:修改类的版本

假如有一个 HelloWorld.java 文件,经过 Java 8 编译之后,生成的 HelloWorld.class 文件的版本就是 Java 8 的版本,我们的目标是将 HelloWorld.class 由 Java 8 版本转换成 Java 7 版本。

1
2
public class HelloWorld {
}

编码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;

public class ClassChangeVersionVisitor extends ClassVisitor {
public ClassChangeVersionVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}

@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(Opcodes.V1_7, access, name, signature, superName, interfaces);
}
}

进行转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

public class HelloWorldTransformCore {
public static void main(String[] args) {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
byte[] bytes1 = FileUtils.readBytes(filepath);

//(1)构建 ClassReader
ClassReader cr = new ClassReader(bytes1);

//(2)构建 ClassWriter
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

//(3)串连 ClassVisitor
int api = Opcodes.ASM9;
ClassVisitor cv = new ClassChangeVersionVisitor(api, cw);

//(4)结合 ClassReader 和 ClassVisitor
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);

//(5)生成 byte[]
byte[] bytes2 = cw.toByteArray();

FileUtils.writeBytes(filepath, bytes2);
}
}

示例二:修改类的接口

在下面的 HelloWorld 类中,我们定义了一个 clone() 方法,但存在一个问题,也就是,如果没有实现 Cloneable 接口,clone() 方法就会出错,我们的目标是希望通过 ASM 为 HelloWorld 类添加上 Cloneable 接口。

1
2
3
4
5
6
public class HelloWorld {
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

编码实现:

1
2
3
4
5
6
7
8
9
10
11
12
import org.objectweb.asm.ClassVisitor;

public class ClassCloneVisitor extends ClassVisitor {
public ClassCloneVisitor(int api, ClassVisitor cw) {
super(api, cw);
}

@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, new String[]{"java/lang/Cloneable"});
}
}

注意:ClassCloneVisitor 这个类写的比较简单,直接添加 java/lang/Cloneable 接口信息;在项目代码当中,有一个 ClassAddInterfaceVisitor 类,实现更灵活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

public class HelloWorldTransformCore {
public static void main(String[] args) {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
byte[] bytes1 = FileUtils.readBytes(filepath);

//(1)构建 ClassReader
ClassReader cr = new ClassReader(bytes1);

//(2)构建 ClassWriter
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

//(3)串连 ClassVisitor
int api = Opcodes.ASM9;
ClassVisitor cv = new ClassCloneVisitor(api, cw);

//(4)结合 ClassReader 和 ClassVisitor
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);

//(5)生成 byte[]
byte[] bytes2 = cw.toByteArray();

FileUtils.writeBytes(filepath, bytes2);
}
}

验证结果:

1
2
3
4
5
6
7
public class HelloWorldRun {
public static void main(String[] args) throws Exception {
HelloWorld obj = new HelloWorld();
Object anotherObj = obj.clone();
System.out.println(anotherObj);
}
}

总结一

上面的两个例子,一个是修改类的版本信息,另一个是修改类的接口信息,那么这两个示例都是基于 ClassVisitor.visit() 方法实现的:

1
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces)

这两个示例,就是通过修改 visit() 方法的参数实现的:

  • 修改类的版本信息,是通过修改 version 这个参数实现的
  • 修改类的接口信息,是通过修改 interfaces 这个参数实现的

其实,在 visit() 方法当中的其它参数也可以修改:

  • 修改 access 参数,也就是修改了类的访问标识信息。
  • 修改 name 参数,也就是修改了类的名称。但是,在大多数的情况下,不推荐修改 name 参数。因为调用类里的方法,都是先找到类,再找到相应的方法;如果将当前类的类名修改成别的名称,那么其它类当中可能就找不到原来的方法了,因为类名已经改了。但是,也有少数的情况,可以修改 name 参数,比如说对代码进行混淆(obfuscate)操作。
  • 修改 superName 参数,也就是修改了当前类的父类信息。

示例三:删除字段

删除掉 HelloWorld 类里的 String strValue 字段。

1
2
3
4
public class HelloWorld {
public int intValue;
public String strValue; // 删除这个字段
}

编码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package Study.ClassReader;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;

public class ClassRemoveFieldVisitor extends ClassVisitor {

private final String fieldName;
private final String fieldDesc;

protected ClassRemoveFieldVisitor(int api, ClassVisitor classVisitor, String fieldName, String fieldDesc) {
super(api, classVisitor);
this.fieldName = fieldName;
this.fieldDesc = fieldDesc;
}

@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
if(name.equals(fieldName)&&descriptor.equals(fieldDesc)){
return null;
} else{
return super.visitField(access, name, descriptor, signature, value);
}
}
}

在这段代码中,ClassRemoveFieldVisitor 通过继承 ClassVisitor 类来实现删除特定字段的功能。关键在于 visitField() 方法。当 visitField() 返回 null 时,该字段就会被“过滤”掉,相当于从字节码中删除。

我们可以用邮递系统来比喻这个过程。ClassReader 就像邮件的发送者,ClassVisitor 是邮件经过的中间站,而 ClassWriter 是最终的收件人。如果在某个中间站 ClassVisitor 拦截并丢弃了某封邮件(字段),那么这封邮件就不会到达收件人。

同样,也可以用流水线来形容。ClassReader 是生产线上最初的步骤,ClassVisitor 是中间的处理环节,而 ClassWriter 是最终的成品阶段。如果某一个处理环节(ClassVisitor)中某个零件(字段)被移除了,那么最终产品中就不会包含这个零件。

通过这种方法,在字节码处理中实现了对特定字段的删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package Study.ClassReader;

import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;

import java.io.IOException;

import static org.objectweb.asm.Opcodes.ASM9;

public class HelloWorldTransformCore {
public static void main(String[] args) throws IOException {
// 定义相对路径
String relative_path="Study/ClassReader/HelloWorld.class";
// 获取文件路径
String filepath= FileUtils.getFilePath(relative_path);

// 读取文件字节码
byte[] bytes = FileUtils.readBytes(filepath);

// 创建ClassReader对象
ClassReader classReader = new ClassReader(bytes);

// 创建ClassWriter对象
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);

ClassRemoveFieldVisitor classRemoveFieldVisitor = new ClassRemoveFieldVisitor(ASM9, classWriter,"strValue","Ljava/lang/String;");

int parsingOptions = ClassReader.SKIP_DEBUG| ClassReader.SKIP_FRAMES;
classReader.accept(classRemoveFieldVisitor,parsingOptions);

// 将修改后的字节码写入文件
FileUtils.writeBytes(filepath,classWriter.toByteArray());
}
}

验证

1
2
3
4
5
6
7
8
9
10
11
public class HelloWorldRun {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("Study.ClassReader.HelloWorld");
System.out.println(clazz.getName());

Field[] declaredFields = clazz.getDeclaredFields();
for (Field f : declaredFields) {
System.out.println(" " + f.getName());
}
}
}

示例四:添加字段

为了 HelloWorld 类添加一个 Object objValue 字段。

1
2
3
4
5
public class HelloWorld {
public int intValue;
public String strValue;
// 添加一个 Object objValue 字段
}

编码方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package Study.ClassReader;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;

public class ClassAddFieldVisitor extends ClassVisitor {
private final int fieldAccess;
private final String fieldName;
private final String fieldDesc;
private boolean isFieldPresent;


protected ClassAddFieldVisitor(int api, ClassVisitor classVisitor,int fieldAccess, String fieldName, String fieldDesc) {
super(api, classVisitor);
this.fieldAccess = fieldAccess;
this.fieldName = fieldName;
this.fieldDesc = fieldDesc;
}

@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
if(name.equals(fieldName)){
isFieldPresent = true;
}
return super.visitField(access, name, descriptor, signature, value);
}

@Override
public void visitEnd() {
if(!isFieldPresent){
FieldVisitor fieldVisitor = super.visitField(fieldAccess,fieldName,fieldDesc,null,null);
if(fieldVisitor != null){
fieldVisitor.visitEnd();
}
}
super.visitEnd();
}
}

第一步,在 visitField() 方法中,判断某个字段是否已经存在,其结果存在于 isFieldPresent 字段当中;第二步,就是在 visitEnd() 方法中,根据 isFieldPresent 字段的值,来决定是否添加新的字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package Study.ClassReader;

import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;

import java.io.IOException;

import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ASM9;

public class HelloWorldTransformCore {
public static void main(String[] args) throws IOException {
// 定义相对路径
String relative_path="Study/ClassReader/HelloWorld.class";
// 获取文件路径
String filepath= FileUtils.getFilePath(relative_path);

// 读取文件字节码
byte[] bytes = FileUtils.readBytes(filepath);

// 创建ClassReader对象
ClassReader classReader = new ClassReader(bytes);

// 创建ClassWriter对象
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);

ClassAddFieldVisitor classAddFieldVisitor = new ClassAddFieldVisitor(ASM9, classWriter, ACC_PUBLIC, "objValue", "Ljava/lang/Object;");

int parsingOptions = ClassReader.SKIP_DEBUG| ClassReader.SKIP_FRAMES;
classReader.accept(classAddFieldVisitor,parsingOptions);

// 将修改后的字节码写入文件
FileUtils.writeBytes(filepath,classWriter.toByteArray());
}
}

验证与实例三相同。

总结二

在字节码处理中,ClassVisitor.visitField() 方法用于字段操作,主要包括以下几种:

1
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value);
  1. 修改字段

    • 可以改变字段名称、类型或访问权限。
    • 需要通过调整 visitField() 方法中的参数来实现。
    • 通常不建议修改,因为这可能导致引用不匹配,从而导致程序错误。例如,如果某个类中的字段名被修改,其他引用该字段的代码也必须同步更新,否则会报错。
  2. 删除字段

    • visitField() 中返回 null 可以实现删除。
    • 通常不建议删除已有字段,因为字段通常有其特定用途,删除可能导致缺失功能或程序错误。
  3. 添加字段

    • 判断字段是否已存在,如果不存在,则在 visitEnd() 中添加。
    • 不建议在 visitField() 中添加,以避免重复添加导致类文件不合法。

以上操作在字节码操作中需要谨慎,以保证程序的正确性和稳定性。

示例五:删除方法

删除掉 HelloWorld 类里的 add() 方法。

1
2
3
4
5
6
7
8
9
public class HelloWorld {
public int add(int a, int b) { // 删除 add 方法
return a + b;
}

public int sub(int a, int b) {
return a - b;
}
}

编码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package Study.ClassReader;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;

public class ClassRemoveMethodVisitor extends ClassVisitor {

private final String methodName;
private final String methodDesc;


protected ClassRemoveMethodVisitor(int api, ClassVisitor classVisitor,String methodName,String methodDesc) {
super(api, classVisitor);
this.methodName = methodName;
this.methodDesc = methodDesc;
}

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if(name.equals(methodName) && descriptor.equals(methodDesc)){
return null;
}else{
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
}
}

和之前删除字段差不多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package Study.ClassReader;

import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;

import java.io.IOException;

import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ASM9;

public class HelloWorldTransformCore {
public static void main(String[] args) throws IOException {
// 定义相对路径
String relative_path="Study/ClassReader/HelloWorld.class";
// 获取文件路径
String filepath= FileUtils.getFilePath(relative_path);

// 读取文件字节码
byte[] bytes = FileUtils.readBytes(filepath);

// 创建ClassReader对象
ClassReader classReader = new ClassReader(bytes);

// 创建ClassWriter对象
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);

ClassRemoveMethodVisitor classRemoveMethodVisitor = new ClassRemoveMethodVisitor(ASM9, classWriter, "add","(II)I");

int parsingOptions = ClassReader.SKIP_DEBUG| ClassReader.SKIP_FRAMES;
classReader.accept(classRemoveMethodVisitor,parsingOptions);

// 将修改后的字节码写入文件
FileUtils.writeBytes(filepath,classWriter.toByteArray());
}
}

验证:

1
2
3
4
5
6
7
8
9
10
11
public class HelloWorldRun {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("Study.ClassReader.HelloWorld");
System.out.println(clazz.getName());

Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method m : declaredMethods) {
System.out.println(" " + m.getName());
}
}
}

示例六:添加方法

HelloWorld 类添加一个 mul() 方法。

1
2
3
4
5
6
7
8
9
10
11
public class HelloWorld {
public int add(int a, int b) {
return a + b;
}

public int sub(int a, int b) {
return a - b;
}

// TODO: 添加一个乘法
}

编码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package Study.ClassReader;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;

public abstract class ClassAddMethodVisitor extends ClassVisitor {

private final int methodAccess;
private final String methodName;
private final String methodDesc;
private final String methodSignature;
private final String[] methodExceptions;
private boolean isMethodPresent;

protected ClassAddMethodVisitor(int api, ClassVisitor classVisitor, int methodAccess, String methodName, String methodDesc,
String signature, String[] exceptions) {
super(api, classVisitor);
this.methodAccess = methodAccess;
this.methodName = methodName;
this.methodDesc = methodDesc;
this.methodSignature = signature;
this.methodExceptions = exceptions;
this.isMethodPresent = false;
}

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if (name.equals(methodName) && descriptor.equals(methodDesc)) {
isMethodPresent = true;
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}

@Override
public void visitEnd() {
if(!isMethodPresent) {
MethodVisitor methodVisitor = super.visitMethod(methodAccess, methodName, methodDesc, methodSignature, methodExceptions);
if(methodVisitor!=null){
generateMethodBody(methodVisitor);
}
}
super.visitEnd();
}

protected abstract void generateMethodBody(MethodVisitor methodVisitor);
}

添加新的方法,和添加新的字段的思路,在前期,两者是一样的,都是先要判断该字段或该方法是否已经存在;但是,在后期,两者会有一些差异,因为方法需要有“方法体”,在上面的代码中,我们定义了一个 generateMethodBody() 方法,这个方法需要在子类当中进行实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package Study.ClassReader;

import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;

import java.io.IOException;

import static org.objectweb.asm.Opcodes.*;

public class HelloWorldTransformCore {
public static void main(String[] args) throws IOException {
// 定义相对路径
String relative_path="Study/ClassReader/HelloWorld.class";
// 获取文件路径
String filepath= FileUtils.getFilePath(relative_path);

// 读取文件字节码
byte[] bytes = FileUtils.readBytes(filepath);

// 创建ClassReader对象
ClassReader classReader = new ClassReader(bytes);

// 创建ClassWriter对象
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);

ClassAddMethodVisitor classAddMethodVisitor = new ClassAddMethodVisitor(
ASM9,classWriter,ACC_PUBLIC,"multiply","(II)I",null,null)
{
@Override
protected void generateMethodBody(MethodVisitor methodVisitor) {
methodVisitor.visitCode();
methodVisitor.visitVarInsn(ILOAD,1);
methodVisitor.visitVarInsn(ILOAD,2);
methodVisitor.visitInsn(IMUL);
methodVisitor.visitInsn(IRETURN);
methodVisitor.visitMaxs(2,3);
methodVisitor.visitEnd();
}
};

int parsingOptions = ClassReader.SKIP_DEBUG| ClassReader.SKIP_FRAMES;
classReader.accept(classAddMethodVisitor,parsingOptions);

// 将修改后的字节码写入文件
FileUtils.writeBytes(filepath,classWriter.toByteArray());
}
}

对于方法的操作,都是基于 ClassVisitor.visitMethod() 方法来实现的:

1
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions);
  1. 修改方法

    • 可以更新方法体,即具体实现的代码。
    • 不建议修改方法名称或类型(参数和返回值类型),因为这可能导致调用不匹配,从而引发错误。
  2. 删除方法

    • 通常不建议删除已有方法,因这可能导致调用失败,进而引发程序错误。
  3. 添加方法

    • 可以在适当的位置新增方法。

在操作方法时需谨慎,以维护程序的正确性和稳定性。

AdviceAdapter 类

对于 AdviceAdapter 类,可以方便地实现在“方法进入”和“方法退出”时插入自定义代码。

AdviceAdapter 类的核心特点是提供了 onMethodEnter()onMethodExit() 方法,这两个方法可以被重写,以便在方法执行之前和之后执行特定的逻辑。

class info

AdviceAdapter 类是一个抽象的 MethodVisitor 子类

具体而言,AdviceAdapter 继承自 GeneratorAdapter,而 GeneratorAdapter 又继承自 LocalVariablesSorter,最终 LocalVariablesSorter 继承自 MethodVisitor

由于 AdviceAdapter 是一个抽象类,如果想要使用它,我们需要创建一个具体的子类来实现其功能:

1
2
3
public abstract class AdviceAdapter extends GeneratorAdapter implements Opcodes {
// 其他方法和实现
}

fields

AdviceAdapter 类定义的字段。其中, isConstructor 字段是判断当前方法是不是构造方法。如果当前方法是构造方法,在“方法进入”时添加代码,需要特殊处理。

1
2
3
4
5
6
public abstract class AdviceAdapter extends GeneratorAdapter implements Opcodes {
protected int methodAccess;
protected String methodDesc;

private final boolean isConstructor;
}

constructors

AdviceAdapter 类定义的构造方法。 AdviceAdapter 的构造方法是用 protected 修饰,因此这个构造方法只能在子类当中访问。 换句话说,在外界不能用 new 关键字来创建对象。

1
2
3
4
5
6
7
8
9
public abstract class AdviceAdapter extends GeneratorAdapter implements Opcodes {
protected AdviceAdapter(final int api, final MethodVisitor methodVisitor,
final int access, final String name, final String descriptor) {
super(api, methodVisitor, access, name, descriptor);
methodAccess = access;
methodDesc = descriptor;
isConstructor = "<init>".equals(name);
}
}

methods

AdviceAdapter 类定义了几个重要的方法,其中最关键的是 onMethodEnter()onMethodExit() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class AdviceAdapter extends GeneratorAdapter implements Opcodes {
// 在被访问的方法开始时生成“进入”建议。
// 默认实现不执行任何操作。
// 子类可以使用或修改所有局部变量,但不应改变栈的状态。
// 此方法在方法开始时或构造函数调用父类构造函数后被调用。
protected void onMethodEnter() {}

// 在被访问的方法结束时生成“退出”建议。
// 默认实现不执行任何操作。
// 子类可以使用或修改所有局部变量,但不应改变栈的状态。
// 此方法在方法结束时被调用,返回和抛出指令之前。
// 栈顶元素包含返回值或异常实例。
protected void onMethodExit(final int opcode) {}
}

我们可以从三个角度来理解这两个方法:

1. 应用场景

  • **onMethodEnter()**:在方法执行开始时,可以插入一些逻辑代码,例如日志记录、性能监控等。
  • **onMethodExit()**:在方法执行结束时,可以执行清理操作、获取返回结果或捕获异常等。

2. 注意事项

  • 对于 onMethodEnter()onMethodExit(),都需要注意:子类可以修改所有局部变量,但不应改变栈的状态。换句话说,修改前后的操作数栈(operand stack)必须保持一致。
  • onMethodExit() 中,要注意栈顶元素包含返回值或异常实例,需要妥善处理,以确保这些信息不会丢失。

3. 工作原理

  • **onMethodEnter()**:通常通过 visitCode() 方法触发,使用 onMethodEnter() 的好处在于它能够处理 <init>() 方法的复杂情况,而直接使用 visitCode() 可能会导致 <init>() 方法的错误。
  • **onMethodExit()**:通常通过 visitInsn(int opcode) 方法触发,用于在方法结束时执行相应的逻辑。

示例:打印方法参数和返回值

假如有一个 HelloWorld 类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class HelloWorld {
private String name;
private int age;

public HelloWorld(String name, int age) {
this.name = name;
this.age = age;
}

public void test(long idCard, Object obj) {
int hashCode = 0;
hashCode += name.hashCode();
hashCode += age;
hashCode += (int) (idCard % Integer.MAX_VALUE);
hashCode += obj.hashCode();
hashCode = Math.abs(hashCode);
System.out.println("Hash Code is " + hashCode);
if (hashCode % 2 == 1) {
throw new RuntimeException("illegal");
}
}
}

我们想实现的预期目标:打印出构造方法(<init>())和 test() 的参数和返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package cmisl.utils;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

public class ParameterUtils {
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);

public static void printValueOnStack(Object value) {
if (value == null) {
System.out.println(" " + value);
}
else if (value instanceof String) {
System.out.println(" " + value);
}
else if (value instanceof Date) {
System.out.println(" " + formatter.get().format(value));
}
else if (value instanceof char[]) {
System.out.println(" " + Arrays.toString((char[])value));
}
else {
System.out.println(" " + value.getClass() + ": " + value.toString());
}
}

public static void printText(String str) {
System.out.println(str);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package Study.AdviceAdapter;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;

import static org.objectweb.asm.Opcodes.ACC_ABSTRACT;
import static org.objectweb.asm.Opcodes.ACC_NATIVE;

public class ClassPrintParameterVisitor extends ClassVisitor {
public ClassPrintParameterVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (mv != null) {
boolean isAbstractMethod = (access & ACC_ABSTRACT) != 0;
boolean isNativeMethod = (access & ACC_NATIVE) != 0;
if (!isAbstractMethod && !isNativeMethod) {
mv = new MethodPrintParameterAdapter(api, mv, access, name, descriptor);
}
}
return mv;
}

public static class MethodPrintParameterAdapter extends AdviceAdapter {
public MethodPrintParameterAdapter(int api, MethodVisitor mv, int access, String name, String descriptor) {
super(api, mv, access, name, descriptor);
}

@Override
protected void onMethodEnter() {
printMessage("Method Enter: " + getName() + methodDesc);

Type[] argumentTypes = getArgumentTypes();
for (int i = 0; i < argumentTypes.length; i++) {
Type t = argumentTypes[i];
loadArg(i);
box(t);
printValueOnStack("(Ljava/lang/Object;)V");
}
}

@Override
protected void onMethodExit(int opcode) {
printMessage("Method Exit: " + getName() + methodDesc);

if (opcode == ATHROW) {
super.visitLdcInsn("abnormal return");
}
else if (opcode == RETURN) {
super.visitLdcInsn("return void");
}
else if (opcode == ARETURN) {
dup();
}
else {
if (opcode == LRETURN || opcode == DRETURN) {
dup2();
}
else {
dup();
}
box(getReturnType());
}
printValueOnStack("(Ljava/lang/Object;)V");
}

private void printMessage(String str) {
super.visitLdcInsn(str);
super.visitMethodInsn(INVOKESTATIC, "cmisl/utils/ParameterUtils", "printText", "(Ljava/lang/String;)V", false);
}

private void printValueOnStack(String descriptor) {
super.visitMethodInsn(INVOKESTATIC, "cmisl/utils/ParameterUtils", "printValueOnStack", descriptor, false);
}
}
}

开始在方法前后插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package Study.AdviceAdapter;

import cmisl.utils.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

import java.io.IOException;

public class HelloWorldTransformCore {
public static void main(String[] args) throws IOException {
String relative_path = "Study/AdviceAdapter/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
byte[] bytes1 = FileUtils.readBytes(filepath);

//(1)构建 ClassReader
ClassReader cr = new ClassReader(bytes1);

//(2)构建 ClassWriter
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

//(3)串连 ClassVisitor
int api = Opcodes.ASM9;
ClassVisitor cv = new ClassPrintParameterVisitor(api, cw);

//(4)结合 ClassReader 和 ClassVisitor
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);

//(5)生成 byte[]
byte[] bytes2 = cw.toByteArray();

FileUtils.writeBytes(filepath, bytes2);
}
}

Java虚拟机指令表

十六进制 助记符 指令说明
0x00 nop 什么都不做
0x01 aconst_null 将null推送至栈顶
0x02 iconst_m1 将int型-1推送至栈顶
0x03 iconst_0 将int型0推送至栈顶
0x04 iconst_1 将int型1推送至栈顶
0x05 iconst_2 将int型2推送至栈顶
0x06 iconst_3 将int型3推送至栈顶
0x07 iconst_4 将int型4推送至栈顶
0x08 iconst_5 将int型5推送至栈顶
0x09 lconst_0 将long型0推送至栈顶
0x0a lconst_1 将long型1推送至栈顶
0x0b fconst_0 将float型0推送至栈顶
0x0c fconst_1 将float型1推送至栈顶
0x0d fconst_2 将float型2推送至栈顶
0x0e dconst_0 将double型0推送至栈顶
0x0f dconst_1 将double型1推送至栈顶
0x10 bipush 将单字节的常量值(-128~127)推送至栈顶
0x11 sipush 将一个短整型常量值(-32768~32767)推送至栈顶
0x12 ldc 将int, float或String型常量值从常量池中推送至栈顶
0x13 ldc_w 将int, float或String型常量值从常量池中推送至栈顶(宽索引)
0x14 ldc2_w 将long或double型常量值从常量池中推送至栈顶(宽索引)
0x15 iload 将指定的int型本地变量推送至栈顶
0x16 lload 将指定的long型本地变量推送至栈顶
0x17 fload 将指定的float型本地变量推送至栈顶
0x18 dload 将指定的double型本地变量推送至栈顶
0x19 aload 将指定的引用类型本地变量推送至栈顶
0x1a iload_0 将第一个int型本地变量推送至栈顶
0x1b iload_1 将第二个int型本地变量推送至栈顶
0x1c iload_2 将第三个int型本地变量推送至栈顶
0x1d iload_3 将第四个int型本地变量推送至栈顶
0x1e lload_0 将第一个long型本地变量推送至栈顶
0x1f lload_1 将第二个long型本地变量推送至栈顶
0x20 lload_2 将第三个long型本地变量推送至栈顶
0x21 lload_3 将第四个long型本地变量推送至栈顶
0x22 fload_0 将第一个float型本地变量推送至栈顶
0x23 fload_1 将第二个float型本地变量推送至栈顶
0x24 fload_2 将第三个float型本地变量推送至栈顶
0x25 fload_3 将第四个float型本地变量推送至栈顶
0x26 dload_0 将第一个double型本地变量推送至栈顶
0x27 dload_1 将第二个double型本地变量推送至栈顶
0x28 dload_2 将第三个double型本地变量推送至栈顶
0x29 dload_3 将第四个double型本地变量推送至栈顶
0x2a aload_0 将第一个引用类型本地变量推送至栈顶
0x2b aload_1 将第二个引用类型本地变量推送至栈顶
0x2c aload_2 将第三个引用类型本地变量推送至栈顶
0x2d aload_3 将第四个引用类型本地变量推送至栈顶
0x2e iaload 将int型数组指定索引的值推送至栈顶
0x2f laload 将long型数组指定索引的值推送至栈顶
0x30 faload 将float型数组指定索引的值推送至栈顶
0x31 daload 将double型数组指定索引的值推送至栈顶
0x32 aaload 将引用型数组指定索引的值推送至栈顶
0x33 baload 将boolean或byte型数组指定索引的值推送至栈顶
0x34 caload 将char型数组指定索引的值推送至栈顶
0x35 saload 将short型数组指定索引的值推送至栈顶
0x36 istore 将栈顶int型数值存入指定本地变量
0x37 lstore 将栈顶long型数值存入指定本地变量
0x38 fstore 将栈顶float型数值存入指定本地变量
0x39 dstore 将栈顶double型数值存入指定本地变量
0x3a astore 将栈顶引用型数值存入指定本地变量
0x3b istore_0 将栈顶int型数值存入第一个本地变量
0x3c istore_1 将栈顶int型数值存入第二个本地变量
0x3d istore_2 将栈顶int型数值存入第三个本地变量
0x3e istore_3 将栈顶int型数值存入第四个本地变量
0x3f lstore_0 将栈顶long型数值存入第一个本地变量
0x40 lstore_1 将栈顶long型数值存入第二个本地变量
0x41 lstore_2 将栈顶long型数值存入第三个本地变量
0x42 lstore_3 将栈顶long型数值存入第四个本地变量
0x43 fstore_0 将栈顶float型数值存入第一个本地变量
0x44 fstore_1 将栈顶float型数值存入第二个本地变量
0x45 fstore_2 将栈顶float型数值存入第三个本地变量
0x46 fstore_3 将栈顶float型数值存入第四个本地变量
0x47 dstore_0 将栈顶double型数值存入第一个本地变量
0x48 dstore_1 将栈顶double型数值存入第二个本地变量
0x49 dstore_2 将栈顶double型数值存入第三个本地变量
0x4a dstore_3 将栈顶double型数值存入第四个本地变量
0x4b astore_0 将栈顶引用型数值存入第一个本地变量
0x4c astore_1 将栈顶引用型数值存入第二个本地变量
0x4d astore_2 将栈顶引用型数值存入第三个本地变量
0x4e astore_3 将栈顶引用型数值存入第四个本地变量
0x4f iastore 将栈顶int型数值存入指定数组的指定索引位置
0x50 lastore 将栈顶long型数值存入指定数组的指定索引位置
0x51 fastore 将栈顶float型数值存入指定数组的指定索引位置
0x52 dastore 将栈顶double型数值存入指定数组的指定索引位置
0x53 aastore 将栈顶引用型数值存入指定数组的指定索引位置
0x54 bastore 将栈顶boolean或byte型数值存入指定数组的指定索引位置
0x55 castore 将栈顶char型数值存入指定数组的指定索引位置
0x56 sastore 将栈顶short型数值存入指定数组的指定索引位置
0x57 pop 将栈顶数值弹出 (数值不能是long或double类型的)
0x58 pop2 将栈顶的一个(long或double类型的)或两个数值弹出(其它)
0x59 dup 复制栈顶数值并将复制值压入栈顶
0x5a dup_x1 复制栈顶数值并将两个复制值压入栈顶
0x5b dup_x2 复制栈顶数值并将三个(或两个)复制值压入栈顶
0x5c dup2 复制栈顶一个(long或double类型的)或两个(其它)数值并将复制值压入栈顶
0x5d dup2_x1 <待补充>
0x5e dup2_x2 <待补充>
0x5f swap 将栈最顶端的两个数值互换(数值不能是long或double类型的)
0x60 iadd 将栈顶两int型数值相加并将结果压入栈顶
0x61 ladd 将栈顶两long型数值相加并将结果压入栈顶
0x62 fadd 将栈顶两float型数值相加并将结果压入栈顶
0x63 dadd 将栈顶两double型数值相加并将结果压入栈顶
0x64 isub 将栈顶两int型数值相减并将结果压入栈顶
0x65 lsub 将栈顶两long型数值相减并将结果压入栈顶
0x66 fsub 将栈顶两float型数值相减并将结果压入栈顶
0x67 dsub 将栈顶两double型数值相减并将结果压入栈顶
0x68 imul 将栈顶两int型数值相乘并将结果压入栈顶
0x69 lmul 将栈顶两long型数值相乘并将结果压入栈顶
0x6a fmul 将栈顶两float型数值相乘并将结果压入栈顶
0x6b dmul 将栈顶两double型数值相乘并将结果压入栈顶
0x6c idiv 将栈顶两int型数值相除并将结果压入栈顶
0x6d ldiv 将栈顶两long型数值相除并将结果压入栈顶
0x6e fdiv 将栈顶两float型数值相除并将结果压入栈顶
0x6f ddiv 将栈顶两double型数值相除并将结果压入栈顶
0x70 irem 将栈顶两int型数值作取模运算并将结果压入栈顶
0x71 lrem 将栈顶两long型数值作取模运算并将结果压入栈顶
0x72 frem 将栈顶两float型数值作取模运算并将结果压入栈顶
0x73 drem 将栈顶两double型数值作取模运算并将结果压入栈顶
0x74 ineg 将栈顶int型数值取负并将结果压入栈顶
0x75 lneg 将栈顶long型数值取负并将结果压入栈顶
0x76 fneg 将栈顶float型数值取负并将结果压入栈顶
0x77 dneg 将栈顶double型数值取负并将结果压入栈顶
0x78 ishl 将int型数值左移位指定位数并将结果压入栈顶
0x79 lshl 将long型数值左移位指定位数并将结果压入栈顶
0x7a ishr 将int型数值右(符号)移位指定位数并将结果压入栈顶
0x7b lshr 将long型数值右(符号)移位指定位数并将结果压入栈顶
0x7c iushr 将int型数值右(无符号)移位指定位数并将结果压入栈顶
0x7d lushr 将long型数值右(无符号)移位指定位数并将结果压入栈顶
0x7e iand 将栈顶两int型数值作“按位与”并将结果压入栈顶
0x7f land 将栈顶两long型数值作“按位与”并将结果压入栈顶
0x80 ior 将栈顶两int型数值作“按位或”并将结果压入栈顶
0x81 lor 将栈顶两long型数值作“按位或”并将结果压入栈顶
0x82 ixor 将栈顶两int型数值作“按位异或”并将结果压入栈顶
0x83 lxor 将栈顶两long型数值作“按位异或”并将结果压入栈顶
0x84 iinc 将指定int型变量增加指定值(i++, i–, i+=2)
0x85 i2l 将栈顶int型数值强制转换成long型数值并将结果压入栈顶
0x86 i2f 将栈顶int型数值强制转换成float型数值并将结果压入栈顶
0x87 i2d 将栈顶int型数值强制转换成double型数值并将结果压入栈顶
0x88 l2i 将栈顶long型数值强制转换成int型数值并将结果压入栈顶
0x89 l2f 将栈顶long型数值强制转换成float型数值并将结果压入栈顶
0x8a l2d 将栈顶long型数值强制转换成double型数值并将结果压入栈顶
0x8b f2i 将栈顶float型数值强制转换成int型数值并将结果压入栈顶
0x8c f2l 将栈顶float型数值强制转换成long型数值并将结果压入栈顶
0x8d f2d 将栈顶float型数值强制转换成double型数值并将结果压入栈顶
0x8e d2i 将栈顶double型数值强制转换成int型数值并将结果压入栈顶
0x8f d2l 将栈顶double型数值强制转换成long型数值并将结果压入栈顶
0x90 d2f 将栈顶double型数值强制转换成float型数值并将结果压入栈顶
0x91 i2b 将栈顶int型数值强制转换成byte型数值并将结果压入栈顶
0x92 i2c 将栈顶int型数值强制转换成char型数值并将结果压入栈顶
0x93 i2s 将栈顶int型数值强制转换成short型数值并将结果压入栈顶
0x94 lcmp 比较栈顶两long型数值大小,并将结果(1,0,-1)压入栈顶
0x95 fcmpl 比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶
0x96 fcmpg 比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶
0x97 dcmpl 比较栈顶两double型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶
0x98 dcmpg 比较栈顶两double型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶
0x99 ifeq 当栈顶int型数值等于0时跳转
0x9a ifne 当栈顶int型数值不等于0时跳转
0x9b iflt 当栈顶int型数值小于0时跳转
0x9c ifge 当栈顶int型数值大于等于0时跳转
0x9d ifgt 当栈顶int型数值大于0时跳转
0x9e ifle 当栈顶int型数值小于等于0时跳转
0x9f if_icmpeq 比较栈顶两int型数值大小,当结果等于0时跳转
0xa0 if_icmpne 比较栈顶两int型数值大小,当结果不等于0时跳转
0xa1 if_icmplt 比较栈顶两int型数值大小,当结果小于0时跳转
0xa2 if_icmpge 比较栈顶两int型数值大小,当结果大于等于0时跳转
0xa3 if_icmpgt 比较栈顶两int型数值大小,当结果大于0时跳转
0xa4 if_icmple 比较栈顶两int型数值大小,当结果小于等于0时跳转
0xa5 if_acmpeq 比较栈顶两引用型数值,当结果相等时跳转
0xa6 if_acmpne 比较栈顶两引用型数值,当结果不相等时跳转
0xa7 goto 无条件跳转
0xa8 jsr 跳转至指定16位offset位置,并将jsr下一条指令地址压入栈顶
0xa9 ret 返回至本地变量指定的index的指令位置(一般与jsr, jsr_w联合使用)
0xaa tableswitch 用于switch条件跳转,case值连续(可变长度指令)
0xab lookupswitch 用于switch条件跳转,case值不连续(可变长度指令)
0xac ireturn 从当前方法返回int
0xad lreturn 从当前方法返回long
0xae freturn 从当前方法返回float
0xaf dreturn 从当前方法返回double
0xb0 areturn 从当前方法返回对象引用
0xb1 return 从当前方法返回void
0xb2 getstatic 获取指定类的静态域,并将其值压入栈顶
0xb3 putstatic 为指定的类的静态域赋值
0xb4 getfield 获取指定类的实例域,并将其值压入栈顶
0xb5 putfield 为指定的类的实例域赋值
0xb6 invokevirtual 调用实例方法
0xb7 invokespecial 调用超类构造方法,实例初始化方法,私有方法
0xb8 invokestatic 调用静态方法
0xb9 invokeinterface 调用接口方法
0xba
0xbb new 创建一个对象,并将其引用值压入栈顶
0xbc newarray 创建一个指定原始类型(如int, float, char…)的数组,并将其引用值压入栈顶
0xbd anewarray 创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶
0xbe arraylength 获得数组的长度值并压入栈顶
0xbf athrow 将栈顶的异常抛出
0xc0 checkcast 检验类型转换,检验未通过将抛出ClassCastException
0xc1 instanceof 检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶
0xc2 monitorenter 获得对象的锁,用于同步方法或同步块
0xc3 monitorexit 释放对象的锁,用于同步方法或同步块
0xc4 wide <待补充>
0xc5 multianewarray 创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶
0xc6 ifnull 为null时跳转
0xc7 ifnonnull 不为null时跳转
0xc8 goto_w 无条件跳转(宽索引)
0xc9 jsr_w 跳转至指定32位offset位置,并将jsr_w下一条指令地址压入栈顶

ASM字节码编辑学习笔记
http://example.com/2024/09/25/ASM字节码编辑学习笔记/
作者
cmisl
发布于
2024年9月25日
许可协议