由于我们会在后续的文章介绍class验证环节,其中在校验方法时需要使用到StackMap.那么什么是StackMap呢?
从Java 6开始,JVM规范有一个更新文档,JSR 202,里面提到一种新的字节码校验算法,“类型检查”;在此之前是用“类型推导”的算法。为了支持新算法,Class文件从版本50开始添加了一个新的属性表,叫做StackMapTable,里面记录的是一个方法中操作数栈与局部变量区的类型在一些特定位置的状态。
在版本号大于或等于50.0的Class文件中,如果方法的Code属性中没有附带StackMapTable属性,那就意味着它带有一个隐式的StackMap属性。这个StackMap属性的作用等同于number_of_entries值为0的StackMapTable属性。一个方法的Code属性最多只能有一个StackMapTable属性,否则将抛出ClassFormatError异常。
StackMapTable的格式如下:
StackMapTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_entries;
stack_map_frame entries[number_of_entries];
}
其中各项的含义如下:
attribute_name_index
attribute_name_index项的值必须是对常量池的有效索引,常量池在该索引的项处必须是CONSTANT_Utf8_info(§4.4.7)结构,表示“StackMapTable”字符串。
attribute_length
attribute_length项的值表示当前属性的长度,不包括开始的6个字节。
number_of_entries
number_of_entries项的值给出了entries表中的成员数量。Entries表的每个成员是都是一个stack_map_frame结构的项。
entries[]
entries表给出了当前方法所需的stack_map_frame结构。
每个stack_map_frame结构都使用一个特定的字节偏移量来表示类型状态。每个帧类型(Frame Type)都显式或隐式地标明一个offset_delta(增量偏移量)值,用于计算每个帧在运行时的实际字节码偏移量。使用时帧的字节偏移量计算方法为:前一帧的字节码偏移量(Bytecode Offset)加上offset_delta的值再加1,如果前一个帧是方法的初始帧(Initial Frame),那这时候字节码偏移量就是offset_delta。
**只要保证栈映射帧有正确的存储顺序,在类型检查时我们就可以使用增量偏移量而不是实际的字节码偏移量。此外,由于堆每一个帧都使用了offset_delta+1的计算方式,我们可以确保偏移量不会重复。 在Code属性的code[]数组项中,如果偏移量i的位置是某条指令的起点,同时这个Code属性包含有StackMapTable属性,它的entries项中也有一个适用于地址偏移量i的stack_map_frame结构,那我们就说这条指令拥有一个与之相对应的栈映射帧。 **
stack_map_frame结构的第一个字节作为类型标记(Tag),第一个字节后会跟随0或多个字节用于说明更多信息,这些信息因类型标记的不同而变化。
一个栈映射帧可以包含若干种帧类型(Frame Types):
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;
}
same_frame {
u1 frame_type = SAME;/* 0-63 */
}
same_locals_1_stack_item_frame {
u1 frame_type = SAME_LOCALS_1_STACK_ITEM;/* 64-127 */
verification_type_info stack[1];
}
same_locals_1_stack_item_frame_extended {
u1 frame_type = SAME_LOCALS_1_STACK_ITEM_EXTENDED;/* 247 */
u2 offset_delta;
verification_type_info stack[1];
}
chop_frame {
u1 frame_type=CHOP; /* 248-250 */
u2 offset_delta;
}
same_frame_extended {
u1 frame_type = SAME_FRAME_EXTENDED;/* 251*/
u2 offset_delta;
}
append_frame {
u1 frame_type = APPEND; /* 252-254 */
u2 offset_delta;
verification_type_info locals[frame_type -251];
}
full_frame {
u1 frame_type = FULL_FRAME; /* 255 */
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];
}
这里需要提的是,在kvm内部使用的是栈类型的数据结构为,关于这点,是本人看kvm源码总结出来.关于相关文档并没有找到:
kvm_frame {
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];
}
verification_type_info结构的第一个字节tag作为类型标记,之后跟随0至多个字节表示由tag类型所决定的信息。每个verification_type_info结构可以描述1个至2个存储单元的验证类型信息。
union verification_type_info {
Top_variable_info;
Integer_variable_info;
Float_variable_info;
Long_variable_info;
Double_variable_info;
Null_variable_info;
UninitializedThis_variable_info;
Object_variable_info;
Uninitialized_variable_info;
}
// Top_variable_info类型说明这个局部变量拥有验证类型top(ᴛ)。
Top_variable_info {
u1 tag = ITEM_Top; /* 0 */
}
// Integer_variable_info类型说明这个局部变量包含验证类型int
Integer_variable_info {
u1 tag = ITEM_Integer; /* 1 */
}
//Float_variable_info类型说明局部变量包含验证类型float
Float_variable_info {
u1 tag = ITEM_Float; /* 2 */
}
// Long_variable_info结构在局部变量表或操作数栈中占用2个存储单元。
Long_variable_info {
u1 tag = ITEM_Long; /* 4 */
}
// Double_variable_info结构在局部变量表或操作数栈中占用2个存储单元。
Double_variable_info {
u1 tag = ITEM_Double; /* 3 */
}
// Null_variable_info类型说明存储单元包含验证类型null。
Null_variable_info {
u1 tag = ITEM_Null; /* 5 */
}
// UninitializedThis_variable_info类型说明存储单元包含验证类型uninitializedThis。
UninitializedThis_variable_info {
u1 tag = ITEM_UninitializedThis; /* 6 */
}
// Object_variable_info类型说明存储单元包含某个Class的实例。由常量池在cpool_index给出的索引处的CONSTANT_CLASS_Info(§4.4.1)结构表示。
Object_variable_info {
u1 tag = ITEM_Object; /* 7 */
u2 cpool_index;
}
// Uninitialized_variable_info说明存储单元包含验证类型uninitialized(offset)。offset项给出了一个偏移量,表示在包含此StackMapTable属性的Code属性中,new指令创建的对象所存储的位置。
Uninitialized_variable_info {
u1 tag = ITEM_Uninitialized /* 8 */
u2 offset;
}
关于栈帧的形成,是在每个基本块开始的位置生成。
一个“基本块”(basic block)就是一个方法中的代码最长的直线型一段段代码序列。“直线型”也就是说代码序列中除了末尾之外不能有控制流(跳转)指令。
一个基本块的开头可以是方法的开头,也可以是某条跳转指令的跳转目标;
一个基本块的结尾可以是方法的末尾,也可以是某条跳转指令(Java中就是goto、if*系列等;invoke*系列的方法调用指令不算在跳转指令中).
如果一个方法代码如下:
public class Foo {
public void foo() {
// basic block 1 start
int i = 0;
int j = 0;
if (i > 0) { // basic block 1 end
// basic block 2 start
int k = 0;
// basic block 2 end
}
// basic block 3 start
int l = 0;
// basic block 3 end
}
}
那么可以看到就有3个基本块。不过在Java Class文件里StackMapTable关心的是类型检查,为了进一步压缩这个表的大小,使用的基本块定义比通常的定义要更宽松些:一个条件跳转的直落分支与条件跳转前的代码算在同一个基本块内。于是前面的例子就变成:
public class Foo {
public void foo() {
// basic block 1 start
int i = 0;
int j = 0;
if (i > 0) {
int k = 0;
// basic block 1 end
}
// basic block 2 start
int l = 0;
// basic block 2 end
}
}
这个方法就会有一个StackMapTable属性表,其中有一个stack frame map记录(本来应该是两个,但第一个是隐式的,不记录在属性表里)。
public void foo();
Code:
Stack=1, Locals=4, Args_size=1
/* basic block 1 start */
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_1
5: ifle 10
8: iconst_0
9: istore_3
/* basic block 1 end */
/* basic block 2 start */
10: iconst_0 /* stack frame map 1 refers to here */
11: istore_3
12: return
/* basic block 2 end */
LocalVariableTable:
Start Length Slot Name Signature
10 0 3 k I
0 13 0 this LFoo;
2 11 1 i I
4 9 2 j I
12 1 3 l I
StackMapTable: number_of_entries = 1
frame_type = 253 /* append */
offset_delta = 10
locals = [ int, int ]
隐式的第一个基本块的stack frame map是从方法签名计算出来的。这个例子foo是个实例方法,没有显示声明的参数,所以参数个数是1,也就是隐藏参数this。那么在字节码偏移量0的位置上,操作数栈为空,
局部变量区:[ Foo ]
下一个基本块从字节码偏移量10开始。此处变量k已经过了作用域,所以局部变量区的有效内容应该是:
局部变量区:[ Foo, int, int ]
这就比前一个基本块开头处的状态多了2个局部变量,类型分别是[ int, int ],所以就有了上面对应的StackMapTable项了,253 - 251 = 2。
上文这是介绍了一下理论,在kvm内部是通过定义如下数据结构来实现的
struct pointerListStruct {
long length;
cellOrPointer data[1];
};
其在分配内存的时候,其最终分配的大小为: 2 * number_of_entries.
其最终的内存布局如下:
kvm读取StackMap属性,是在loadCodeAttribute方法中调用loadStackMaps实现的.代码如下:
unsigned short codeAttrNameIndex = loadShort(ClassFileH); // 读取name index
unsigned int codeAttrLength = loadCell(ClassFileH); // 读取AttrLength
if (!strcmp(codeAttrName, "StackMap")) {
unsigned int stackMapAttrSize;
if (!needStackMap) {
raiseExceptionWithMessage(ClassFormatError,
KVM_MSG_DUPLICATE_STACKMAP_ATTRIBUTE);
}
needStackMap = FALSE;
// 真正读取
stackMapAttrSize = loadStackMaps(ClassFileH, thisMethodH);
if (stackMapAttrSize != codeAttrLength) {
raiseExceptionWithMessage(ClassFormatError,
KVM_MSG_BAD_ATTRIBUTE_SIZE);
}
}
loadStackMaps的代码如下:
static long
loadStackMaps(FILEPOINTER_HANDLE ClassFileH, METHOD_HANDLE thisMethodH)
{
long bytesRead;
INSTANCE_CLASS CurrentClass = unhand(thisMethodH)->ofClass;
START_TEMPORARY_ROOTS
// 1. 读取 number_of_entries
unsigned short nStackMaps = loadShort(ClassFileH);
// 2. 分配内存
DECLARE_TEMPORARY_ROOT(POINTERLIST, stackMaps,
(POINTERLIST)callocObject(SIZEOF_POINTERLIST(2*nStackMaps),
GCT_POINTERLIST));
METHOD thisMethod = unhand(thisMethodH); /* Very volatile */
// 为stackMap,分配内存,大小为(maxStack+frameSize + 2)* 4,这里保存verification_type_info
unsigned tempSize = (thisMethod->u.java.maxStack + thisMethod->frameSize + 2);
DECLARE_TEMPORARY_ROOT(unsigned short*, stackMap,
(unsigned short *)mallocBytes(sizeof(unsigned short) * tempSize));
unsigned short stackMapIndex;
stackMaps->length = nStackMaps;
unhand(thisMethodH)->u.java.stackMaps.verifierMap = stackMaps;
bytesRead = 2;
/*
这里读取的数据结构如下:
* kvm_frame {
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];
}
*
*/
for (stackMapIndex = 0; stackMapIndex < nStackMaps; stackMapIndex++) {
unsigned short i, index;
thisMethod = unhand(thisMethodH);
/* Read in the offset */
stackMaps->data[stackMapIndex + nStackMaps].cell =
loadShort(ClassFileH); // 读取offset_delta 在stackMaps的后面保存
bytesRead += 2;
/*
这里通过循环读取如下数据:
* u2 number_of_locals;
verification_type_info locals[number_of_locals];
u2 number_of_stack_items;
verification_type_info stack[number_of_stack_items];
*/
for (index = 0, i = 0 ; i < 2; i++) {
unsigned short j;
unsigned short size = loadShort(ClassFileH); // number_of_locals,number_of_stack_items
unsigned short size_delta = 0;
unsigned short size_index = index++;
unsigned short maxSize = (i == 0 ? thisMethod->frameSize
: thisMethod->u.java.maxStack);
bytesRead += 2;
// 读取verification_type_info
for (j = 0; j < size; j++) {
unsigned char stackType = loadByte(ClassFileH);
bytesRead += 1;
/* We are reading the j-th element of the stack map.
* This corresponds to the value in the j + size_delta'th
* local register or stack location
*
* j + size_delta 对应的是local registe 或者是stack
*/
if (j + size_delta >= maxSize) {
raiseExceptionWithMessage(ClassFormatError,
KVM_MSG_BAD_STACKMAP);
} else if (stackType == ITEM_NewObject) {
/*
* Uninitialized_variable_info说明存储单元包含验证类型uninitialized(offset)。
* offset项给出了一个偏移量,表示在包含此StackMapTable属性的Code属性中,new指令创建的对象所存储的位置
* Uninitialized_variable_info { u1 tag = ITEM_Uninitialized ;
* u2 offset; }
*/
unsigned short instr = loadShort(ClassFileH); // offset
bytesRead += 2;
if (instr >= thisMethod->u.java.codeLength) {
raiseExceptionWithMessage(ClassFormatError,
KVM_MSG_BAD_NEWOBJECT);
}
stackMap[index++] = ENCODE_NEWOBJECT(instr);
} else if (stackType < ITEM_Object) {
/*
* 数据类型
*/
stackMap[index++] = stackType;
if (stackType == ITEM_Long || stackType == ITEM_Double){
if (j + size_delta + 1 >= maxSize) {
raiseExceptionWithMessage(ClassFormatError,
KVM_MSG_BAD_STACKMAP);
}
stackMap[index++] = (stackType == ITEM_Long)
? ITEM_Long_2
: ITEM_Double_2;
size_delta++;
}
} else if (stackType == ITEM_Object) {
/*
* Object_variable_info类型说明存储单元包含某个Class的实例。由常量池在cpool_index给出的索引处的CONSTANT_CLASS_Info(§4.4.1)结构表示
*/
unsigned short classIndex = loadShort(ClassFileH);
CONSTANTPOOL ConstantPool = CurrentClass->constPool;
CLASS clazz;
bytesRead += 2;
verifyConstantPoolEntry(CurrentClass,
classIndex, CONSTANT_Class);
clazz = CP_ENTRY(classIndex).clazz;
stackMap[index++] = clazz->key;
} else {
raiseExceptionWithMessage(ClassFormatError,
KVM_MSG_BAD_STACKMAP);
}
}
stackMap[size_index] = size + size_delta; // 这里实际保存的是verification_type_info所占用的大小
}
/*
* 检查是否有重复
* */
for (i = 0; ; i++) {
if (i == stackMapIndex) {
/* 此时没有之前的stackMap是重复的,则需要分配内存,进行数据复制 */
char *temp = mallocBytes(index * sizeof(unsigned short));
memcpy(temp, stackMap, index * sizeof(short));
stackMaps->data[stackMapIndex].cellp = (cell*)temp;
break;
} else {
unsigned short *tempMap =
(unsigned short *)stackMaps->data[i].cellp;
/* 这里获取的是Loacl的长度*/
unsigned short tempLen = tempMap[0];
/* 这里获取的是Loacl的长度 */
unsigned short mapLen = stackMap[0];
/* tempMap[tempLen + 1] 获得是stack的长度,长度 数据数据数据数据数据数据 长度 数据数据数据数据数据数据, tempMap[0] +tempMap[tempLen + 1]
* 只是计算了两部分数据的长度,但是没有计算2个记录数据长度的大小.由于tempLen 的类型为unsigned short,因此+2正好可以加上2个记录数据长度的大小 */
tempLen += tempMap[tempLen + 1] + 2;
mapLen += stackMap[mapLen + 1] + 2;
/* 如果长度相同的话且内容相同的话,则直接引用即可.节省内存*/
if (mapLen == tempLen &&
memcmp(stackMap, tempMap,
mapLen * sizeof(unsigned short)) == 0) {
/* We have found a duplicate */
stackMaps->data[stackMapIndex].cellp = (cell*)tempMap;
break;
}
}
}
}
END_TEMPORARY_ROOTS
return bytesRead;
}
关于该方法,注释的内容较详细,就不展开了
本文的内容参考如下书籍,文章:
JVM 规范, (java se 7, java se 8),涉及 StackMap的章节
能介绍一下StackMapTable属性的运作原理吗?