【问题标题】:Inlining tryCatchBlock results in Current frame's stack size doesn't match stackmap exception内联 tryCatchBlock 导致当前帧的堆栈大小与堆栈映射异常不匹配
【发布时间】:2015-05-05 17:52:29
【问题描述】:

我正在使用 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的三个指令弹出两个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 ]

}

任何人都可以提供建议吗?我被这个问题困扰了三天。这里的堆栈图处理应该有问题,但我不知道如何调整这个错误。

为了您的方便,我还贴出了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,在classpath中加入ASM lib后可以直接运行MainInliner类。

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

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

LocalVariableTable 更新

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

  • 避免声明任何形式变量,让 ASM 推断一切。

要禁用声明形式变量,visitLocalVariableInliningAdapterMethodCallInliner 中都被覆盖但为空实现,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 处失败,并显示相同的消息。

【问题讨论】:

  • 谢谢@Holger。 visitLocalVariable 可能会被间接调用,因为在进行内联时,两个主适配器从 LocalVariablesSorter 扩展而来。我对变量#7 感到困惑,因为这个变量(#3)是在原始代码中创建的。我的代码中的内联只对这个局部变量重新编号。我还将我的代码推送到了 Github(请参阅我的帖子底部)。谢谢
  • 您是否尝试过要求 ASM 重新计算整个方法的堆栈图?
  • 感谢@Antimony。我已经做到了。 ASM 通过使用 ClassWriter.COMPUTE_FRAMES|ClassWriter.COMPUTE_MAXS 选项初始化 ClassWriter 并在方法体末尾调用 visitMax 来重新计算堆栈图。
  • @Holger,感谢您的清晰解释。我尝试了以下选项:“避免任何形式变量声明”并将调用者和被调用者的LocalVariableTable 合并在一起。验证仍然失败并显示相同的消息(请参阅我的第一篇文章中的“LocalVariableTable 更新”部分)。还有什么建议吗?
  • 好吧,忘记局部变量,我被误导了。合并已过时,但不是问题。这只是操作数堆栈深度不匹配。我把它变成了一个答案……

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


【解决方案1】:

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

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

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

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

【讨论】:

  • 感谢@Holger 并指出其他可能性。你是对的。我现在正在尝试的是,在内联 Callee 之前,弹出堆栈中的所有堆叠变量,然后将它们的变量 id 以及类型按顺序推送到单独的堆栈中。然后,当被调用者退出时,分离的堆栈用于重建堆栈。这里我使用 AnalyzerAdapter 来计算 Callee::calculate 开头的堆栈深度。这应该可以,但性能可能不好。
猜你喜欢
  • 1970-01-01
  • 2017-12-11
  • 1970-01-01
  • 2012-06-03
  • 1970-01-01
  • 2020-08-24
  • 2018-10-31
  • 2021-04-07
  • 1970-01-01
相关资源
最近更新 更多