ASM历险记

Posted by kingkk on 2020-08-23

前言

之前就一直说着要学ASM,但每次看着一堆JVM指令就脑壳疼,前段时间算是静下心来看了一遍文档,不得不说ASM的文档写的确实很赞,和Soot简直鲜明对比。

本文主要内容来自于ASM4官方文档,推荐直接看原文档,本文仅做个人学习记录,不保证正确性。

https://asm.ow2.io/asm4-guide.pdf

JVM基础

类结构

如上就是一个类文件的大体文件结构(其中Attribute属性值似乎在注解出现之后变得没什么用处,但还是一个可选的选项)

classpy可以比较直观的看到字节码文件结构,和与之对应的字节块。

https://github.com/zxh0/classpy

在class文件中不存在package和import之类的信息,可以理解为一个语言层面提供的”语法糖”,class文件中的类名都以全限定的方式存在,又分为几种类型。

内部名:主要用于描述类和接口类型

/替代原有的.,如String的内部名则为java/lang/String

类型描述符: 用于描述字段类型,除了Java中的类以外还有几个原生类型

方法描述符:用来描述一个函数的参数和返回值(但不包括函数名)

可以看到其实就是类型描述符的拼接,前面括号中为参数的描述符,括号后为返回类型的描述符

执行模型

Java是在线程内部执行的,每个线程都有自己的执行栈,栈由帧(Frame)组成,对应一个方法调用。

一个帧(Frame)由两部分组成:本地变量(local variables)操作数栈(operand stack)

本地变量和操作数栈都是栈结构,其单位为slot,除了long和double类型占两个slot以外,其余Java中的值都仅占一个slot。

由于long和double的存在导致不能单纯的用值的数量来判断所占slot大小,还得判断其数据类型,才能确定其最终所需的slot大小。

在Java6之后,为了加快JVM的类型校验,引入了一个栈映射帧的结构,它描述了在某一执行过程中的状态,也就是某一指令前,本地变量和操作数栈中值的类型。

为了节省空间,仅在跳转和异常处理的指令处设置了栈映射帧进行类型校验。

也正是因为存在栈映射帧这个操作,导致后面ASM在原有代码上编织字节码时,还得考虑栈映射帧相关细节,导致整体流程变的繁琐。

字节码指令

完整的字节码指令很多,这里只介绍几种常见的。

字节码指令有几个不成文的约定。

例如ILOAD表述读取一个int、boolean、byte、char、short类型的局部变量,并将它的值压入操作数栈中。

LLOAD对应long类型、FLOAD对应float、DLOAD对应double类型、ALOAD对应Java对象

因此一下用xLOAD来泛指这一类的操作

在操作数栈和局部变量见传递值的指令

xLOAD:读取一个局部变量的值,并将其压入操作数栈

xSTORE:从操作数栈弹出一个值,存储至局部变量中

仅在操作数栈中操作的指令

POP:弹出顶部栈

DUP:压入顶部栈值的副本

SWAP:交换顶部两个值

xCONST_0:压入一个常量值0

xCONST_NULL:压入一个null

LDC:压入任意 int、float、long、double、String、class常量值

xADDxSUBxMULxDIVxREM:弹出顶部两个值,进行加减乘除取余等操作,并将结果压回栈内

I2fF2DL2D:弹出一个值,类型转换后重新压入栈内

CHECKCAST t:弹出一个值,引用类型转换为t之后重新压入栈内

NEW:将一个指定类型的新对象压入栈内

GETFIELD:弹出一个对象引用,并压入其指定字段的值

PUTFIELD:弹出一个值和一个对象引用,并将值存入对象的指定字段中

INVOKExxx:弹出目标对象与参数所需的对象个数,调用方法(根据xxx的不同调用不同类型的方法),并将返回值压入栈中

xALOAD:弹出一个索引和一个数组,并压入数组指定索引处的值

xASTORE:弹出一个值、一个索引、一个数组,并将数组的指定索引处存入对应值。

IFEQIFNEIFGE:从栈中弹出一个int值,这个值等于/小于/大于0则跳转至指定lable,否则正常执行下一个指令

RETURNxRETURN:用于终止一个方法执行,返回对应值

ASM操作

asm主要有两个api,core-api和tree-api,类似于XML解析的SAX(Simple API for XML,事件驱动)和DOM(Document Object Model,基于对象)

core-api也是基于事件驱动的,因此内存和性能消耗要小于tree-api,但使用可能偏复杂

tree-api会在一开始将类读取至内存中,因内存和性能消耗偏大,但是可以获取整个类

如下操作没有明确说明都是基于core-api的(貌似也是大家比较偏爱的一个api,不知道是出于性能考虑还是什么)

ASM的运行结构可以理解为由类读取器(ClassReader)读取类,产生事件驱动,然后由多个转换器(Adapter)对读取到的事件进行转换,最后交由类编写器(ClassWriter)向指定文件中写入类。

新生成类

可以直接通过ClassWriter生成一个任意的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyClassWriter implements Opcodes {
public static void main(String[] args) throws IOException {
ClassWriter cw = new ClassWriter(0);
cw.visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,
"pkg/Comparable", null, "java/lang/Object", new String[]{"pkg/Mesurable"});
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC,
"LESS", "I", null, -1).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC,
"EQUAL", "I",
null, 0).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC,
"GREATER", "I",
null, 1).visitEnd();
cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo",
"(Ljava/lang/Object;)I", null, null).visitEnd();
cw.visitEnd();
Files.write(Paths.get("Comparable.class"), cw.toByteArray());

}
}

如上代码就生成了一个jdk1.8版本的Comparable接口

V1_8ACC_PUBLIC这些字节码常量都在Opcodes接口中有定义,通常只要implements这个接口即可

ClassWriter.visit的参数分别代表 版本号、访问控制权限、类名、signature、父类、接口

signature这个变量是适用于泛型的,一般填null即可

ClassWriter.visitField返回一个FieldVisitor,每次调用完之后访问一次visitEnd()属于约定俗称的规范(尽管这里的visitEnd什么都没做),当多个Visitor协同调用时,可以正确触发对应的visitEnd中的修改。

最后ClassWriter.toByteArray()即可获取对应的字节码数组。

转换类

通常情况,通过读取原有的class文件,在其基础上生成新的类拥有更广泛的使用场景,它的运行模型也更贴近一开始给出的转换模型。其中会通过ClassReader读取原有的字节码文件,然后交给自定义的ClassVisitor转换器进行对应的转换,最后从过ClassWriter生成对应的类。

例如通过一个简单的例子,将之前生成的JDK1.8的Comparable类转换成JDK1.5的字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ChangeVersionAdapter extends ClassVisitor implements Opcodes {
public ChangeVersionAdapter(ClassVisitor cv) {
super(Opcodes.ASM4, cv);
}

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

public static void main(String[] args) throws Exception {
byte[] b1 = Files.readAllBytes(Paths.get("Comparable.class"));
ClassWriter cw = new ClassWriter(0);
ClassVisitor cv = new ChangeVersionAdapter(cw);
ClassReader cr = new ClassReader(b1);
cr.accept(cv, 0);
Files.write(Paths.get("Comparable2.class"), cw.toByteArray());
}
}

可以看到对应的做法其实也很简单,主要是通过重写visiti方法,在委托下层ClassVisitor时(ClassWriter本质上也是一个ClassVisitor)将原有的version版本替换成1.5的即可。

这样,这个过程就被串联成了一个简单的调用链,通过ClassReader产生驱动事件,将事件传递给我们自定义的ClassVisitor,经过一些自定义转换之后,传递给下一层的ClassWriter,最后输出成字节码文件。

在我们调用ClassReader.accept之后,ClassReader会依次调用对应的visitxxx方法,并将驱动事件依次向下传递,这里是直接传递给我们的转换器。因此在我们编写的转换器中也要将这个事件经过转换之后再传递给下一层的转换器,这里就是ClassWriter,最后调用ClassWriter的toByteArray即可。

当这个转换链复杂时,可以由多个ClassReader产生事件驱动,并进行多个ClassVisitor转换器的转换,最后多个事件统一传递到ClassWriter中,也就形成了本节开头提到的复杂转换模型。

删除方法

删除方法的方式相对简单,只要在读取到对应方法时传递一个null给下层转换器即可。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public MethodVisitor visitMethod(
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions) {
if (name.equals("compareTo") && descriptor.equals("(Ljava/lang/Object;)I")) {
return null;
}
return cv.visitMethod(access, name, descriptor, signature, exceptions);
}

这里删除了之前生成接口中的compareTo方法,也可以将方法名和方法描述符弄成成员变量的形式,这样就可以动态删除指定方法(方法名和方法描述符可以确定唯一的方法)。

添加字段

为了确保不重复添加字段,需要在每次visitField时记录访问过的字段,并在最后visitEnd时添加对应的字段(因为只有最后才能知道该字段是否被加载过)

但其实通常用一些特征性的字符即可避免这一情况,这里从简,直接在visitEnd访问对应字段,即可完成字段的添加。

1
2
3
4
5
6
7
8
9
@Override
public void visitEnd() {
FieldVisitor fv = cv.visitField(ACC_PUBLIC + ACC_STATIC + ACC_FINAL, "newField", "Ljava/lang/String;", null, null);
if (fv != null) {
// 前面删除字段时可以知道,visitField方法可能会返回一个null
fv.visitEnd();
}
cv.visitEnd();
}

这样即可完成一个newField字段的添加

生成方法

刚才的例子都是以接口为例,一个好处就是它只提供函数声明,不提供具体的方法,但实际上我们可能更多的要生成一个可以实际运行的方法。

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
ClassWriter cw = new ClassWriter(0);
cw.visit(V1_8, ACC_PUBLIC, "pkg/Bean", null, "java/lang/Object", null);
cw.visitField(ACC_PRIVATE, "f", "I", null, 0);

MethodVisitor getter = cw.visitMethod(ACC_PUBLIC, "getF", "()I", null, null);
getter.visitCode();
getter.visitVarInsn(ALOAD, 0);
getter.visitFieldInsn(GETFIELD, "pkg/Bean", "f", "I");
getter.visitInsn(IRETURN);
getter.visitMaxs(1, 1);
getter.visitEnd();

MethodVisitor setter = cw.visitMethod(ACC_PUBLIC, "setF", "(I)V", null, null);
setter.visitCode();
setter.visitVarInsn(ILOAD, 1);
Label label = new Label();
setter.visitJumpInsn(IFLT, label);
setter.visitVarInsn(ALOAD, 0);
setter.visitVarInsn(ILOAD, 1);
setter.visitFieldInsn(PUTFIELD, "pkg/Bean", "f", "I");
Label end = new Label();
setter.visitJumpInsn(GOTO, end);
setter.visitLabel(label);
setter.visitFrame(F_SAME, 0, null, 0, null);
setter.visitTypeInsn(NEW, "java/lang/IllegalArgumentException");
setter.visitInsn(DUP);
setter.visitMethodInsn(INVOKESPECIAL, "java/lang/IllegalArgumentException",
"<init>", "()V", false);
setter.visitInsn(ATHROW);
setter.visitLabel(end);
// 指定栈映射帧
// 指定对应本地变量表和操作栈的类型对应关系
setter.visitFrame(F_SAME, 0, null, 0, null);
setter.visitInsn(RETURN);
// 指定局部变量表和操作栈
setter.visitMaxs(2, 2);
setter.visitEnd();

Files.write(Paths.get("Bean.class"), cw.toByteArray());

这里新生成了一个类pkg/Bean,并生成了一个成员变量f,这都是之前演示过的内容,重点在后面的生成两个函数。

这里为f成员变量生成了一个getter和setter,对应的生产类如下。

setter

setter相对比较简单,通过ClassWriter.visitMethod即可获得到一个 MethodVisitor

visitCode和visitEnd分别对应函数的开头和结尾,意味着可以在转换时在这里做一些额外操作。

之后就是正经的函数字节码指令,每一行对应一条指令

  • 通过ALOAD从本地变量中将this变量压入操作数栈
  • 弹出操作数栈的this变量,执行GETFIELD指令,读取f字段,然后压回操作数栈
  • 弹出栈顶的f字段的指,通过IRETURN返回

然后的visitMaxs则是用来声明本地变量表和操作数栈的大小,这里可以简单的推算出两者都为1。

getter

getter方法则略微复杂一点,加了一部分if判断。这里明显比getter中多了一些新的操作。

首先来介绍lable,lable用来标记一段字节码操作,可以理解为编程中用大括号引起的一个block。

lable的范围声明则是从visitLabel开始的,直至下一个visitLabel或者无操作。

所以这里定义的两个lable,分别对应如下字节码

lable:

1
2
3
4
5
6
setter.visitFrame(F_SAME, 0, null, 0, null);
setter.visitTypeInsn(NEW, "java/lang/IllegalArgumentException");
setter.visitInsn(DUP);
setter.visitMethodInsn(INVOKESPECIAL, "java/lang/IllegalArgumentException",
"<init>", "()V", false);
setter.visitInsn(ATHROW);

先忽略visitFrame,这部分的操作就是生成一个IllegalArgumentException然后抛出。

end:

1
2
setter.visitFrame(F_SAME, 0, null, 0, null);
setter.visitInsn(RETURN);

这里则是直接返回。

所以整体逻辑也就比较明了了。

获取第一个参数的值,小于0则跳转至lable,否则就给f成员变量赋值之后跳转到end。也就实现了逻辑语句。

然后就是visitFrame

之前JVM基础中有提到,JVM会在跳转和异常处理处设置栈映射帧,对本地变量和操作数栈进行类型校验。

visitFrame的函数声明:

1
public void visitFrame(final int type, final int numLocal, final Object[] local, final int numStack, final Object[] stack)

第一个参数是栈映射帧的类型,numLocallocal表示局部变量的大小和类型数组,numStackstack表示操作数栈的大小和和类型数组。

可以看到,手动计算visitFrame和visitMax所需的栈映射帧、本地变量、操作数栈的大小是一件很繁琐的事情,尤其是对一个复杂的函数而言。因此ClassWriter提供了几个参数,可以帮我们自动完成这些操作,但牺牲的是一部分性能。

  • new ClassWriter(0):不计算任何东西
  • new ClassWriter(ClassWriter.COMPUTE_MAXS):自动计算本地变量和操作数栈的大小,10%性能损耗
  • new ClassWriter(ClassWriter.COMPUTE_FRAMES):自动计算本地变量、操作数栈大小和所需的栈映射帧,50%性能损耗

可以看到,比较繁琐的栈映射帧也带来了更多的性能损耗。

转换方法

和转换类一样,可以在原有方法的指令基础上进行修改,添加或删除一些想要的指令操作。

例如为以下函数m记录下运行的时间

1
2
3
4
5
public class C {
public void m() throws Exception {
Thread.sleep(100);
}
}

这里借助了一个静态成员变量timer来实现这个功能,并为除了接口、构造函数的方法都添加这个计时的功能

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
public class AddTimerAdapter extends ClassVisitor implements Opcodes {
private String owner;
private boolean isInterface;

public AddTimerAdapter(ClassVisitor cv) {
super(ASM4, cv);
}

@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
cv.visit(version, access, name, signature, superName, interfaces);
owner = name;
isInterface = (access & ACC_INTERFACE) != 0;
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
if (!isInterface && mv != null && !name.equals("<init>")) {
mv = new AddTimerMethodAdapter(mv);
}
return mv;
}

@Override
public void visitEnd() {
if (!isInterface) {
FieldVisitor fv = cv.visitField(ACC_PUBLIC + ACC_STATIC, "timer", "J", null, null);
if (fv != null) {
fv.visitEnd();
}
}
cv.visitEnd();
}
}

visitMethod中,将ClassVisitor.visitMethod返回的MethodVisitor传递给我们的AddTimerMethodAdapter,经过转换之后再return回去。

visitEnd时添加了timer静态变量。

然后是AddTimerMethodAdapter的部分

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
public class AddTimerMethodAdapter extends MethodVisitor implements Opcodes {
public AddTimerMethodAdapter(MethodVisitor mv) {
super(ASM4, mv);
}

@Override
public void visitCode() {
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J", false);
mv.visitInsn(LSUB);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}

@Override
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J", false);
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
mv.visitInsn(opcode);
}

@Override
public void visitMaxs(int maxStack, int maxLocals) {
mv.visitMaxs(maxStack + 4, maxLocals);
}
}

visitCode之前介绍过是MethodVisitor最先访问的函数,也就相当于在函数的开头进行操作。

这里将当前时间戳减去timer的值并重新赋值给了timer。

然后重写visitInsn,在函数return和throw抛出之前插入对应代码。

比较庆幸的是,这里插入的代码没有用到跳转语句,也就是说控制流图CFG没有被更改,从而无需重写计算栈映射帧的大小。

但是由于引入新变量的原因,还是要改操作数栈的大小。

由于visitCodevisitInsn中的代码都在一开始往操作数栈中压入了两个long类型的变量,以最坏的情况来算,这里需要额外增加4个slot的大小。(不一定非得给出最优的操作数栈大小,可以大于或等于最优值,虽然会存在一些内存浪费)

最后生成的字节码文件如下

ASM中还提供了一些比较有用的工具来简化我们完成这个工作

AnalyzerAdapter

通过AnalyzerAdapterstack成员变量,获取到实时的操作数栈大小,从而获取得到操作数栈的最优值(但实际不推荐使用,其效率远低于COMPUTE_MAXS

maxStack为自定义的成员变量,记录最大的操作数栈大小

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
@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J", false);
mv.visitInsn(LSUB);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
maxStack = 4;
}

@Override
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J", false);
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
maxStack = Math.max(maxStack, stack.size() + 4);
}
super.visitInsn(opcode);
}

@Override
public void visitMaxs(int maxStack, int maxLocals) {
mv.visitMaxs(Math.max(this.maxStack, maxStack), maxLocals);
}

或者直接利用AnalyzerAdapter中的方法进行指令插入,他会自动更新操作数栈和本地变量的大小,而无需重写visitMaxs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public void visitCode() {
super.visitCode();
super.visitFieldInsn(GETSTATIC, owner, "timer", "J");
super.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J", false);
super.visitInsn(LSUB);
super.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}

@Override
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
super.visitFieldInsn(GETSTATIC, owner, "timer", "J");
super.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J", false);
super.visitInsn(LADD);
super.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
super.visitInsn(opcode);
}

LocalVariablesSorter

或者可以借助一个本地变量来实现计时功能,LocalVariablesSorter可以很简单的帮助我们生成一个本地变量,否则这个过程将变的很繁琐。

通过newLocal函数就可以生成一个局部变量

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
public class AddTimerMethodAdapter4 extends LocalVariablesSorter implements Opcodes {
private int time;

public AddTimerMethodAdapter4(int access, final String desc, final MethodVisitor mv) {
super(ASM4, access, desc, mv);
}

@Override
public void visitCode() {
mv.visitCode();
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J", false);
time = newLocal(Type.LONG_TYPE);
mv.visitVarInsn(LSTORE, time);
}

@Override
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J", false);
mv.visitVarInsn(LLOAD, time);
mv.visitInsn(LSUB);
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
super.visitInsn(opcode);
}

@Override
public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack + 4, maxLocals);
}
}

这样,就可以通过newLocal返回的opcode,在函数的所有地方调用到这个本地变量。

但是还是要手动计算本地变量和操作数栈。

生成的class文件

AdviceAdapter

AdviceAdapter可以很简单的在RETURNATHROW指令之前插入代码。可以理解为提供了一个简易的API在函数头和末尾插入想要的指令。

只要重写onMethodEnteronMethodExit方法即可(它也继承了LocalVariablesSorter类,因此也可以使用newLocal创建本地变量)

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
public class AddTimerMethodAdapter6 extends AdviceAdapter implements Opcodes {
public AddTimerMethodAdapter6(int access, String name, String desc, MethodVisitor mv) {
super(ASM4, mv, access, name, desc);
}

@Override
protected void onMethodEnter() {
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J", false);
mv.visitInsn(LSUB);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}

@Override
protected void onMethodExit(int opcode) {
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J", false);
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}

@Override
public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack + 4, maxLocals);
}

}

工具类

像前面介绍的LocalVariablesSorterAnalyzerAdapterAdviceAdapter其实都算工具类,它帮我们简化了一些常见情况下重复的操作。

TraceClassVisitor

效果类似于javap,将字节码转换成人类友好的模式,显示类成员变量、函数前面、函数中的操作指令等。

1
2
3
ClassReader cr = new ClassReader(b1);
ClassVisitor cv = new TraceClassVisitor(new PrintWriter(System.out));
cr.accept(cv, 0);

CheckClassAdapter

它会帮我们校验我们生成的类对方法的调用顺序是否恰当、参数是否有效,从而在类加载进JVM前验证类是否有效。

当检测到异常时则会抛出异常,否则则正常传递给下一个ClassVisitor

1
2
3
4
ClassReader cr = new ClassReader(b1);
ClassVisitor tcv = new TraceClassVisitor(new PrintWriter(System.out));
ClassVisitor cv = new CheckClassAdapter(tcv);
cr.accept(cv, 0);

Type

Type中定义了很多常量,并且可以做类、内部名、类描述符、函数描述符之间的转换。

输出结果在每行的注释中

1
2
3
4
5
6
System.out.println(Type.getDescriptor(String.class));	// Ljava/lang/String;
System.out.println(Type.getInternalName(String.class)); // java/lang/String

String desc = "(ILjava/lang/String;)V";
System.out.println(Type.getReturnType(desc)); // V
System.out.println(Arrays.toString(Type.getArgumentTypes(desc))); // [I, Ljava/lang/String;]

ASMifier

个人感觉比较好用的一个类,它可以将一个class文件,转换为ASM代码的格式。

就相当于告诉你如何通过ASM生成这个class类。

可以直接命令行调用

1
java -classpath asm.jar:asm-util.jar org.objectweb.asm.util.ASMifier java.lang.Runnable

或者直接调用其main函数

1
ASMifier.main(new String[]{"D:\\p.class"});

支持类名和class文件路径

输出的结果

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
82
83
84
85
86
87
88
89
90
91
package asm.sun.reflect;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.ConstantDynamic;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.RecordComponentVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.TypePath;
public class GeneratedMethodAccessor1Dump implements Opcodes {

public static byte[] dump () throws Exception {

ClassWriter classWriter = new ClassWriter(0);
FieldVisitor fieldVisitor;
RecordComponentVisitor recordComponentVisitor;
MethodVisitor methodVisitor;
AnnotationVisitor annotationVisitor0;

classWriter.visit(V1_5, ACC_PUBLIC, "sun/reflect/GeneratedMethodAccessor1", null, "sun/reflect/MethodAccessorImpl", null);

{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
methodVisitor.visitCode();
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "sun/reflect/MethodAccessorImpl", "<init>", "()V", false);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", null, new String[] { "java/lang/reflect/InvocationTargetException" });
methodVisitor.visitCode();
Label label0 = new Label();
Label label1 = new Label();
Label label2 = new Label();
methodVisitor.visitTryCatchBlock(label0, label1, label2, "java/lang/ClassCastException");
methodVisitor.visitTryCatchBlock(label0, label1, label2, "java/lang/NullPointerException");
Label label3 = new Label();
Label label4 = new Label();
methodVisitor.visitTryCatchBlock(label1, label3, label4, "java/lang/Throwable");
methodVisitor.visitVarInsn(ALOAD, 1);
methodVisitor.visitJumpInsn(IFNONNULL, label0);
methodVisitor.visitTypeInsn(NEW, "java/lang/NullPointerException");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/NullPointerException", "<init>", "()V", false);
methodVisitor.visitInsn(ATHROW);
methodVisitor.visitLabel(label0);
methodVisitor.visitVarInsn(ALOAD, 1);
methodVisitor.visitTypeInsn(CHECKCAST, "ReflectObject");
methodVisitor.visitVarInsn(ALOAD, 2);
methodVisitor.visitJumpInsn(IFNULL, label1);
methodVisitor.visitVarInsn(ALOAD, 2);
methodVisitor.visitInsn(ARRAYLENGTH);
methodVisitor.visitIntInsn(SIPUSH, 0);
methodVisitor.visitJumpInsn(IF_ICMPEQ, label1);
methodVisitor.visitTypeInsn(NEW, "java/lang/IllegalArgumentException");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/IllegalArgumentException", "<init>", "()V", false);
methodVisitor.visitInsn(ATHROW);
methodVisitor.visitLabel(label1);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "ReflectObject", "test", "()V", false);
methodVisitor.visitLabel(label3);
methodVisitor.visitInsn(ACONST_NULL);
methodVisitor.visitInsn(ARETURN);
methodVisitor.visitLabel(label2);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "toString", "()Ljava/lang/String;", false);
methodVisitor.visitTypeInsn(NEW, "java/lang/IllegalArgumentException");
methodVisitor.visitInsn(DUP_X1);
methodVisitor.visitInsn(SWAP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/IllegalArgumentException", "<init>", "(Ljava/lang/String;)V", false);
methodVisitor.visitInsn(ATHROW);
methodVisitor.visitLabel(label4);
methodVisitor.visitTypeInsn(NEW, "java/lang/reflect/InvocationTargetException");
methodVisitor.visitInsn(DUP_X1);
methodVisitor.visitInsn(SWAP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/reflect/InvocationTargetException", "<init>", "(Ljava/lang/Throwable;)V", false);
methodVisitor.visitInsn(ATHROW);
methodVisitor.visitMaxs(5, 3);
methodVisitor.visitEnd();
}
classWriter.visitEnd();

return classWriter.toByteArray();
}
}

当遇到一些不知道如何生成的字节码,就可以先用java的形式写好之后编译成class文件,再通过ASMifier输出结果,就可以依葫芦画瓢的生成对应操作了。

Tree-API

之前的操作都是基于core-api的,ASM还提供了一个tree-api,顾名思义类似于一个树的形状来解析和操作一个类,他会在最开始将所有数据读到内存中,在对内存和性能要求不那么高的情况下,tree-api能更直观的操作一个类。

ClassNode

tree-api中用ClassNode来表示一个类节点,FieldNode表示一个字段节点,MethodNode表示一个方法节点。

如下就是通过ClassNode生成一个class类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ClassWriter cw = new ClassWriter(0);
ClassNode cn = new ClassNode();
cn.version = V1_8;
cn.access = ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE;
cn.name = "pkg/CompareByTreeNode";
cn.superName = "java/lang/Object";
cn.interfaces.add("pkg/Mesurable");
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC,
"LESS", "I", null, -1));
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC,
"EQUAL", "I", null, 0));
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC,
"GREATER", "I", null, 1));
cn.methods.add(new MethodNode(ACC_PUBLIC + ACC_ABSTRACT,
"compareTo", "(Ljava/lang/Object;)I", null, null));
cn.accept(cw);
Files.write(Paths.get("CompareByTreeNode.class"), cw.toByteArray());

这样,一些JDK版本、访问控制之类的参数就不是在类生成时实时要求的,可以是在类生成之后,动态添加的。

同时,ClassNode是继承ClassVisitor的,所以可以仿照之前的方式,通过ClassReader将一个class文件转换成一个ClassNode。

1
2
3
4
5
byte[] b1 = Files.readAllBytes(Paths.get("Comparable.class"));
ClassReader cr = new ClassReader(b1);
ClassNode cn = new ClassNode();
cr.accept(cn, 0);
System.out.println(cn.name);

MethodNode

如下是MethodNode的字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MethodNode ... {
public int access;
public String name;
public String desc;
public String signature;
public List<String> exceptions;
public List<AnnotationNode> visibleAnnotations;
public List<AnnotationNode> invisibleAnnotations;
public List<Attribute> attrs;
public Object annotationDefault;
public List<AnnotationNode>[] visibleParameterAnnotations;
public List<AnnotationNode>[] invisibleParameterAnnotations;
public InsnList instructions;
public List<TryCatchBlockNode> tryCatchBlocks;
public List<LocalVariableNode> localVariables;
public int maxStack;
public int maxLocals;
}

其中instructions是一个指令列表,类型为InsnList

InstList是由AbstractInsnNode组成的双向链表,它记录了指令之间的链接关系。

因此每个AbstractInsnNode对象是不可以复用的,要确保每条指令对应唯一的AbstractInsnNode。因此一个AbstractInsnNode在一个InstList中只能出现一次,并且不能同时属于多个InstList

AbstractInsnNode的子类则是xxxInstNode,也对应core-api中visitXxxInsn

生成方法主要也是在这个instructions上添加对应的InsnNode

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
ClassNode cn = new ClassNode();
cn.version = V1_8;
cn.access = ACC_PUBLIC;
cn.name = "pkg/Bean";
cn.superName = "java/lang/Object";
cn.fields.add(new FieldNode(ACC_PRIVATE, "f", "I", null, 0));

MethodNode mn = new MethodNode(ACC_PUBLIC, "setF", "(I)V", null, null);
InsnList il = mn.instructions;
LabelNode label = new LabelNode();
LabelNode end = new LabelNode();
il.add(new VarInsnNode(ILOAD, 1));
il.add(new JumpInsnNode(IFLT, label));
il.add(new VarInsnNode(ALOAD, 0));
il.add(new VarInsnNode(ILOAD, 1));
il.add(new FieldInsnNode(PUTFIELD, "pkg/BeanNode", "f", "I"));
il.add(new JumpInsnNode(GOTO, end));
il.add(label);
il.add(new FrameNode(F_SAME, 0, null, 0, null));
il.add(new TypeInsnNode(NEW, "java/lang/IllegalArgumentException"));
il.add(new InsnNode(DUP));
il.add(new MethodInsnNode(INVOKESPECIAL,
"java/lang/IllegalArgumentException", "<init>", "()V"));
il.add(new InsnNode(ATHROW));
il.add(end);
il.add(new FrameNode(F_SAME, 0, null, 0, null));
il.add(new InsnNode(RETURN));
mn.maxStack = 2;
mn.maxLocals = 2;

cn.methods.add(mn);
ClassWriter cw = new ClassWriter(0);
cn.accept(cw);
Files.write(Paths.get("BeanNode.class"), cw.toByteArray());

这样就可以生成一个和之前用core-api生成过一样的setF函数

然后方法的转换其实相比core-api会更加直观,只要for循环instructions变量,读取所有的InstNode,对想要的InstNode进行对应操作即可。

最后

相当于对字节码的基础知识和ASM中基础的操作都介绍了一遍。还有一些关于泛型、注解之类的操作,自认为暂时用不上,需要时再学即可。

不得不说ASM设计的还是很精妙小巧的,它更贴近于字节码底层,它能以极低的内存成本提供提高的处理性能,也能做到对字节码数据近乎百分百的解析。core-api更面向工程,对性能和内存的要求比较高,而tree-api个人感觉则更适合用来做字节码分析用。