首页 文章

在ASM的字节码方法内联期间重新映射变量

提问于
浏览
8

我正在使用ASM进行在线字节码方法内联优化 . 我的更改基于示例 3.2.6 Inline Methodhttp://asm.ow2.org/current/asm-transformations.pdf) . 测试示例(在Caller :: test中内联被调用者的计算(int,int))是:

public class Caller {
    final Callee _callee;

    public Caller(Callee callee){
        _callee = callee;
    }
    public static void main(String[] args) {
        new Caller(new Callee("xu", "shijie")).test(5, 100);
    }

    public void test(int a, int b){
        int t = a;
        int p = b;
        int r = t+p-_callee.calculate(a, b);
        int m = t-p;
        System.out.println(t);
    }
}
public class Callee {

    final String _a;
    final String _b;
    public Callee(String a, String b){
        _a = a;
        _b = b;
    }

    public int calculate(int t, int p){
        int tmp = _a.length()+_b.length();
        tmp+=t+p;
        return tmp;
    }
}

基于ASM 5.0版本,我的代码是:

//MainInliner.java
public class MainInliner extends ClassLoader{

    public byte[] generator(String caller, String callee) throws ClassNotFoundException{
        String resource = callee.replace('.', '/') + ".class";
        InputStream is =  getResourceAsStream(resource);
        byte[] buffer;
        // adapts the class on the fly
        try {
            resource = caller.replace('.', '/')+".class";
            is = getResourceAsStream(resource);
            ClassReader cr = new ClassReader(is);
            ClassWriter cw = new ClassWriter(0);

            ClassVisitor visitor = new BCMerge(Opcodes.ASM5, cw, callee);
            cr.accept(visitor, 0);

            buffer= cw.toByteArray();

        } catch (Exception e) {
            throw new ClassNotFoundException(caller, e);
        }

        // optional: stores the adapted class on disk
        try {
            FileOutputStream fos = new FileOutputStream("/tmp/data.class");
            fos.write(buffer);
            fos.close();
        } catch (IOException e) {}
        return buffer;
     }


      @Override
      protected synchronized Class<?> loadClass(final String name,
                final boolean resolve) throws ClassNotFoundException {
            if (name.startsWith("java.")) {
                System.err.println("Adapt: loading class '" + name
                        + "' without on the fly adaptation");
                return super.loadClass(name, resolve);
            } else {
                System.err.println("Adapt: loading class '" + name
                        + "' with on the fly adaptation");
            }
            String caller = "code.sxu.asm.example.Caller";
            String callee = "code.sxu.asm.example.Callee";
            byte[] b = generator(caller, callee);
            // returns the adapted class
            return defineClass(caller, b, 0, b.length);
        }

        public static void main(final String args[]) throws Exception {
            // loads the application class (in args[0]) with an Adapt class loader
            ClassLoader loader = new MainInliner();
            Class<?> c = loader.loadClass(args[0]);
            Method m = c.getMethod("main", new Class<?>[] { String[].class });

        }
}


class BCMerge extends ClassVisitor{

    String _callee;
    String _caller;

    public BCMerge(int api, ClassVisitor cv, String callee) {
        super(api, cv);
        // TODO Auto-generated constructor stub
        _callee = callee.replace('.', '/');
    }

    public void visit(int version, int access, String name, String signature,
            String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this._caller = name;
    } 

    public MethodVisitor visitMethod(int access, String name, String desc,
            String signature, String[] exceptions) {

        if(!name.equals("test")){
            return super.visitMethod(access, name, desc, signature, exceptions);
        }
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);

        ClassReader cr = null;
        try {
            cr = new ClassReader(this.getClass().getClassLoader().getResourceAsStream(_callee.replace('.', '/')+".class"));
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

         ClassNode classNode = new ClassNode();
         cr.accept(classNode, 0);

         MethodNode inlinedMethod = null;
         for(MethodNode node: classNode.methods){
            if(node.name.equals("calculate")){
                inlinedMethod = node;
                break;
            }
        }

        return new MethodCallInliner(access, desc, mv, inlinedMethod, _callee, _caller  );
    }
}

//MethodCallInliner.java
public class MethodCallInliner extends LocalVariablesSorter {

    private final String oldClass;
    private final String newClass;
    private final MethodNode mn;  //Method Visitor wrappers the mv.
    private List blocks = new ArrayList();
    private boolean inlining;

    public MethodCallInliner(int access, String desc, MethodVisitor mv, MethodNode mn, 
            String oldClass, String newClass){
        super(Opcodes.ASM5, access, desc, mv);
        this.oldClass = oldClass;
        this.newClass =  newClass;
        this.mn = mn;
        inlining = false;
    }

    public void visitMethodInsn(int opcode, String owner, String name,
            String desc, boolean itf) {

        System.out.println("opcode:" + opcode + " owner:" + owner + " name:"
                + name + " desc:" + desc);
        if (!canBeInlined(owner, name, desc)) {
            mv.visitMethodInsn(opcode, owner, name, desc, itf);
            return;
        }
        //if it is INVOKEVIRTUAL  ../Callee::calculate(II), then..
        Remapper remapper = new SimpleRemapper(oldClass, newClass);
        Label end = new Label();
        inlining = true;
        mn.instructions.resetLabels();
        mn.accept(new InliningAdapter(this,opcode == Opcodes.INVOKESTATIC ? Opcodes.ACC_STATIC : 0, desc,remapper, end));
        inlining = false;
        super.visitLabel(end);
    }

    private boolean canBeInlined(String owner, String name, String decs){
            if(name.equals("calculate") && owner.equals("code/sxu/asm/example/Callee")){
            return true;
        }
        return false;
    }
}

//InliningAdapter.java
public class InliningAdapter extends RemappingMethodAdapter {

    private final LocalVariablesSorter lvs;
    private final Label end;

    public InliningAdapter(LocalVariablesSorter mv,
            int acc, String desc,Remapper remapper, Label end) {
        super(acc, desc, mv, remapper);
        this.lvs = mv;
        this.end = end;

//      int offset = (acc & Opcodes.ACC_STATIC)!=0 ?0 : 1;
//      Type[] args = Type.getArgumentTypes(desc);
//      for (int i = args.length-1; i >= 0; i--) {
//          super.visitVarInsn(args[i].getOpcode(
//                  Opcodes.ISTORE), i + offset);
//      }
//      if(offset>0) {
//          super.visitVarInsn(Opcodes.ASTORE, 0);
//      }
    }

    public void visitInsn(int opcode) {
        if(opcode==Opcodes.RETURN || opcode == Opcodes.IRETURN) {
            super.visitJumpInsn(Opcodes.GOTO, end);
        } else {
            super.visitInsn(opcode);
        }
    }

    public void visitMaxs(int stack, int locals) {
        System.out.println("visit maxs: "+stack+"  "+locals);
    }

    protected int newLocalMapping(Type type) {
        return lvs.newLocal(type);
    }
}

在代码中, InliningAdapterMethodCallInliner 都扩展 LocalVariablesSorter ,它重新编号局部变量 . 并且内联在Caller :: test :: invokevirtual(Callee :: calculate)调用站点引用了Callee :: calculate()的应对体 .

Caller :: test(),Callee :: calculate和generated :: test的字节码是:

//Caller::test()
 public void test(int, int);
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=7, args_size=3
         0: iload_1       
         1: istore_3      
         2: iload_2       
         3: istore        4
         5: iload_3       
         6: iload         4
         8: iadd          
         9: aload_0       
        10: getfield      #13                 // Field _callee:Lcode/sxu/asm/example/Callee;
        13: iload_1       
        14: iload_2       
        15: invokevirtual #39                 // Method code/sxu/asm/example/Callee.calculate:(II)I   //Copy calculate's body here
        18: isub          
        19: istore        5
        21: iload_3       
        22: iload         4
        24: isub          
        25: istore        6
        27: getstatic     #43                 // Field java/lang/System.out:Ljava/io/PrintStream;
        30: iload_3       
        31: invokevirtual #49                 // Method java/io/PrintStream.println:(I)V
        34: getstatic     #43                 // Field java/lang/System.out:Ljava/io/PrintStream;
        37: ldc           #55                 // String 1..........
        39: invokevirtual #57                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        42: return        

//Callee::calculate()

 public int calculate(int, int);
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=3
         0: aload_0       
         1: getfield      #14                 // Field _a:Ljava/lang/String;
         4: invokevirtual #26                 // Method java/lang/String.length:()I
         7: aload_0       
         8: getfield      #16                 // Field _b:Ljava/lang/String;
        11: invokevirtual #26                 // Method java/lang/String.length:()I
        14: iadd          
        15: istore_3      
        16: iload_3       
        17: iload_1       
        18: iload_2       
        19: iadd          
        20: iadd          
        21: istore_3      
        22: iload_3       
        23: ireturn       

 //data.class
  public void test(int, int);
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=8, args_size=3
         0: iload_1       
         1: istore_3      
         2: iload_2       
         3: istore        4
         5: iload_3       
         6: iload         4
         8: iadd          
         9: aload_0       
        10: getfield      #14                 // Field _callee:Lcode/sxu/asm/example/Callee;
        13: iload_1       
        14: iload_2       
        15: aload_0       
        16: getfield      #40                 // Field _a:Ljava/lang/String;
        19: invokevirtual #46                 // Method java/lang/String.length:()I
        22: aload_0       
        23: getfield      #49                 // Field _b:Ljava/lang/String;
        26: invokevirtual #46                 // Method java/lang/String.length:()I
        29: iadd          
        30: istore        6
        32: iload         6
        34: iload_1       
        35: iload_2       
        36: iadd          
        37: iadd          
        38: istore        6
        40: iload         6
        42: goto          45
        45: isub          
        46: istore        6
        48: iload_3       
        49: iload         4
        51: isub          
        52: istore        7
        54: getstatic     #59                 // Field java/lang/System.out:Ljava/io/PrintStream;
        57: iload_3       
        58: invokevirtual #65                 // Method java/io/PrintStream.println:(I)V
        61: getstatic     #59                 // Field java/lang/System.out:Ljava/io/PrintStream;
        64: ldc           #67                 // String 1..........
        66: invokevirtual #70                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        69: return

data.class上的javap结果显示Callee :: calculate的主体已插入到正确的位置(Caller :: test line :: 15) . 但是,有两个主要问题:

  • invokevirtual之前的前三个堆栈对象

Callee :: calculate(第15行)9:aload_0
10:getfield#14 // Field _callee:Lcode / sxu / asm / example / Callee; 13:iload_1
14:iload_2

内联后不应该在堆栈上 .

  • 复制正文中的变量号0(Callee :: calculate())应映射到正确的数字

  • 变量号不正确 . 首先,在data.class(从第15行到第42行)中复制的Callee :: calculate体的变量号开始5(而不是0) . 其次,Callee :: calculate()之后的变量数应该由规则重新编号:a)如果它在(0,4)之间则不改变; b)如果它与被复制的Callee体中的数字冲突则重新编号: :计算()

我去检查基类 LocalVariablesSorter 的实现 . 问题似乎在于它的构建:

protected LocalVariablesSorter(final int api, final int access,
            final String desc, final MethodVisitor mv) {
        super(api, mv);
        Type[] args = Type.getArgumentTypes(desc);
        nextLocal = (Opcodes.ACC_STATIC & access) == 0 ? 1 : 0;
        for (int i = 0; i < args.length; i++) {
            nextLocal += args[i].getSize();
        }
        firstLocal = nextLocal; 
    }
    private int[] mapping = new int[40];

对我来说,似乎 firstLocal 总是从1 args.length()开始(在这种情况下它是3) . 此类还提供 private int remap(final int var, final Type type) ,它创建新的局部变量并保持映射数组中的映射(从现有变量号到新索引) .


My problem is that how to use LocalVariablesSorter and inline the bytecode method (Callee::calculate) correctly. Any suggestion for efficient inline is welcome.

对于内联之前的堆栈上的参数(在第15行之前) . 我的想法是将它们存储为新创建的局部变量,这些变量将由Callee :: calculate的复制主体引用 . 例如,在10之后添加: astore 5 :getfield#14 // Field _callee:Lcode / sxu / asm / example / Callee;

并在 LocalVariablesSorter 中添加 mapping[0]=5+1

但主要问题是不允许用户更新 LocalVariablesSorter::mapping (从旧变量号到映射数组中的新变量),因为 mapping 数组是私有的,并且其更新的唯一位置在方法中:

private int remap(final int var, final Type type) {
        if (var + type.getSize() <= firstLocal) { 
        //Variable index will never be modified if it is less than firstLocal. 0 < 3. Nothing i can do for ALOAD 0. 
            return var;
        }
        int key = 2 * var + type.getSize() - 1;
        int size = mapping.length;
        if (key >= size) {
                 .....
        }
        int value = mapping[key];
        if (value == 0) {
            value = newLocalMapping(type);
            setLocalType(value, type);
            mapping[key] = value + 1;
        } else {
            value--;
        }
        if (value != var) {
            changed = true;
        }
        return value;
    }

Update1 :取消注释InliningAdapter构造函数后的data.class:

0: iload_1       
     1: istore_3      
     2: iload_2       
     3: istore        4
     5: iload_3       
     6: iload         4
     8: iadd          
     9: aload_0       
    10: getfield      #14                 // Field _callee:Lcode/sxu/asm/example/Callee;
    13: iload_1       
    14: iload_2       
    15: istore_2        //should be istore 5
    16: istore_1        //should be istore 6
    17: astore_0        //should be astore 7
    18: aload_0        
    19: getfield      #40                 // Field _a:Ljava/lang/String;
    22: invokevirtual #46                 // Method java/lang/String.length:()I
    25: aload_0       
    26: getfield      #49                 // Field _b:Ljava/lang/String;
    29: invokevirtual #46                 // Method java/lang/String.length:()I
    32: iadd          
    33: istore        6
    35: iload         6
    37: iload_1       
    38: iload_2       
    39: iadd          
    40: iadd          
    41: istore        6
    43: iload         6
    45: goto          48
    48: isub          
    49: istore        6
    51: iload_3       
    52: iload         4
    54: isub          
    55: istore        7
    57: getstatic     #59

新存储的三个变量(15,16,17)应编号为5,6,7,而不是2,1,0,并且内联代码中 *store/*load 中的映射应该像

0 => 7
       1 => 6 
       2 => 5
       3 => 8
       4 => 9  ...

这些映射应该在数组中: LocalVariablesSorter::mapping ,它由方法 LocalVariablesSorter::remap() 更新 . 但是,我似乎无法将它们插入到 mapping 数组中 .

应该进行两种重新映射:

  • 重新映射内联代码内部(从第18行到第45行),变量索引从5开始 . 最大索引是k

  • 内联代码后重新映射(从第46行到结尾),如果原始索引大于5,则应重新映射任何变量索引(新索引从k 1开始)

1 回答

  • 2

    正如@Holger已经建议的那样,首先取消注释 InliningAdapter 中的行 .

    • 要解决您列出的主要问题: LocalVariablesSorter (由 InliningAdapter 扩展)认为参数已经存储在固定位置的局部变量中 - 这确实是输入方法时的正常情况 . 因此它根本不映射那些(参见 LocalVariablesSorter.remap() 中的第一行 - 在构造函数中计算 firstLocal ) . 但是在这种情况下,我们在堆栈上获取参数,并需要手动分配局部变量 . 解决方法是告诉 LocalVariablesSorter 没有参数已经存储在局部变量中(make firstLocal = 0 ) . 然后,它会将对它们的任何引用视为新变量,并为它们分配新的局部变量 . 我们可以通过欺骗 LocalVariablesSorter 来认为没有参数并且该方法是静态的(即使它确实不是) . 所以我们改变了 InliningAdapter 的第一行
    super(acc, desc, mv, remapper);
    

    super(acc | Opcodes.ACC_STATIC, "()V", mv, remapper);
    

    现在变量0,1,2,...重新映射到5,6,7,...或类似的(并不重要它们是什么, LocalVariablesSorter Caller (即 MethodCallInliner 实例)负责分配他们) .

    • 还有另一个问题,即通过 InliningAdapter 扩展 RemappingMethodAdaptorCallee 类映射到 Caller - 但是我想你想保留 _a_b 变量存储在 Callee 实例中 .

    • 如果我的猜测是正确的,那么你实际上不应该将引用从 Callee 重新映射到 Caller . 您可以改为 InliningAdapter 扩展 LocalVariableSorter 并删除重映射器 .

    • 如果我的猜测不正确,那么我猜你可能需要将 Callee 的变量嵌入到 Caller ,在这种情况下你应该保留你拥有的 RemappingMethodAdaptor .

    • 在调试内联代码时,自从代码内联到 Caller 类以来, Callee 中的亚麻布将没有意义 . 所以来自 Callee 的所有亚麻布应该用 Caller 中发生内联呼叫的线路的行号替换 . 遗憾的是,在Java中,您无法逐行指定不同的源代码文件(例如,您可以在C中) . 所以你会使用类似的东西覆盖 InliningAdapter 中的 visitLineNumber()inlinedLine 将被传递给 InliningAdapter 的构造函数):

    @Override
    public void visitLineNumber(int line, Label start) {
        super.visitLineNumber(inlinedLine, start);
    }
    

    ..或者也许完全跳过超级电话,我不是百分百肯定的 .

相关问题