参考书目:《Java虚拟机规范》/第四章:class文件解析
不同语言编写的源文件(e.g. java, groovy, kotlin, scala)编译后生成的.class字节码文件都能在JVM上运行。
不同语言能在JVM上运行的本质:
小端&大端
大端模式:低地址存放数据的高位,高地址存放数据的低位。
小端模式:低地址存放数据的低位,高地址存放数据的高位。
字节码在JVM(计算机)内存中是小端存储的。在写JVM时需要用大端模式读取。证明:
void JavaVirtualMachine::parse_single_class(JavaClassContent *class_content) {
printf(“%X \n”, *(int*)class_content->get_content_ptr());
printf(“%X\n”, htonl(*(int*)class_content->get_content_ptr())); //htonl: 转换为大端模式
…
}
输出:BEBAFECA CAFEBABE
原因:小端;大端
描述符
根据 Class文件结构中常量池中11种数据类型的结构总表, CONSTANT_Methodref_Info、CONSTANT_InterfaceMethodref_Info和CONSTANT_NameAndType_Info中都含有CONSTANT_NameAndType_info。而CONSTANT_NameAndType_Info中又包含名称和描述符。
数据类型的描述符
void类型 V e.g. ()v 类型为v的方法
引用类型 e.g. String的描述符: Ljava/lang/String;
数组类型 e.g. byte数组的描述符: [B
e.g. String数组的描述符: [Ljava/lang/String;
方法的描述符
参数数据类型的描述符)返回值的描述符
如果有多个参数,描述符用逗号分隔
e.g. javap –verbose输出中查看到main方法的描述符
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;]V //main方法的描述符
e.g. 某个方法的描述符
([[Ljava/lang/String;. I, Ljava/AA/B;)Ljava/lang/String;
原形:String XXX(String[][], int, BB)
实验:手动解析字节码
Example java code:
public class Simple {
static int a = 10;
public static void main(String[] args) {}
}
使用hexdump -C查看字节码文件,再打开jclasslib对比确认手动解析结果是否正确。
class文件的组成部分
uX:占X个字节。
!:字节长度不确定,需要动态计算。
魔数 (u4): 值为ca fe ba be。每个Java class文件都以0xCAFEBABE开头。用于判断一个文件是否为合格class文件。如果不是则不执行。
次版本号 (u2) & 主版本号 (u2):JDK的版本对应的major、minor号。
e.g. 00000000 ca fe ba be 00 00 00 34
次版本号为0,主版本号为16进制34转为10进制52,所以表示jdk版本为1.8。jclasslib查看genral information可证。
常量池大小 (u2):常量池个数。
*注意:常量池最小index不是0而是1。真实常量池个数=字节码文件中常量池个数-1。
e.g. 00 19
00 19->25,真实常量池个数为25-1=24。 Jclasslib中查看Constant Pool可证。
常量池:每个数据结构tag都占前1个字节。根据tag值对应数据类型的结构解析其中存储的信息。
CONSTANT Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
…
常量池中的11种数据结构类型:
e.g. 解析常量池
常量池中第1个元素:
00000000 ca fe ba be 00 00 00 34 00 19 0a 00 04 00 15
tag:0a==10 –> CONSTANT_Methodref
class_index: 00 04 == 4 –> 04位置还没解析出来,这里只能放符号引用,后面才能转为直接引用
name_and_type:
Jclasslib中查看Constant Pool中第1个元素可证。
常量池中第2个元素:
00000000 ca fe ba be 00 00 00 34 00 19 0a 00 04 00 15 09
00000010 00 03 00 16
tag: 09 ==9 -> CONSTANT_Fieldref_info
class_index:
name_and_type:
Jclasslib中查看Constant Pool中第2个元素可证。查看静态常量池也可证明符号引用了第3个元素,因为#2 field的值为#3。
常量池中第3个元素:
00000010 00 03 00 16 07 00 17 07 00 18 01 00 01 6101 00
tag: 07 ==7 -> CONSTANT_Class_Info
name_index:
常量池中第4个元素:
00000010 00 03 00 16 07 00 17 07 00 18
tag: 07 ==7 -> CONSTANT_Class_Info
…
常量池中第5个元素:
00000010 00 03 00 16 07 00 17 07 00 18 01 00 01 61
tag: 01 ==1 -> CONSTANT_Utf8_info,说明是String类型
length: 00 01==1 –> 长度为1,所以byte只往后取1位
bytes:
…
access flags (u2):
通过或运算解析access flag值的含义.e.g. 0x11表示public final
e.g. 解析access flag
00000120 61 6e 67 2f 4f 62 6a 65 63 74 00 21
0x0021 -> public
this_class (u2): 当前类
e.g. 解析this class
00000120 61 6e 67 2f 4f 62 6a 65 63 74 00 21 00 03
查看静态常量池,当前类是常量池中第3个元素
super_class (u2):
e.g. 解析super class
00000120 61 6e 67 2f 4f 62 6a 65 63 74 00 21 00 03 00 04
查看静态常量池,super class是常量池中第4个元素
interfaces_count (u2): 接口数量
e.g. 解析Interfaces_count
00000130 00 00
接口数量是0。
*一个类最多可以实现多少个接口?
- 65535。//因为接口数量占2个字节=2*8=16位,二进制16位数最大为1111 1111 1111 1111=十进制2^16-1。
interfaces[] (!): 如果接口数量是0,则该区域在字节码文件中不会出现。
fields_count[] (u2): fields(类的属性)的数量
e.g. 解析fields数量
00000130 00 00 00 01
fields_info (!): field在常量池中的存储方式。
field_info {
u2 access_flags;
u2 name_index; //指向常量池
u2 descriptor_index;
u2 attributes_count; //属性数量,
attribute_info attributes[attributes_count]; //属性内容
}
attributes指的是field的属性(e.g. 加了final就会有ConstantValue属性)。如果attributes_count为0,attributes区域在字节码文件中就不会出现。
e.g. 解析一个field
先前fields_count的解析结果说明该类中只有一个field。
00000130 00 00 00 01 00 08 00 05 00 06 00 00
第1个属性:
access_flags:
//jclasslib查看对应field可证0008为static:
name_index:
descriptor_index:
attribute_count:
attributes:
methods_count (u2):方法数量
e.g.解析方法数量
00000130 00 00 00 01 00 08 00 05 00 06 00 00 00 03
-> 有3个方法:静态方法clint、默认构造方法init、main。
methods_info (!):方法
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count; //属性、Code、Exception
attribute_info attributes[attributes_count];
}
如果attributes_count为0,attributes区域在字节码文件中就不会出现。
Code_attribute {
u2 attribute_name_index; //即为method_info中解析出的attributes值
u4 attribute_length;
u2 max_stack; //操作数栈大小
u2 max_locals; //局部变量表大小
u4 code_length;
u1 code[code_length]; //方法体/字节码指令
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table [exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count]; //Code属性的属性
}
exceptions分别在method_info和Code_attribute中都有记录,可验证得知throws异常和try catch异常在字节码中有不同的表示。
Code_attribute中有max_stack和max_locals,说明操作数栈和局部变量表的大小在编译时就已经确定。
Jclasslib打开/Methods/方法名/Code,在specific info查看指令内容。
Code的属性包括LineNumberTable(代码行号,存储Java代码所在行数)和LocalVariableTable(局部变量表,存储参数和局部变量),Jclasslib打开/Methods/方法名/Code可查。
e.g.解析第1个方法
00000130 00 00 00 01 00 08 00 05 00 06 00 00 00 03 00 01
00000140 00 07 00 08 00 01 00 09
access_flag:
name_index:
descriptor_index:
attr_count: 00 01
attributes:
name_index==7 -> 常量池中第7个元素为CONSTANT_Utf8_info, String为<init>,说明是构造方法。
Attributes==9 -> 第1个属性为Code,按照Code_attribute结构继续解析:
e.g. 解析init方法的code属性
00000140 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
00000150 00 00 00 05 2a b7 00 01 b1 00 00 00 02
attribute_name_index:
attribute_length:
max_stack: 00 01 -- 操作数栈大小
max_locals: 00 01 -- 局部表大小
code_length:
code: 2a b7 00 01 b1 -- 方法体(字节码指令)
exception_table_length:
exception_table:
attributes_count:
方法体:
0x2a == 42 -> aload_0
…
0xb1 == 177 -> return
e.g. Jclasslib->Methods/<init>/Code点开方法体中的指令科与字节码对应
根据attributes_count确定code有2个attributes后,继续解析code的attributes:
00000150 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
00000160 00 00 06 00 01 00 00 00 07
Code的第1个属性:
attr_name_index:
Code第1个属性的attr_name_index==10, jclasslib查看常量池第10个元素为CONSTANT_Utf8_info, 其String值为LineNumberTable。所以第1个属性是代码行号表。
确定当前code属性为LineNumberTable后,根据LineNumberTable的结构继续解析:
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}
attr_name_index:
attribute_length:
line_number_table_length:
start_pc:
line_number: 00 07 ]
Code的第2个属性:
00000160 00 00 06 00 01 00 00 00 07 00 0b 00 00 00 0c 00
00000170 01 00 00 00 05 00 0c 00 0d 00 00
attr_name_index:
Code第2个属性的attr_name_index==11, jclasslib查看常量池第11个元素为CONSTANT_Utf8_info, 其String值为LocalVariableTable。所以第2个属性是局部变量表。
确定当前code属性为LocalVariableTable后,根据LocalVariableTable的结构继续解析:
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{ u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable _table[local_variable _table_length];
}
attr_name_index:
attribute_length:
local_variable_table_length: 00 01
[ start_pc: 00 00
length: 00 05
name_index:
descriptor_index:
index: 00 00 ]
attributes_count (u2)
attributes[] (!):
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}