ASM指南翻译-13 有状态转换

3.2.5有状态转换

在前面章节看到的转换都是局部的,它们都没有依赖于之前已经访问过的代码:在方法最开始添加的代码始终是一样的并且总是会被添加,同样,在RETURN之前添加的代码也是如此。这样的转换称之为无状态转换。这些转换较容易实现,但是也只有这种最简单的转换才满足这个属性。

 

更复杂的转换,需要记住之前访问过代码的一些状态。考虑这样的一个转换,移除所有ICONST_0 IADD指令序列,这些指令的效果类似于加零。当一个IADD指令被访问时,只有当最后一个被访问指令是ICONST_0时,这个指令序列才可以被移除。这需要在方法适配器内部保存一些状态。类似于这样的转换称之为有状态转换。

 

让我们详细地了解下这个示例。当一个ICONST_0指令被访问时,如果其下一条指令是IADD时,才可以将这两条指令删除。问题是在访问ICONST_0指令时,下一条指令还未知。解决方案就是推迟最初这个判断知道下一条指令被访问:如果访问到IADD指令,那么就移除这两个指令,否则忽略ICONST_0指令和当前的指令。

 

为了实现这样的一个转换:移除或者替换某些指令序列,简便的方式就是引入一个方法适配器MethodAdapter,这个类的visitXxxInsn方法会调用一个共同的方法visitInsn方法:

public abstract class PatternMethodAdapter extends MethodAdapter {

         protected final static int SEEN_NOTHING = 0;

         protected int state;

         public PatternMethodAdapter(MethodVisitor mv) {

                   super(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();

}

 

上面的转换可以像下面这样实现:

public class RemoveAddZeroAdapter extends PatternMethodAdapter {

         private static int SEEN_ICONST_0 = 1;

         public RemoveAddZeroAdapter(MethodVisitor mv) {

                   super(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方法首先会判断指令序列是否被检测到。在这个例子中,它初始化state然后立即返回,它具有删除指令序列的效果。在另外的情形中,它会调用visitInsn方法,这个方法会

忽略ICONST_0指令。如果当前的指令是ICONST_0,它会记录下来,然后返回,这样就可以在下一条指令到来时判断了。在所有其它的情形中,当前的指令直接被传递给了下一个visitor

 

标签和帧

 

如我们在前面章节中见到的一样,标签和帧会先于它们关联的指令被访问。换句话说,它们会和指令同时被访问,尽管它们本身不是指令。这会对检测指令序列这样的转换造成一定的影响,但这种影响事实上是有利的。如果我们移除的指令是跳转指令的目标,那会发生什么了?如果有指令会跳转到ICONST_0指令,这意味着这里有一个标签来代表这个指令。在移除这两个指令后,这个标签就会代表IADD之后的指令,这正是我们想要的。但是如果一些指令要跳转到IADD指令,那我们就不能移除这个指令序列了(我们不能确保在跳转之前,一个0值入栈了)。让人有希望的是,在这种情况下,在ICONST_0IADD之间必须有一个标签,并且这很容易检测到。

 

这里的原因也同样适用于栈映射帧:如果一个栈映射帧在这两个指令之间被访问,我们也不能移除这两个指令。这两种情形,我们都可以把标签和帧当做指令使用模式匹配算法来处理。这可以使用PatternMethodAdapter类完成(注意visitMaxs方法也会调用visitInsn方法,这是用来处理这样的情形:方法的结尾可能是这个序列的前缀,它们也应该被检测到):

public abstract class PatternMethodAdapter extends MethodAdapter {

         ...

         @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。在实现这个转换之前,我们最好来设计一个识别这种序列的状态机:


ASM指南翻译-13 有状态转换_第1张图片

 

每个转换都会以一个条件作为标签(就是当前的指令)以及一个动作(一个必须被执行的指令序列,以粗体表示)。例如从S1S0的转换,只有当前的指令不是ALOAD 0时才进行。在这种情形下,ALOAD 0会被访问然到达初始状态。来看看S2到自身的转换:如果找到3个或者更多的连续的ALOAD 0指令,那么将进行S2到自身的转换。在这种情形下,我们会停留在这样的状态:两条ALOAD 0指令已经被访问,同时我们将再执行一个ALOAD 0指令。一旦这个状态机模型被找到,那么编写对应的方法适配器就很直接了(下面的8switch case就对应着上面的8个转换):

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相似的原因,本节中出现的有状态转换不需要转换栈映射帧:原始的帧仍在转换之后然有效。也不需要转换局部变量和操作数栈的大小。最后,注意有状态转换不局限于检测和转换指令序列,还有很多其它类型的转换也是状态转换。下一节出现的有关方法适配器就是这样的例子。

你可能感兴趣的:(ASM)