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

Inlining tryCatchBlock results in Current frame's stack size doesn't match stackmap exception

我正在使用 ASM 将包含 try-catch 块的 Callee::calcualte(int,int)int 的主体内联到 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的body,标签9到标签12的3条指令弹出两个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处理应该有问题,但我不知道如何调整这个错误。

为了您的方便,我还post 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,classMainInliner可以直接在class路径添加ASM lib后运行。

项目中的主要程序是MethodCallInliner::visitMethodInsn(..),其中新建了一个InliningAdapter,用来访问Callee::calculate()体。

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

LocalVariableTable更新:

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

要禁用声明正式变量,visitLocalVariable 被覆盖但在 InliningAdapterMethodCallInliner 中都是空实现,LocalVariableTable 在生成的代码中消失但验证仍在失败并出现相同的错误。我也试过

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

但是结果和空覆盖一样visitLocalVariable

完整生成的字节码是:

  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 处验证仍然失败,并显示相同的消息。

在调用calculate之前,栈上有一个int值,调用后会用到。正常完成的方法调用只会消耗适当的参数值,而不会影响所有其他操作数堆栈值,而不管调用的方法内部发生了什么。如果方法不是 void,return 值将被压入堆栈。

当您内联方法的代码时,情况会发生变化。然后,除了使用参数和压入一个 return 值之外,代码可能会对操作数堆栈产生影响。在您的情况下,有一个异常处理程序将恢复正常执行。但是,正如 中所讨论的,异常处理程序从一个仅包含一个值的操作数堆栈开始,即遇到的异常。在遇到异常之前压入堆栈的所有值都将被刷新。将方法的代码内联到调用者之后,这也会影响调用者的操作数堆栈。

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

没有简单的解决办法。您不能强制异常处理程序保留该值,因此您必须重写代码以不依赖于保留推送的值,这使您最初通过插入指令进行内联的想法无效。你必须知道,甚至相反的情况也是可能的:当一个方法遇到 return 指令时,有多少额外的未使用值悬在堆栈上并不重要,因为堆栈帧将在 [= 时被销毁24=]ing 给呼叫者。因此,天真地内联代码可能会在堆栈上留下额外的值,这会在您对方法有分支或方法有多个 return 指令时导致错误。