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 { 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的核心组件,用于生成和转换方法:
- ClassReader类解析一个编译后的方法,并且通过传递ClassVisitor作为accept方法的参数获得的返回,调用MethodVisitor’相应的方法。
- ClassWriter的‘visitMethod’返回了MethodVisitor抽象类的一个实现,该实现可以直接用二进制的方式构建编译后的方法。
- 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’方法调用一次。
最后一个约束是标签是不能被共享的:每个方法必须有他们自己的标签。
现在可以猜到,可以像改造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)。
但请注意,这并非是强制:可以完全构建一个不同于类适配链的方法适配链。
甚至每一个方法都有一个不同的方法适配链。
例如,类适配器可以选择只移除普通方法中的‘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’更加有趣的适配器。
假设我们要检测一个程序中每个类的耗时,我们需要在每一个类中加入一个静态的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’。
现在一个有趣的问题是:栈哈希帧怎么办?
源码中不包含任何帧,也没有任何相转换的代码,但这是由于我们代码使用了特殊的代码么?
是否在某些特定的情境下这些帧会被更新?
答案是否定的,因为:
- 插入的指令离不开操作栈的变化。
- 插入的代码不能包含跳转指令。
- 跳转指令,更确切的讲,源码的控制流图没有改变。
这说明原始的帧没有变化,由于插入的新代码没有新增帧,压缩的原始帧也不需要修改。
现在我们可以把所有的元素组合到一起,并于‘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属性,通过存储在类中的该属性名,方法适配器可以访问该属性。
在上一节中的讲述的转换是本地的,并不依赖在当前转换前已经访问的指令集:比如在方法开始时插入的代码都是相同的,与之类似的,代码在每个‘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)。
每个过渡都标有一个条件(当前指令的值)和一个动作(一个必须被发送的指令序列)。
例如,如果当前的指令不是‘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: if (opcode == ALOAD && var == 0) { state = SEEN_ALOAD_0; return; } break; case SEEN_ALOAD_0: if (opcode == ALOAD && var == 0) { state = SEEN_ALOAD_0ALOAD_0; return; } break; case SEEN_ALOAD_0ALOAD_0: 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: if (opcode == GETFIELD) { state = SEEN_ALOAD_0ALOAD_0GETFIELD; fieldOwner = owner; fieldName = name; fieldDesc = desc; return; } break; case SEEN_ALOAD_0ALOAD_0GETFIELD: 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: mv.visitVarInsn(ALOAD, 0); break; case SEEN_ALOAD_0ALOAD_0: mv.visitVarInsn(ALOAD, 0); mv.visitVarInsn(ALOAD, 0); break; case SEEN_ALOAD_0ALOAD_0GETFIELD: mv.visitVarInsn(ALOAD, 0); mv.visitVarInsn(ALOAD, 0); mv.visitFieldInsn(GETFIELD, fieldOwner, fieldName, fieldDesc); break; } state = SEEN_NOTHING; } }
|
注意,出于和3.2.4节中‘AddTimerAdapter’示例中相同的原因,在本节中有状态的转换不需要改变栈哈希帧:改造后原本的帧仍然有效。
甚至我们不需要改变本地变量和操作栈的大小。最后必须要声明的是,有状态的转换不仅限于检测和改造指令序列。
很多其他类型的转换也是有状态的。在这种情况下,在下一节将介绍方法适配器。