类型检查与栈映射桢

类型检查与栈映射桢

VerifyError:需要一个栈映射桢

在使用ASM操作字节码的过程中,如果在加载使用ASM生成的类或者改写的类时,遇到如下错误:

java.lang.VerifyError: Expecting a stackmap frame at branch target xx

说明该类的某个方法在字节码验证阶段的类型检查出错。

如果在编写代码的过程中需要自己手动计算出每个栈映射桢,这将会是一个痛苦的过程,好在ASM提供自动计算栈映射桢的能力。那么,如何让ASM帮我们自动计算栈映射桢,生成StackMapTable属性呢?

第一步:在创建ClassWriter时,构造方法的flag参数传COMPUTE_FRAMES

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

第二步:在使用MethodVisitor操作方法的字节码时,根据需要生成的java代码判断该方法是否需要有个显式的StackMapTable属性,如果需要,可在调用visitMaxs方法之前调用MethodVisitor的visitFrame方法让ASM自动生成,参数随意传。

MethodVisitor mv = cw.visitMethod();
mv.visitFrame(0,0,null,0,null);

StackMapTable属性与栈映射桢

从Java 6开始,JSR 202规范中新增一种字节码校验算法,即类型检查。为支持类型检查,class文件结构从版本50开始添加StackMapTable属性。StackMapTable属性描述一个方法中一段字节码指令区间的操作数栈与局部变量表中元素的类型。

◇Basic Block

一段字节码指令区间指的是一个基本块:Basic Block。就是一个方法中最长直线型的一段代码,即除末尾之外不能有跳转指令,方法调用指令不算在跳转指令中。每个方法都有一个隐式的Basic Block,隐式的Basic Block从方法签名计算出来[1]

◆一个Basic Block的开头可以是方法的开头,也可以是某条跳转指令的跳转目标;

◆一个Basic Block的结尾可以是方法的末尾,也可以是某条跳转指令;

例如:

9: ifle          21

ifle指令在条件成立的情况下会跳转到偏移量为21的字节码指令,则偏移量为21的字节码指令是一个Basic Block的开头。ifle指令的偏移量为9,则偏移量为9的字节码指令是前一个Basic Block的尾部。

◇StackMapTable属性

StackMapTable属性位于Code属性的属性表中,一个Code属性的属性表最多可以有一个StackMapTable属性。StackMapTable属性是可变长度的属性,Java虚拟机在验证阶段验证字节码时使用,用于类型检查。

可在hotspot虚拟机源码的ClassVerifier类的verify_stackmap_table方法查看验证逻辑,verify_stackmap_table方法被调用的链路如下:

InstanceKlass::verify_code
   > Verifier::verify
        >  ClassVerifier::verify_class
            > ClassVerifier::verify_method
                > ClassVerifier::verify_stackmap_table

StackMapTable属性是JDK 1.6加入的属性。如果方法的Code属性没有附带StackMapTable属性,意味着它带有一个隐式的栈映射属性,等同于Code属性有一个number_of_entries为0的StackMapTable属性。

根据《Java虚拟机规范》规定,StackMapTable属性的结构为:

StackMapTable_attribute{
    u2 attribute_name_index;
    u4 attribute_length;
    u2 number_of_entries;
    stack_map_frame entries[number_of_entries];
}

◆attribute_name_index:常量池中表示字符串“StackMapTable”的CONSTANT_Utf8_info常量的索引;

◆attribute_length:属性entries项的字节长度;

◆number_of_entries:entries数组的大小;

◆entries:表中每一项元素都是一个栈映射桢,即每个元素都是一个stack_map_frame结构;

栈映射桢使用名为stack_map_frame的联合体表示:

union stack_map_frame{
    same_frame;
    same_locals_1_stack_item_frame;
    same_locals_1_stack_item_frame_extended;
    chop_frame;
    same_frame_extended;
    append_frame;
    full_frame;
}

一个stack_map_frame只能表示联合体中的某项,每一项可称为一个栈映射桢类型,桢类型有一个通用的结构:

frame{
    u1 frame_type;
    u1 body[];
}

frame_type项使用一个字节标志这是哪一种栈映射桢类型,frame_type的不同值对应的栈映射桢类型如表8.1所示。

表8.1 栈映射桢类型与frame_type值映射表

每个栈映射桢都显式或隐式指明一个偏移量offset_delta,通过该偏移量可计算出当前栈映射桢对应的“Basic Block”的开头,对应Code属性中code数组的索引。如果是第一个显示的栈映射桢,则offset_delta就是某条字节码指令在code数组中的索引,该索引处的字节码指令就是当前栈映射桢对应的“Basic Block”的开头。否则可通过前一个栈映射桢计算出的字节码指令索引加上当前栈映射桢的offset_delta的值再加上1得出当前栈映射桢的“Basic Block”的开头字节码指令索引。

same_locals_1_stack_item_frame、same_locals_1_stack_item_frame_extended、full_frame这几个栈映射桢类型都有一个stack[]表;append_frame、full_frame这几个栈映射桢类型都有一个locals[]表,locals[]表与stack[]表的每一项都是一个verification_type_info。

locals[]表并非局部变量表,该表用于与局部变量表做映射;stack[]表也并非指操作数栈,而是用于与操作数栈做映射。表示方法中某条字节码指令执行之前,局部变量表中所存储的元素的类型是否与locals[]表中表示的核查类型相匹配,以及此时的操作数栈所存储的元素的类型是否与stack[]表中表示的核查类型相匹配。

locals[]表与stackp[]表的每一项都是一个verification_type_info,存储类型核查信息,如Object_veriable_info类型表示该存储单元的核查类型是某个类。

verification_type_info也是一个联合体:

verification_type_info:{
    Top_veriable_info;
    Integer_veriable_info;
    Float_veriable_info;
    Null_veriable_info;
    UninitializedThis_veriable_info;
    Object_veriable_info;
    Uninitialized_veriable_info;
    Long _veriable_info;
    Double_veriable_info;
}

◆same_frame

如果一个stack_map_frame的第一个字节的值在0~63范围内,则可确定该栈映射桢的类型为same_frame。same_frame结构如下:

same_frame{
      u1 frame_type;
  }

如果栈映射桢的类型是same_frame,则表示当前栈映射桢拥有和前一个栈映射桢完全相同的locals[]表,且stack[]表为空表,当前栈映射桢的offset_delta的值等于frame_type项的值。

◆same_locals_1_stack_item_frame

如果一个stack_map_frame的第一个字节的值在64~127范围内,则可确定该栈映射桢的类型为same_locals_1_stack_item_frame。same_locals_1_stack_item_frame结构如下:

same_locals_1_stack_item_frame{
    u1 frame_type;
    verification_type_info stack[1];
}

如果栈映射桢类型是same_locals_1_stack_item_frame,则表示当前栈映射桢与前一个栈映射桢拥有完成相同的locals[]表,对应的stackp[]表的成员个数是1,当前栈映射桢的offset_delta值为frame_type项的值减64。

◆same_locals_1_stack_item_frame_extended

如果一个stack_map_frame的第一个字节的值为247,则可确定该栈映射桢的类型为same_locals_1_stack_item_frame_extended。

same_locals_1_stack_item_frame{
    u1 frame_type;
    verification_type_info stack[1];
}

same_locals_1_stack_item_frame_extended类型与same_locals_1_stack_item_frame类唯一不同的地方是,same_locals_1_stack_item_frame_extended类型显示指出当前栈映射桢的offset_delta的值。

◆chop_frame

如果一个stack_map_frame的第一个字节的值在248~250范围内,则可确定该栈映射桢的类型为chop_frame。chop_frame如下:

chop_frame{
    u1 frame_type;
    u2 offset_delta;
}

如果栈映射桢类型是chop_frame,表示当前栈映射桢的locals[]表比前一个栈映射桢的locals[]表少N个元素,N的值为251减frame_type的值。该栈映射桢类型显示给出offset_delta的值。

◆same_frame_extended

如果一个stack_map_frame的第一个字节的值在252~254范围内,则可确定该栈映射桢的类型为append_frame。append_frame结构如下:

append_frame{
    u1 frame_type;
    u2 offset_delta;
    verification_type_info locals[frame_type-251];
}

如果栈映射桢的类型是append_frame,表示当前栈映射桢的stack[]表为空表,且locals[]表比前一个栈映射桢的locals[]表多N项,N的值为frame_type的值减去251。locals[]表中的每一项表示相比上一个栈映射桢附加的局部变量的核查类型。该栈映射桢类型显示给出offset_delta的值。

◆full_frame

如果一个stack_map_frame的第一个字节的值为255,则可确定该栈映射桢的类型为full_frame。full_frame结构如下:

    u1 frame_type;
    u2 offset_delta;
    u2 number_of_locals;
    verification_type_info locals[number_of_locals];
    u2 number_of_stack_items;
    verification_type_info stack[number_of_stack_items];
}

如果栈映射桢的类型为full_frame,则locals[]表中的每一项表示对应的局部变量的核查类型,stack[]表中的每一项表示对应操作数栈元素的核查类型。

◇举例分析,案例一:

public void showUserName(int userId) {
    UserService userService = new UserService();
    if (userId > 0) {
        String userName = userService.getUserName(userId);
    } else {
        String userName = "admin";
    }
}

使用javap命令输出该方法的字节码如下:

public void showUserName(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: new           #2                  // class com/wujiuye/asmbytecode/book/advanced/UserService
         3: dup
         4: invokespecial #3                  // Method com/wujiuye/asmbytecode/book/advanced/UserService."<init>":()V
         7: astore_2
         8: iload_1
         9: ifle          21
        12: aload_2
        13: iload_1
        14: invokevirtual #4                  // Method com/wujiuye/asmbytecode/book/advanced/UserService.getUserName:(I)Ljava/lang/String;
        17: astore_3
        18: goto          24
        21: ldc           #5                  // String admin
        23: astore_3
        24: return

      StackMapTable: number_of_entries = 2
        frame_type = 252 /* append */
          offset_delta = 21
          locals = [ class com/wujiuye/asmbytecode/book/advanced/UserService ]
        frame_type = 2 /* same */

该方法的隐式栈映射桢对应的Basic Block的开头是索引为0的字节码指令,stack[]表为空,local[]表为:[I]。

该方法的StackMapTable属性有两个显示的栈映射桢:

第一个栈映射桢的类型为append_frame,offset_delta为21,由于该栈映射桢是第一个显示的栈映射,所以根据offset_delta的值计算出来Basic Block的开头是索引为21的字节码指令。此栈映射桢用于核查,在索引为21的字节码指令执行之前,操作数栈为空,且局部变量表比上一桢(也就是索引为21的字节码指令之前的桢,这里是隐式桢)多出一个局部变量,局部变量的类型为UserService,核查类型为Object_veriable_info。

第二个栈映射桢的类型为same_frame,此栈映射桢类型不显示给出offset_delta。offset_delta的值等于frame_type项的值,即offset_delta的值为2。算出该栈映射桢的Basic Block的开头是:

前一桢Basic Block的开头(21)+当前栈映射桢的offset_delta的值(2)+1

算出Basic Block的开头是索引为24的字节码指令,对应的字节码指令为return。

该栈映射桢的类型为same_frame,说明当前栈映射桢拥有和前一个栈映射桢完全相同的locals[]表,且stack[]表为空表,即与第一个栈映射桢的locals[]表相同,stack[]表为空表(操作数栈为空)。

◇举例分析,案例二:

public void showUserName(int... userIds) {
    // basic block 1 start
    UserService userService = new UserService();
    for (int i = 0;// basic block 1 end
         i < userIds.length; i++) {
        // basic block 2 start
        int userId = userIds[i];
        String userName = userService.getUserName(userId);
        // basic block 2 end
    }
}

使用javap命令输出该方法的字节码如下:

public void showUserName(int...);
    descriptor: ([I)V
    flags: ACC_PUBLIC, ACC_VARARGS
    Code:
      stack=2, locals=6, args_size=2
         0: new           #2                  // class com/wujiuye/asmbytecode/book/advanced/UserService
         3: dup
         4: invokespecial #3                  // Method com/wujiuye/asmbytecode/book/advanced/UserService."<init>":()V
         7: astore_2
         8: iconst_0
         9: istore_3
        10: iload_3
        11: aload_1
        12: arraylength
        13: if_icmpge     35
        16: aload_1
        17: iload_3
        18: iaload
        19: istore        4
        21: aload_2
        22: iload         4
        24: invokevirtual #4                  // Method com/wujiuye/asmbytecode/book/advanced/UserService.getUserName:(I)Ljava/lang/String;
        27: astore        5
        29: iinc          3, 1
        32: goto          10
        35: return

      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 10
          locals = [ class com/wujiuye/asmbytecode/book/advanced/UserService, int ]
        frame_type = 250 /* chop */
          offset_delta = 24

该方法的隐式栈映射桢的Basic Block的开头是索引为0的字节码指令,stack[]表为空,local[]表为:[I]。

该方法的StackMapTable属性有两个显示的栈映射桢:

第一个栈映射桢的类型为append_frame,offset_delta为10。由于该栈映射桢是第一个显示的栈映射,所以根据offset_delta的值计算出Basic Block的开头是索引为10的字节码指令。此栈映射桢用于核查,在索引为10的字节码指令执行之前,操作数栈为空,且局部变量表比上一桢(也就是索引为10的字节码指令之前的桢,这里是隐式桢)多出一个局部变量,局部变量的类型为UserService,核查类型为Object_veriable_info。

第二个栈映射桢的类型为chop_frame,offset_delta为24,算出该栈映射桢的Basic Block的开头是:

前一桢Basic Block的开头(10)+当前栈映射桢的offset_delta的值(24)+1

算出Basic Block的开头是索引为35的字节码指令,对应的字节码指令为return。

此类型表示当前栈映射桢的locals[]表比前一个栈映射桢的locals[]表少1个局部变量(251-frame_type),除了少的那个局部变量之外,当前桢的locals[]表的每一项元素都与前一桢的locals[]表的对应项元素相同。


注释:

[1] 参考ITeye论坛的一篇帖子:https://www.iteye.com/topic/779102