BaiFan
文章目录
  1. 1. 3.2 接口和组件
    1. 1.1. 3.2.1 介绍
    2. 1.2. 3.2.2 生成方法 Generating methods
    3. 1.3. 3.2.3. 改造方法 Transforming methods
    4. 1.4. 3.2.4 无状态转换 Stateless transformations
    5. 1.5. 3.2.5. 有状态转换 Statefull transformations
      1. 1.5.1. 标签和帧:Labels and frames
      2. 1.5.2. 一个更加复杂的示例

ASM-方法-接口和组件

3.2 接口和组件

3.2.1 介绍

在ASM API中,用来生成和转变编译后方法的都是基于‘MethodVisitor’抽象类的(参照图表 3.4),这是由‘ClassVisitor’‘visitMethod’方法返回的。
除了一些注解和调试相关的信息(这些信息将在下一章说明),这个类定义了每个字节码指令类别一个方法,根据这些指令的参数数量和参数类型(这些类别不对应3.1.2节介绍的那些类别)。
这些方法必须按照以下顺序调用(和MethodVisitor接口在Javadoc中指定的一些额外约束):

1
2
3
4
5
6
visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )\*
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxx Insn | visitLocalVariable | visitLineNumber ) \*
visitMaxs )?
visitEnd

这意味着,如有注释和属性的话,则必须先访问,后面是非抽象方法的字节码。
对于这些方法,这些代码必须按顺序访问,在唯一一个‘visitCode’方法调用和唯一一个‘visitMaxs’方法调用之间。

代码 3.4.: 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
28
29
abstract class MethodVisitor { // public accessors ommited
MethodVisitor(int api);
MethodVisitor(int api, MethodVisitor mv);
AnnotationVisitor visitAnnotationDefault();
AnnotationVisitor visitAnnotation(String desc, boolean visible);
AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible);
void visitAttribute(Attribute attr);
void visitCode();
void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack);
void visitInsn(int opcode);
void visitIntInsn(int opcode, int operand);
void visitVarInsn(int opcode, int var);
void visitTypeInsn(int opcode, String desc);
void visitFieldInsn(int opc, String owner, String name, String desc);
void visitMethodInsn(int opc, String owner, String name, String desc);
void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs);
void visitJumpInsn(int opcode, Label label);
void visitLabel(Label label);
void visitLdcInsn(Object cst);
void visitIincInsn(int var, int increment);
void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels);
void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels);
void visitMultiANewArrayInsn(String desc, int dims);
void visitTryCatchBlock(Label start, Label end, Label handler, String type);
void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index);
void visitLineNumber(int line, Label start);
void visitMaxs(int maxStack, int maxLocals);
void visitEnd();
}

因此在一系列的事件中,‘visitCode’方法和‘visitMaxs’方法可以用于检测一个方法字节码的开始和结束。
和class一样,‘visitEnd’方法必须最后调用,并且用于检测在一系列事件中一个方法的结束。

‘ClassVisitor’‘MethodVisitor’类整合后可以用于生成一个完整的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ClassVisitor cv = ...;
cv.visit(...);
MethodVisitor mv1 = cv.visitMethod(..., "m1", ...);
mv1.visitCode();
mv1.visitInsn(...);
...
mv1.visitMaxs(...);
mv1.visitEnd();
MethodVisitor mv2 = cv.visitMethod(..., "m2", ...);
mv2.visitCode();
mv2.visitInsn(...);
...
mv2.visitMaxs(...);
mv2.visitEnd();
cv.visitEnd();

需要注意的是,没有必要为了开始访问另外一个方法,而结束当前访问的方法。
实际上,‘MethodVisitor’实例间是完全独立的,可以用任何顺序调用(但必须在‘cv.visitEnd()’调用之前使用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ClassVisitor cv = ...;
cv.visit(...);
MethodVisitor mv1 = cv.visitMethod(..., "m1", ...);
mv1.visitCode();
mv1.visitInsn(...);
...
MethodVisitor mv2 = cv.visitMethod(..., "m2", ...);
mv2.visitCode();
mv2.visitInsn(...);
...
mv1.visitMaxs(...);
mv1.visitEnd();
...
mv2.visitMaxs(...);
mv2.visitEnd();
cv.visitEnd();

ASM提供了三个基于MethodVisitor API的核心组件,用于生成和转换方法:

  1. ClassReader类解析一个编译后的方法,并且通过传递ClassVisitor作为accept方法的参数获得的返回,调用MethodVisitor’相应的方法。
  2. ClassWriter‘visitMethod’返回了MethodVisitor抽象类的一个实现,该实现可以直接用二进制的方式构建编译后的方法。
  3. MethodVisitor类可以传递所有调用它的方法给另一个MethodVisitor类。MethodVisitor类可以看作一个事件过滤器。

第50页 50/154

ClassWriter选项
如3.1.5节所讲,计算一个方法的栈哈希帧不是一件简单的事情:你需要计算所有的帧,找到对应着跳转目标的帧,或者在无条件跳转后紧挨着的帧,并且最后还要压缩保留的帧。
同样的,计算本地变量和操作栈部分的大小看似容易,其实不然。
庆幸的是ASM可以帮你处理这些。
当你创建一个ClassWriter对象的时候,你可以指定自动计算这些:

  • 使用‘new ClassWriter(0)’构造函数,不会自动计算这些属性。你必须自己计算帧的大小、本地变量的大小和操作栈的大小。
  • 使用‘new ClassWriter(ClassWriter.COMPUTE_MAXS)’构造函数,本地变量的大小和操作栈的大小会被自动计算。你仍然需要调用‘visitMax’方法,但是你可以传递任意参数:ASM会忽略这些参数,并重新计算它们的大小。使用该选项,你必须计算帧的大小。
  • 使用‘new ClassWriter(ClassWriter.COMPUTE_FRAMES)’构造函数,所有的属性都会被自动计算。你不必调用‘visitFrame’方法,但你仍然需要调用‘visitMax’方法(ASM会忽略这些参数,并重新计算它们的大小)。

使用这些选项非常方便,但有一定的性能损耗:‘COMPUTE_MAX’会使‘ClassWriter’慢10%,‘COMPUTE_FRAMES’选项会慢两倍。
这必须将程序自己计算的时间与开发人员自己计算的时间相比较:在特定的情况下往往有更方便、快速的算法来计算这些,跟在ASM中使用的算法相比,必须能够处理所有情况。

需要注意的是,如果开发人员自己计算帧的大小,可以让‘ClassWriter’来为你处理压算环节。
这种情况下,你紧紧需要调用‘visitFrame(F_NEW, nLocals, locals, nStack, stack)’去访问未被压缩的帧,‘nLocals’**和‘nStack’表示本地变量和操作栈的大小,*’locals’‘stack’*是这些值相对应的数据类型数组(详情参考Javadoc)。

同样需要注意的是,为了自动计算帧,有时候需要还要计算两个给定class的公共的超类。
默认‘ClassWriter’会为它们计算,在‘getCommonSuperClass’方法中,通过加载两个class到JVM中,并通过反射API实现。
如果你生成了几个class文件,并且它们之间相互引用,这可能是会是一个问题,因为引用的class可能还不存在。
在这种情况下,你可以重写‘getCommonSuperClass’方法来解决这个问题。

3.2.2 生成方法 Generating methods

在3.1.3章中定义的‘getF’方法的字节码,可以通过下面的方法调用生成(mv是一个MethodVisitor对象):

1
2
3
4
5
6
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "pkg/Bean", "f", "I");
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();

第一个调用开启了字节码生成。紧跟着三个调用生成了这个方法的三条指令(如你所见,字节码和ASM API之间的映射非常简单明了)。
‘visitMaxs’方法,必须在所有指令方法调用之后调用。
该方法用于指定本方法执行帧的本地变量区和操作栈大小。
如3.1.3节所见,本地变量区和操作栈的大小都是一个槽。
最后调用‘visitEnd’方法生成本方法。

‘setF’方法和构造函数也可以使用相同的方法生成字节码。
一个更加有趣的示例是‘checkAndSetF’方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mv.visitCode();
mv.visitVarInsn(ILOAD, 1);
Label label = new Label();
mv.visitJumpInsn(IFLT, label);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitFieldInsn(PUTFIELD, "pkg/Bean", "f", "I");
Label end = new Label();
mv.visitJumpInsn(GOTO, end);
mv.visitLabel(label);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitTypeInsn(NEW, "java/lang/IllegalArgumentException");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL,"java/lang/IllegalArgumentException", "<init>", "()V");
mv.visitInsn(ATHROW);
mv.visitLabel(end);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();

‘visitCode’‘visitEnd’方法调用之间,你可以看到与3.1.5节结束时展示的字节码所准确对应的方法调用:每个指令、标签和帧都对应一个方法调用(唯一的例外就是声明和构造‘labe’‘end’标签)。

备注

一个标签对象用于后面的需要该标签的指令。
例如‘end’标签用于‘RETURN’指令,并不是紧跟其后的被访问的帧,因为帧不是一个指令。
多个标签用于一个相同的指令是完全合法的,但一个标签只能仅仅用于一个指令。
换句话说,使用不同的标签连续调用‘visitLabel’方法是可以的,但在一个指令中的标签必须仅能被‘visitLabel’方法调用一次。
最后一个约束是标签是不能被共享的:每个方法必须有他们自己的标签。

3.2.3. 改造方法 Transforming methods

现在可以猜到,可以像改造class一样改造方法,即通过使用一个方法适配器转发调用它的方法,并进行一些修改:通过改变参数可以用于修改单条指令,通过不转发收到的调用来移除一个指令,通过在收到的调用间插入方法来插入新的指令。
‘MethodVisitor’类提供了一个这样的方法适配器,不做任何处理,仅仅转发它所收到的方法调用。

为了理解如何使用方法适配器,让我们来设计一个非常简单的适配器:移除方法内部的‘NOP’指令(移除这些指令不会出现任何问题,因为它们不做任何操作):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import static org.objectweb.asm.Opcodes.ASM4;
import static org.objectweb.asm.Opcodes.NOP;
import org.objectweb.asm.MethodVisitor;
public class RemoveNopAdapter extends MethodVisitor {
public RemoveNopAdapter(MethodVisitor mv) {
super(ASM4, mv);
}
@Override
public void visitInsn(int opcode) {
if (opcode != NOP) {
mv.visitInsn(opcode);
}
}
}

该适配器可以用在一个class适配器中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import static org.objectweb.asm.Opcodes.ASM4;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
public class RemoveNopClassAdapter extends ClassVisitor {
public RemoveNopClassAdapter(ClassVisitor cv) {
super(ASM4, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
MethodVisitor mv;
mv = cv.visitMethod(access, name, desc, signature, exceptions);
if (mv != null) {
mv = new RemoveNopAdapter(mv);
}
return mv;
}
}

换句话说,在链路中,类适配器(ClassAdapter)仅仅创建了一个方法适配器(MethodAdapter),封装了下一个类访问器(ClassVisitor)返回的方法访问器(MethodVisitor),并返回封装后的方法适配器。
其效果是一个方法适配链的构建,似于一个类适配链的构建(参考图表 3.5)。

图表 3.5 RemoveNopAdapter时序图

但请注意,这并非是强制:可以完全构建一个不同于类适配链的方法适配链。
甚至每一个方法都有一个不同的方法适配链。
例如,类适配器可以选择只移除普通方法中的‘NOP’指令,而不移除构造函数中的。
实现的方式如下:

1
2
3
4
5
6
...
mv = cv.visitMethod(access, name, desc, signature, exceptions);
if (mv != null && !name.equals("<init>")) {
mv = new RemoveNopAdapter(mv);
}
...

在这种情况下,适配链对于构造函数来说会短一点。
与之相反,构造函数的调用链也可以更长,当几个方法适配链在visitMethod方法中被一起创建。
甚至方法适配链可以于类适配链拥有不同的拓扑结构。
例如类适配链可以是线性的,方法适配链可以具有多个分支结构:

1
2
3
4
5
6
7
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
MethodVisitor mv1, mv2;
mv1 = cv.visitMethod(access, name, desc, signature, exceptions);
mv2 = cv.visitMethod(access, "_" + name, desc, signature, exceptions);
return new MultiMethodAdapter(mv1, mv2);
}

现在,你已经知道在一个类适配器中,多个方法适配如何使用和结合,接下来让我们实现一些比‘RemoveNopAdapter’更加有趣的适配器。

3.2.4 无状态转换 Stateless transformations

假设我们要检测一个程序中每个类的耗时,我们需要在每一个类中加入一个静态的timer属性,我们只需要把每一个方法的执行时间加在该属性上。
换句话说,我们想要将一个类C:

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

转换成:

1
2
3
4
5
6
7
8
public class C {
public static long timer;
public void m() throws Exception {
timer -= System.currentTimeMillis();
Thread.sleep(100);
timer += System.currentTimeMillis();
}
}

为了有一个如何通过ASM实现该转换的思路,我们可以编译这两个类,使用‘TraceClassVisitor’输出两个编译类的字节码,并进行比较(可以使用Textifier,或者使用ASMifier)。
下面是使用默认的Textifier输出,注意不同的部分(粗体):

GETSTATIC C.timer : J
INVOKESTATIC java/lang/System.currentTimeMillis()J
LSUB
PUTSTATIC C.timer : J
LDC 100
INVOKESTATIC java/lang/Thread.sleep(J)V
GETSTATIC C.timer : J
INVOKESTATIC java/lang/System.currentTimeMillis()J
LADD
PUTSTATIC C.timer : J
RETURN
MAXSTACK = 4
MAXLOCALS = 1

可以看到,我们需要在方法的开始增加四条指令,在‘return’指令前增加另外四条指令。
同样也需要更改操作栈的最大值。方法代码的开始使用visitCode方法访问。
因此我们可以在方法适配器中重写该方法,增加开始的四条指令:

1
2
3
4
5
6
7
public void visitCode(){
mv.visitCode();
mv.visitFieldInsn(GETSTATIC,owner,"timer","J");
mv.visitMethodInsn(INVOKESTATIC,"java/lang/System","currentTimeMillis","()J");
mv.visitInsn(LSUB);
mv.visitFieldInsn(PUTSTATIC,owner,"timer","J");
}

owner参数必须设置成需要改造类的名字。
现在我们需要在RETURN指令前插入四条指令,但是也可能是在xRETURN指令或者ATHROW指令前,这些指令都是结束方法执行的指令。
这些指令需要参数,因此可以使用visitInsn方法访问。
我们可以覆盖该方法来增加四条指令:

1
2
3
4
5
6
7
8
9
10
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");
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
mv.visitInsn(opcode);
}

最后我们需要更操作栈的最大值。
变更后的指令集中,我们会压入栈中两个long型值,所有我们需要在操作栈上有四个槽。
在方法开始,操作栈会初始换成空的,因此方法开始时新增的四条指令需要栈的大小是4.
同时我们也知道,插入的四个指令在执行完后,操作栈的空间大小会恢复空(因为这些指令执行后,会弹出所有压入栈中的值)。
总结一下,如果原代码需要栈的大小是s,那么改造后需要的最大栈空间是‘max(4,s)’,即取变量‘s’和常量‘4’的最大值。
不幸的,我们还在方法结束前增加了四条指令,我们需要知道在这四条指令执行前,操作栈的大小。
我们只知道,该值小于等于‘s’。因此我们可以总结出来,插入这四条指令后,我们需要将栈的空间大小设置成‘s+4’
这种最坏的情况在实践中几乎和很少发生:通常的编译器,在执行返回指令前操作栈中仅保留返回值,即需要的栈空间大小可能是0、1,最多是2。
但是如果我们想要处理所有可能发生的情况,我们就必须作最坏的打算2

2:最坏的情况
给出最佳的操作栈不是必要的。给出任何大于等于最佳操作栈大小的值就可以,尽快这样会浪费线程执行栈的内存。

我们必须重写‘visitMaxs’方法,如下所示:

1
2
3
public void visitMaxs(int maxStack, int maxLocals) {
mv.visitMaxs(maxStack + 4, maxLocals);
}

当然也可以不用操心最大操作栈大小,可以依靠‘COMPUTE_MAXS’参数,使用该参数后会计算出最佳的操作栈大小,而不是最坏情况的值。
对于这么一个简单的转换,没有必要花费大力气来手动更新‘maxStack’

现在一个有趣的问题是:栈哈希帧怎么办?
源码中不包含任何帧,也没有任何相转换的代码,但这是由于我们代码使用了特殊的代码么?
是否在某些特定的情境下这些帧会被更新?
答案是否定的,因为:

  1. 插入的指令离不开操作栈的变化。
  2. 插入的代码不能包含跳转指令。
  3. 跳转指令,更确切的讲,源码的控制流图没有改变。
    这说明原始的帧没有变化,由于插入的新代码没有新增帧,压缩的原始帧也不需要修改。

现在我们可以把所有的元素组合到一起,并于‘ClassVisitor’‘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
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
public class AddTimerAdapter extends ClassVisitor {
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();
}
class AddTimerMethodAdapter extends MethodVisitor {
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");
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");
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);
}
}
}

类适配器用于实例化方法适配器(排除构造方法),但也在类中加入了一个timer属性,通过存储在类中的该属性名,方法适配器可以访问该属性。

3.2.5. 有状态转换 Statefull transformations

在上一节中的讲述的转换是本地的,并不依赖在当前转换前已经访问的指令集:比如在方法开始时插入的代码都是相同的,与之类似的,代码在每个‘RETURN’指令前插入的,也都是相同。
像这样的转换被成为无状态的转换。这种转换很容易使先,只有简单的转换并验证该属性。
更负责的转换需要在本次转换前,存储某些已被访问过的指令的状态。
例如,考虑一个转换:移除所有出现的指令序列‘ICONST_0 IADD’,该指令序列产生的效果是数值加0。
显然,当一个‘IADD’指令被访问时,当且仅当上一个被访问的指令是‘ICONST_0’时,移除这两个指令。

这个转换需要在方法适配器中存储状态信息。
出于这个原因,这种转换被称为有状态转换。
让我们更加仔细的看一下这个例子。当一个‘ICONST_0’指令被访问时,如果下一条指令是IADD,那么该条指令必须被移除。
问题是下一条指令还不知道是什么。
解决方法是推迟访问该指令到一下条指令:当访问到下一个指令时,如果指令是‘IADD’,那么移除这两个指令,否则访问‘ICONST_0’指令和当前指令。
为了实现删除或替换某些指令序列,可以很方便的引入一个‘MethodVisitor’的子类,使用子类的‘visitXxx Insn’方法调用通用的‘visitInsn’方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class PatternMethodAdapter extends MethodVisitor {
protected final static int SEEN_NOTHING = 0;
protected int state;
public PatternMethodAdapter(int api, MethodVisitor mv) {
super(api, mv);
}
@Overrid
public void visitInsn(int opcode) {
visitInsn();
mv.visitInsn(opcode);
}
@Override
public void visitIntInsn(int opcode, int operand) {
visitInsn();
mv.visitIntInsn(opcode, operand);
}
...
protected abstract void visitInsn();
}

然后,上面的转换可以被这样实现:

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
public class RemoveAddZeroAdapter extends PatternMethodAdapter {
private static int SEEN_ICONST_0 = 1;
public RemoveAddZeroAdapter(MethodVisitor mv) {
super(ASM4, mv);
}
@Override
public void visitInsn(int opcode) {
if (state == SEEN_ICONST_0) {
if (opcode == IADD) {
state = SEEN_NOTHING;
return;
}
}
visitInsn();
if (opcode == ICONST_0) {
state = SEEN_ICONST_0;
return;
}
mv.visitInsn(opcode);
}
@Override
protected void visitInsn() {
if (state == SEEN_ICONST_0) {
mv.visitInsn(ICONST_0);
}
state = SEEN_NOTHING;
}
}

‘visitInsn(int)’方法首先测试需要移除的指令序列是否被检测到了。
在这种情况中,重新初始化状态后就立即返回,产生的效果是移除指令集序列。
在其他情况下,调用通用的‘visitInsn(int)’方法,发出一个‘ICONST_0’指令,如果这个是最后被访问的指令。
然后,如果当前的指令是‘ICONST_0’,就会记录这种状态并且返回,为了推迟关于该指令的决策。
在所有其他情况下,当前指令被转发给下一个访问器。

标签和帧:Labels and frames

在上一节中,我们看到标签和帧会在他们所关联的指令前即将访问前被访问。
换句话说他们作为指令集同时被访问,尽管他们本身不是指令。
这会对检测指令集序列的转换产生影响,但这种影响实际上是一个优点。
事实上,如果我们移除的一个指令是一个跳转指令的目标,那么会发生什么?
如果某些指令可能会跳转到‘ICONST_0’,这意味着有一个标签指派该指令。
在移除这两个指令后,这个标签会指派给已被移除‘IADD’指令的后一个指令,这是我们想要的。
但某些指令可能跳转到‘IADD’指令,我们就不能移除这个指令序列(我们不能保证,在跳转指令前有一个‘0’值被压入到栈上)。。
庆幸的是,在本示例中,在‘ICONST_0’指令和‘IADD’指令中间肯定会有一个标签,可以很容易检测到。

原因和栈哈希帧相同:如果在两个指令中间有一个栈哈希帧被访问,我们就不能移除这两个指令。
在这两种情况下,可以在模式匹配算法中,将标签和帧作为指令处理。
这些可以在‘PatternMethodAdapter’中实现(注意:visitMaxs也会调用通用的‘visitInsn’方法,用于处理将一个方法的结束当作序列的前缀,这种情况必须被检测到):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class PatternMethodAdapter extends MethodVisitor {
...
@Override public void visitFrame(int type, int nLocal, Object[] local,
int nStack, Object[] stack) {
visitInsn();
mv.visitFrame(type, nLocal, local, nStack, stack);
}
@Override public void visitLabel(Label label) {
visitInsn();
mv.visitLabel(label);
}
@Override public void visitMaxs(int maxStack, int maxLocals) {
visitInsn();
mv.visitMaxs(maxStack, maxLocals);
}
}

在下一节我们会看到,一个编译后的方法可能会包含源文件的行号,用于实例化异常的栈跟踪信息。
这种信息使用‘visitLineNumber’方法访问,和调用指令同时掉用。
在两个指令序列的中间仍然存在行号,这对转换和移除指令序列没有任何影响。
因此解决方案是在匹配算法中完全忽视他们。

一个更加复杂的示例

上一个示例可以很容易的延用到更加复杂的指令序列。
设想一个转换,移除自己赋值给自己的指令,一般是由错误书写,比如‘f==f’;或者‘ALOAD 0 ALOAD 0 GETFIELD f PUTFIELD f’这样的字节码。
在实现这个转换之前,最好先设计一个状态机来识别这种序列(参考图表 3.6)。

图表 3.6 指令序列'ALOAD 0 ALOAD 0 GETFIELD f PUTFIELD f'的状态机

每个过渡都标有一个条件(当前指令的值)和一个动作(一个必须被发送的指令序列)。
例如,如果当前的指令不是‘ALOAD 0’会发生从‘S1’‘S0’的过渡。
在这种情况下访问‘ALOAD 0’指令,能到达该状态,会被发送。
需要注意‘S2’到本身的过度:这个过度发生在有三个以上的连续‘ALOAD 0’指令被访问,就将两个‘ALOAD 0’之后访问的‘ALOAD 0’指令发送出去。
只要状态机被发现了,编写相应的方法适配器是很简单的(8个swtich case对应了图表中的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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class RemoveGetFieldPutFieldAdapter extends PatternMethodAdapter {
private final static int SEEN_ALOAD_0 = 1;
private final static int SEEN_ALOAD_0ALOAD_0 = 2;
private final static int SEEN_ALOAD_0ALOAD_0GETFIELD = 3;
private String fieldOwner;
private String fieldName;
private String fieldDesc;
public RemoveGetFieldPutFieldAdapter(MethodVisitor mv) {
super(mv);
}
@Override
public void visitVarInsn(int opcode, int var) {
switch (state) {
case SEEN_NOTHING: // S0 -> S1
if (opcode == ALOAD && var == 0) {
state = SEEN_ALOAD_0;
return;
}
break;
case SEEN_ALOAD_0: // S1 -> S2
if (opcode == ALOAD && var == 0) {
state = SEEN_ALOAD_0ALOAD_0;
return;
}
break;
case SEEN_ALOAD_0ALOAD_0: // S2 -> S2
if (opcode == ALOAD && var == 0) {
mv.visitVarInsn(ALOAD, 0);
return;
}
break;
}
visitInsn();
mv.visitVarInsn(opcode, var);
}
@Override
public void visitFieldInsn(int opcode, String owner, String name, String desc) {
switch (state) {
case SEEN_ALOAD_0ALOAD_0: // S2 -> S3
if (opcode == GETFIELD) {
state = SEEN_ALOAD_0ALOAD_0GETFIELD;
fieldOwner = owner;
fieldName = name;
fieldDesc = desc;
return;
}
break;
case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 -> S0
if (opcode == PUTFIELD && name.equals(fieldName)) {
state = SEEN_NOTHING;
return;
}
break;
}
visitInsn();
mv.visitFieldInsn(opcode, owner, name, desc);
}
@Override
protected void visitInsn() {
switch (state) {
case SEEN_ALOAD_0: // S1 -> S0
mv.visitVarInsn(ALOAD, 0);
break;
case SEEN_ALOAD_0ALOAD_0: // S2 -> S0
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 0);
break;
case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 -> S0
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, fieldOwner, fieldName, fieldDesc);
break;
}
state = SEEN_NOTHING;
}
}

注意,出于和3.2.4节中‘AddTimerAdapter’示例中相同的原因,在本节中有状态的转换不需要改变栈哈希帧:改造后原本的帧仍然有效。
甚至我们不需要改变本地变量和操作栈的大小。最后必须要声明的是,有状态的转换不仅限于检测和改造指令序列。
很多其他类型的转换也是有状态的。在这种情况下,在下一节将介绍方法适配器。

文章目录
  1. 1. 3.2 接口和组件
    1. 1.1. 3.2.1 介绍
    2. 1.2. 3.2.2 生成方法 Generating methods
    3. 1.3. 3.2.3. 改造方法 Transforming methods
    4. 1.4. 3.2.4 无状态转换 Stateless transformations
    5. 1.5. 3.2.5. 有状态转换 Statefull transformations
      1. 1.5.1. 标签和帧:Labels and frames
      2. 1.5.2. 一个更加复杂的示例