内联tryCatchBlock导致当前帧的堆栈大小与堆栈映射异常

时间:2015-05-05 17:52:29

标签: java bytecode java-bytecode-asm bytecode-manipulation

我使用ASM将Callee::calcualte(int,int)int的内联体(包含try-catch块)内联到Caller::test方法。生成的字节码似乎没问题,但验证因异常而失败:

Exception in thread "main" java.lang.VerifyError: Instruction type does not match stack map
Exception Details:
  Location:
    CallerI.test(II)V @50: iload
  Reason:
    Current frame's stack size doesn't match stackmap.
  Current Frame:
    bci: @50
    flags: { }
    locals: { 'CallerI', integer, integer, integer, integer, 'code/sxu/asm/example/Callee', integer, 'java/lang/ReflectiveOperationException' }
    stack: { }
  Stackmap Frame:
    bci: @50
    flags: { }
    locals: { 'CallerI', integer, integer, integer, integer, 'code/sxu/asm/example/Callee', integer, 'java/lang/Object' }
    stack: { integer }
  Bytecode:
    0000000: 1b1c 602a b400 0e1b 1c3e 3604 3a05 0336
    0000010: 06b8 002e 1230 1232 b200 3812 30b8 003e
    0000020: b600 443a 07a7 000d 3a07 b200 4a12 4cb6
    0000030: 0052 1506 a700 0364 3605 b200 5515 05b6
    0000040: 0058 b200 5512 5ab6 0052 b1            
  Exception Handler Table:
    bci [17, 37] => handler: 40
    bci [17, 37] => handler: 40
  Stackmap Table:
    full_frame(@40,{Object[#2],Integer,Integer,Integer,Integer,Object[#21],Integer},{Object[#101]})
    full_frame(@50,{Object[#2],Integer,Integer,Integer,Integer,Object[#21],Integer,Object[#4]},{Integer})
    full_frame(@55,{Object[#2],Integer,Integer,Integer,Integer,Object[#21],Integer,Object[#4]},{Integer,Integer})

生成的代码中从标签14到标签52的字节码指令来自Callee :: calculate的主体,标签9到12的三个指令弹出两个int参数和接收器(Callee)。

 //The generated bytecode method.
 public void test(int, int);
    flags: ACC_PUBLIC
    Code:
      stack=6, locals=8, args_size=3
         0: iload_1       
         1: iload_2       
         2: iadd          
         3: aload_0       
         4: getfield      #14                 // Field _callee:Lcode/sxu/asm/example/Callee;
         7: iload_1       
         8: iload_2       
         9: istore_3      
        10: istore        4
        12: astore        5
        14: iconst_0      
        15: istore        6
        17: invokestatic  #46                 // Method java/lang/invoke/MethodHandles.publicLookup:()Ljava/lang/invoke/MethodHandles$Lookup;
        20: ldc           #48                 // class java/lang/String
        22: ldc           #50                 // String say
        24: getstatic     #56                 // Field java/lang/Void.TYPE:Ljava/lang/Class;
        27: ldc           #48                 // class java/lang/String
        29: invokestatic  #62                 // Method java/lang/invoke/MethodType.methodType:(Ljava/lang/Class;Ljava/lang/Class;)Ljava/lang/invoke/MethodType;
        32: invokevirtual #68                 // Method java/lang/invoke/MethodHandles$Lookup.findVirtual:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
        35: astore        7
        37: goto          50
        40: astore        7
        42: getstatic     #74                 // Field java/lang/System.err:Ljava/io/PrintStream;
        45: ldc           #76                 // String I find exception in the catch
        47: invokevirtual #82                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        50: iload         6
        52: goto          55
        55: isub          
        56: istore        5
        58: getstatic     #85                 // Field java/lang/System.out:Ljava/io/PrintStream;
        61: iload         5
        63: invokevirtual #88                 // Method java/io/PrintStream.println:(I)V
        66: getstatic     #85                 // Field java/lang/System.out:Ljava/io/PrintStream;
        69: ldc           #90                 // String 1..........
        71: invokevirtual #82                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        74: return        
      Exception table:
         from    to  target type
            17    37    40   Class java/lang/NoSuchMethodException
            17    37    40   Class java/lang/IllegalAccessException
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
              14      41     0  this   Lcode/sxu/asm/example/Callee;
              14      41     1     t   I
              14      41     2     p   I
              17      38     3   tmp   I
              42       8     4     e   Ljava/lang/ReflectiveOperationException;
               0      75     0  this   LCallerI;
               0      75     1     a   I
               0      75     2     b   I
              58      17     5     r   I
      LineNumberTable:
        line 16: 0
        line 18: 14
        line 26: 17
        line 27: 37
        line 29: 42
        line 31: 50
        line 18: 58
        line 19: 66
        line 20: 74
      StackMapTable: number_of_entries = 3
           frame_type = 255 /* full_frame */
          offset_delta = 40
          locals = [ class CallerI, int, int, int, int, class code/sxu/asm/example/Callee, int ]
          stack = [ class java/lang/ReflectiveOperationException ]
           frame_type = 255 /* full_frame */
          offset_delta = 9
          locals = [ class CallerI, int, int, int, int, class code/sxu/asm/example/Callee, int, class java/lang/Object ]
          stack = [ int ]
           frame_type = 255 /* full_frame */
          offset_delta = 4
          locals = [ class CallerI, int, int, int, int, class code/sxu/asm/example/Callee, int, class java/lang/Object ]
          stack = [ int, int ]

}

有人可以提供建议吗?我已经被这个问题困住了三天。这里的stackmap处理应该有问题,但我不知道如何调整这个错误。

为了您的方便,我还发布了Caller和Callee的原始方法:

public class Callee {
    public final String _a;
    public final String _b;
    public Callee(String a, String b){
        _a = a;
        _b = b;
    }
  ....
    public int calculate(int t, int p){
        int tmp=0;
        try {
            MethodHandle handle  = MethodHandles.publicLookup().findVirtual(String.class, "say", MethodType.methodType(void.class, String.class));
        } catch (NoSuchMethodException | IllegalAccessException e) {
            // TODO Auto-generated catch block
            System.err.println("I find exception in the catch");
        }
        return tmp;
    }
}

public class Caller {
    final Callee _callee;

     public Caller(Callee callee){
    _callee = callee;
}
  ...   
public void test(int a, int b){
    int r = a+b-_callee.calculate(a, b);

    System.out.println(r);
    System.out.println("1..........");
}
}

更新

原来的Callee :: calculate的字节码:

 public int calculate(int, int);
    flags: ACC_PUBLIC
    Code:
      stack=5, locals=5, args_size=3
         0: iconst_0      
         1: istore_3      
         2: invokestatic  #26                 // Method java/lang/invoke/MethodHandles.publicLookup:()Ljava/lang/invoke/MethodHandles$Lookup;
         5: ldc           #32                 // class java/lang/String
         7: ldc           #34                 // String say
         9: getstatic     #36                 // Field java/lang/Void.TYPE:Ljava/lang/Class;
        12: ldc           #32                 // class java/lang/String
        14: invokestatic  #42                 // Method java/lang/invoke/MethodType.methodType:(Ljava/lang/Class;Ljava/lang/Class;)Ljava/lang/invoke/MethodType;
        17: invokevirtual #48                 // Method java/lang/invoke/MethodHandles$Lookup.findVirtual:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
        20: astore        4
        22: goto          35
        25: astore        4
        27: getstatic     #54                 // Field java/lang/System.err:Ljava/io/PrintStream;
        30: ldc           #60                 // String I find exception in the catch
        32: invokevirtual #62                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        35: iload_3       
        36: ireturn       
      Exception table:
         from    to  target type
             2    22    25   Class java/lang/NoSuchMethodException
             2    22    25   Class java/lang/IllegalAccessException
      LineNumberTable:
        line 18: 0
        line 26: 2
        line 27: 22
        line 29: 27
        line 31: 35
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      37     0  this   Lcode/sxu/asm/example/Callee;
               0      37     1     t   I
               0      37     2     p   I
               2      35     3   tmp   I
              27       8     4     e   Ljava/lang/ReflectiveOperationException;
      StackMapTable: number_of_entries = 2
           frame_type = 255 /* full_frame */
          offset_delta = 25
          locals = [ class code/sxu/asm/example/Callee, int, int, int ]
          stack = [ class java/lang/ReflectiveOperationException ]
           frame_type = 9 /* same */

我的代码也被推送到Github Repository,并且在将ASM lib添加到类路径后可以直接运行类MainInliner

项目中的主要过程是MethodCallInliner::visitMethodInsn(..),其中创建了新的InliningAdapter并用于访问Callee::calculate()正文。

=========================================

更新LocalVariableTable

根据@ Holger的解释和一些选项:

  • 避免声明任何正式变量,让ASM推断所有内容。

要禁用声明形式变量,visitLocalVariable会覆盖InliningAdapterMethodCallInliner中的空实现,LocalVariableTable在生成的代码中消失,但验证仍然失败,同样的错误。我也试过

 ClassReader.accept(, ClassReader.EXPAND_FRAME|ClassReader.SKIP_DEBUG)

但结果与空覆盖visitLocalVariable

相同
  • 将Callee的localvariabletable合并到生成的表格中。

完整生成的字节码是:

  public void test(int, int);
    flags: ACC_PUBLIC
    Code:
      stack=6, locals=8, args_size=3
         0: iload_1       
         1: iload_2       
         2: iadd          
         3: aload_0       
         4: getfield      #14                 // Field _callee:Lcode/sxu/asm/example/Callee;
         7: iload_1       
         8: iload_2       
         9: istore_3      
        10: istore        4
        12: astore        5
        14: iconst_0      
        15: istore        6
        17: invokestatic  #46                 // Method java/lang/invoke/MethodHandles.publicLookup:()Ljava/lang/invoke/MethodHandles$Lookup;
        20: ldc           #48                 // class java/lang/String
        22: ldc           #50                 // String say
        24: getstatic     #56                 // Field java/lang/Void.TYPE:Ljava/lang/Class;
        27: ldc           #48                 // class java/lang/String
        29: invokestatic  #62                 // Method java/lang/invoke/MethodType.methodType:(Ljava/lang/Class;Ljava/lang/Class;)Ljava/lang/invoke/MethodType;
        32: invokevirtual #68                 // Method java/lang/invoke/MethodHandles$Lookup.findVirtual:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
        35: astore        7
        37: goto          50
        40: astore        7
        42: getstatic     #74                 // Field java/lang/System.err:Ljava/io/PrintStream;
        45: ldc           #76                 // String I find exception in the catch
        47: invokevirtual #82                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        50: iload         6
        52: goto          55
        55: isub          
        56: istore_3      
        57: getstatic     #85                 // Field java/lang/System.out:Ljava/io/PrintStream;
        60: iload_3       
        61: invokevirtual #88                 // Method java/io/PrintStream.println:(I)V
        64: getstatic     #85                 // Field java/lang/System.out:Ljava/io/PrintStream;
        67: ldc           #90                 // String 1..........
        69: invokevirtual #82                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        72: return        
      Exception table:
         from    to  target type
            17    37    40   Class java/lang/NoSuchMethodException
            17    37    40   Class java/lang/IllegalAccessException
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
              14      41     5  this   Lcode/sxu/asm/example/Callee;
              14      41     4     t   I
              14      41     3     p   I
              17      38     6   tmp   I
              42       8     7     e   Ljava/lang/ReflectiveOperationException;
               0      73     0  this   LCallerI;
               0      73     1     a   I
               0      73     2     b   I
              57      16     3     r   I
      LineNumberTable:
        line 20: 0
        ..
        line 24: 72
      StackMapTable: number_of_entries = 3
           frame_type = 255 /* full_frame */
          offset_delta = 40
          locals = [ class CallerI, int, int, int, int, class code/sxu/asm/example/Callee, int ]
          stack = [ class java/lang/ReflectiveOperationException ]
           frame_type = 255 /* full_frame */
          offset_delta = 9
          locals = [ class CallerI, int, int, int, int, class code/sxu/asm/example/Callee, int, class java/lang/Object ]
          stack = [ int ]
           frame_type = 255 /* full_frame */
          offset_delta = 4
          locals = [ class CallerI, int, int, int, int, class code/sxu/asm/example/Callee, int, class java/lang/Object ]
          stack = [ int, int ]

}

和原始的LocalVariableTables是:

Callee: LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      37     0  this   Lcode/sxu/asm/example/Callee;
               0      37     1     t   I
               0      37     2     p   I
               2      35     3   tmp   I
              27       8     4     e   Ljava/lang/ReflectiveOperationException;
Caller: LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      30     0  this   Lcode/sxu/asm/example/Caller;
               0      30     1     a   I
               0      30     2     b   I
              14      16     3     r   I

合并似乎没问题(我认为如果这些名称位于不同的区域,例如在不同区域中有两个this符号,则不必避免变量名称冲突)。但验证仍然在@ 50 :: iload失败,并显示相同的消息。

1 个答案:

答案 0 :(得分:1)

在调用calculate之前,堆栈上有一个int值,将在调用后使用。正常完成的方法调用只会消耗适当的参数值,并保持所有其他操作数堆栈值不受影响,无论调用方法内部发生了什么。如果方法不是void,则返回值将被推送到堆栈之后。

当您内联方法的代码时,事情会发生变化。然后,代码可能会对操作数堆栈产生影响,超出消耗参数并推送一个返回值。在您的情况下,有一个异常处理程序将恢复正常执行。但是,正如this answer中所讨论的,异常处理程序以只包含一个值的操作数堆栈开始,即遇到的异常。在遇到异常之前推送到堆栈的所有值都将被刷新。在将方法的代码内联到调用者之后,这也会影响调用者的操作数堆栈。

因此,在您的情况下,两个代码路径在内联代码的末尾附近合并,一个用于正常完成的情况,其中将保留堆栈上的int值以及异常处理程序的路径,值已被删除的地方。这种不匹配会导致VerifyError

没有简单的解决方案。您不能强制异常处理程序保留该值,因此您必须重写代码以不依赖于要保留的推送值,这通过仅插入非工作指令来呈现内联的原始想法。你必须要知道,即使相反也是可能的:当一个方法遇到一个返回指令时,堆栈上悬挂的有多少额外的未消耗值并不重要,因为当返回给调用者时,堆栈帧将被销毁。因此,天真内联的代码可能会在堆栈上留下额外的值,当您在方法上有分支或者方法有多个返回指令时,这会导致错误。