ART 虚拟机-Class文件逐字节分析
学习虚拟机的第一步,当然就是分析Class文件啦,直接进入主题,Class文件是经过Java编辑器编译后得到的Java字节码文件,可以看做是Java虚拟机的可执行文件。
1、Class 文件格式
顺序 | 类型 | 名称 | 数量 | 说明 |
---|---|---|---|---|
1 | u4 | magic | 1 | 魔数,固定为0XCAFEBABE |
2 | u2 | minor_version | 1 | 次版本号 |
3 | u2 | major_version | 1 | 主版本号 |
4 | u2 | consant_pool_count | 1 | 常量池元素个数 |
5 | cp_info | constant_pool | constant_pool_count-1 | 常量池中的元素,索引从1开始 |
6 | u2 | access_flags | 1 | 该类的访问权限(public、private等信息) |
7 | u2 | this_class | 1 | 指向常量池的索引,此类的类名 |
8 | u2 | super_class | 1 | 指向常量池的索引,父类的类名 |
9 | u2 | interfaces_count | 1 | 指向常量池的索引,该类实现了都多少个接口 |
10 | u2 | interfaces | interfaces_count | 指向常量池的索引,该类实现的接口类名 |
11 | u2 | fields_count | 1 | 成员变量的数量 |
12 | field_info | fields | fields_count | 成员变量的信息 |
13 | u2 | methods_count | 1 | 函数的数量 |
14 | method_info | methods | methods_count | 函数的信息 |
15 | u2 | attribute_count | 1 | 该类包含的属性的个数 |
16 | attribute_info | attribute | attribute_count | 该类包含的属性 |
在Java虚拟机规范里,一个Class文件的结构如上表所示,一共有16项内容,其中u4和u2表示为4个字节长度的无符号整数和2个字节长度的无符号整数。
以一个简单的示例,来挨着解析一下Class文件的结构,测试代码如下,一个类文件,里面只有一个变量和一个方法。
package com.rainbow.pet;
public class ClassFile {
public int q = 1;
public int add(int n) {
int m = 2;
return m + n;
}
}
使用javac指令,生成对应的Class文件,以及使用javap指令反编译这个Class文件,便于查看对照,生成的结果如下。
public class com.rainbow.pet.ClassFile
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #3 // com/rainbow/pet/ClassFile
super_class: #4 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/rainbow/pet/ClassFile.q:I
#3 = Class #17 // com/rainbow/pet/ClassFile
#4 = Class #18 // java/lang/Object
#5 = Utf8 q
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 add
#12 = Utf8 (I)I
#13 = Utf8 SourceFile
#14 = Utf8 ClassFile.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // q:I
#17 = Utf8 com/rainbow/pet/ClassFile
#18 = Utf8 java/lang/Object
{
public int q;
descriptor: I
flags: (0x0001) ACC_PUBLIC
public com.rainbow.pet.ClassFile();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field q:I
9: return
LineNumberTable:
line 3: 0
line 5: 4
public int add(int);
descriptor: (I)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=2
0: iconst_2
1: istore_2
2: iload_2
3: iload_1
4: iadd
5: ireturn
LineNumberTable:
line 8: 0
line 9: 2
}
SourceFile: "ClassFile.java"
1.1、魔数
按照Class文件的结构,文件的开头是4个字节长度的魔数,每个Class文件开头都是CAFEBABE,没有什么好讲的。
1.2、次版本号
魔数之后,是两个字节的次版本号 0 。
1.3、主版本号
次版本号之后是两个字节的主版本号0x37,转成十进制为 55 。 主版本号和次版本号,我们在反编译的Class文件之后,也可以看到minor version: 0 ,major version: 55 。
public class com.rainbow.pet.ClassFile
minor version: 0
major version: 55
1.4、常量池
常量池由第四项constant_pool_count(常量池中元素个数)和constant_pool(常量池)组成。常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,其中constant_pool_count是一个u2类型的数据,代表了接着的constant_pool中的元素个数,constant_pool是一个数组(一段连续的空间),数组里面存放的内容为cp_info(常量信息),但是由于这个常量池的容量的计数是从1开始,而不是从0开始,所以实际这个数组里面存放的cp_info的个数为constant_pool_count所表示的数量-1个。设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。
可以看到接下来的两个字节表示的是constant_pool_count,显示结果为0x13,转换成十进制为19,按照上述描述,数组计数从1开始,那么应该就一共有18个常量,可以看到一共是有18个常量,接下来就一个一个的挨着看这18个常量是怎么解析出来的。
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/rainbow/pet/ClassFile.q:I
#3 = Class #17 // com/rainbow/pet/ClassFile
#4 = Class #18 // java/lang/Object
#5 = Utf8 q
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 add
#12 = Utf8 (I)I
#13 = Utf8 SourceFile
#14 = Utf8 ClassFile.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // q:I
#17 = Utf8 com/rainbow/pet/ClassFile
#18 = Utf8 java/lang/Object
常量池中的每一个cp_info都是一个表,这个表到JDK13的时候,一共由17种类型,cp_info的伪代码可以如下理解,具体tag的可能取值也见下表。
cp_info {
u1 tag; // 每一个常量元素的第一个字节,表示该常量的类型
ux info; // 根据tag的不同类型,后续的字节长度不定,并表示具体的内容。
}
每个常量的第一个字节都是tag,用来确认这个常量具体是什么类型的常量,所以我们继续往下看一个字节,下一个字节是0x0A,根据这个值,查看下表常量类型,可以确认接下来的这个常量类型是CONSTANT_Methodref_info,确认好常量类型之后,再去查找这个类型的常量的具体的数据结构。
CONSTANT_Methodref_info | 类型 | 说明 |
---|---|---|
tag | u1 | 取值上表所示为 10 |
index | u2 | 指向声明方法的类描述符CONSTANT_Class_info的索引项 |
index | u2 | 指向名称及类描述符CONSTANT_NameAndType的索引项 |
可以看到这个类型的常量一共占有5个字节,第一个字节表示类型,后面两个字节都是指向不同类型的索引,继续往下看4个字节,0x0004和0x000F,分别指向常量池的第4项(一个CONSTANT_Class_info类型的常量)和第15项目(一个CONSTANT_NameAndType的常量)。
可以看到常量池的第一项,确实如此,这样我们就分析完了第一个常量,也知道了常量的解析方法:先根据第一个字节的tag,确认这个常量的类型,再根据常量的类型,确认不同的常量的具体的数据结构,接着分析后续的字节。
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
所有的常量类型如下
常量类型 | 标志(tag取值) | 说明 |
---|---|---|
CONSTANT_Utf8_info | 1(0x01) | UFT-8编码的字符串 |
CONSTANT_Integer_info | 3(0x03) | 整型常量 |
CONSTANT_Float_info | 4(0x04) | 浮点数常量 |
CONSTANT_Long_info | 5(0x05) | 长整型常量 |
CONSTANT_Double_info | 6(0x06) | 双精度浮点常量 |
CONSTANT_Class_info | 7(0x07) | 类或者接口的引用 |
CONSTANT_String_info | 8(0x08) | 字符串,并不存储字符串的具体内容,只存了一个索引 |
CONSTANT_Fieldref_info | 9(0x09) | 类中成员变量的符号引用 |
CONSTANT_Methodref_info | 10(0x0A) | 类中成员函数的符号引用 |
CONSTANT_InterfaceMethodref_info | 11(0x0B) | 类中接口函数的符号引用 |
CONSTANT_NameAndType_info | 12(0x0C) | 描述类的成员域或成员函数相关的信息 |
CONSTANT_MethodHandle_info | 15(0x0F) | 成员句柄,描述MethodHandle的信息,和反射有关 |
CONSTANT_MethodType_info | 16(0x10) | 描述成员函数的信息,只包括函数的参数类型和返回值类型 |
CONSTANT_Dynamic_info | 17(0x11) | 描述一个动态计算常量 |
CONSTANT_InvokeDynamic_info | 18(0x12) | 描述一个动态方法的调用点 |
CONSTANT_Module_info | 19(0x13) | 描述一个模块 |
CONSTANT_Package_info | 20(0x14) | 描述一个模块中开放或者导出的包 |
每一种常量的类型,都对应着不同的数据结构
在上面我们已经了解了具体的常量类型和不同的常量类型的数据结构,接着看下面的剩余的常量
第二个常量的tag值是0x09,查表得是一个CONSTANT_Methodref_info,一共占有5个字节,后续得两个字节也是索引,分别指向0x0003(第3个常量)和0x0010(第16个常量)
#2 = Fieldref #3.#16 // com/rainbow/pet/ClassFile.q:I
第三个常量的tag是0x07,查表得是一个CONSTANT_Class_info,一共占有3个字节,后续两个字节也是索引,指向常量池0x0011(第17个常量)
#3 = Class #17 // com/rainbow/pet/ClassFile
第四个常量的tag是0x07,查表得是一个CONSTANT_Class_info,一共占有3个字节,后续两个字节也是索引,指向常量池0x0012(第18个常量)
#4 = Class #18 // java/lang/Object
第五个常量的tag是0x01,查表得是一个CONSTANT_Utf8_info,数据结构的第2、第3个字节0x0001,表示这个utf8字符串的长度,可值这个字符串长度只有一个字节,那么接下来的那个字节则表示的是这个字符串的内容0x71,转换成ascii码为:“q”
#5 = Utf8 q
按照第五个常量CONSTANT_Utf8_info的解析方法,第一个字节是tag,第二第三个字节表示字符串的长度,后续的字符串表示具体的内容,我们解析出后续的第6到第14个字符串常量。
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 add
#12 = Utf8 (I)I
#13 = Utf8 SourceFile
#14 = Utf8 ClassFile.java
第15个常量的tag为0x0C,查表可知是CONSTANT_NameAndType类型的常量,这个常量的数据结构,后面的0x0007和0x0008均为索引,指向常量池的第7项(<init>)和第8项(()V),同理可以得出第16个常量 。
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // q:I
第17个常量的tag是0x01,可知是之前分析过的CONSTANT_Utf8_info,第18个常量也是,这里直接给出对应的结果
#17 = Utf8 com/rainbow/pet/ClassFile
#18 = Utf8 java/lang/Object
到此为止,整个常量池就分析完了,可以看到按照表格的数据,解析出来的结果,和使用javap指令,直接解析出来的结果是一致的。
1.5、访问标志
按照Class文件的结构,在常量池结束之后,为两个字节长度(u2)的访问标志(access_flags),用于标识这个类或者接口的访问信息,多个标志取或得到的结果为这两个字段的结果。
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否为final类型 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义,jdk 1.0.2之后,这个标志都必须为真 |
ACC_INTERFACE | 0x0200 | 是否为一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstact类型,对于接口或者抽象类,此标志值为真,其他类型为假 |
ACC_SYNTHETIC | 0x1000 | 标志这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
ACC_MODULE | 0x8000 | 标识这是一个模块 |
访问标志为0x0021,查看上表,可以知道是ACC_PUBLIC和ACC_SUPER或起来的结果,ACC_SUPER必定为真 ,可知道这个类的可访问下为pulic。
public class com.rainbow.pet.ClassFile
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #3 // com/rainbow/pet/ClassFile
super_class: #4 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
1.6、类索引、父类索引
在访问标志结束之后,是两个字节长度(u2)的类索引(this_class),类索引指向一个CONSTANT_Class_info的类描述符常量,这个类描述符常量里面的索引值才指向CONSTANT_Utf8_info类型的常量中的具体的字符串。同理,接下来的父类索引跟类索引是一样的数据结构。
0x0003是当前类的索引,指向第三个常量,而第3个常量,按照之前的分析,指向第17个常量,查看第17个常量,得到当前类的名字“com/rainbow/pet/ClassFile”,同理可得父类名字“java/lang/Object”。
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/rainbow/pet/ClassFile.q:I
#3 = Class #17 // com/rainbow/pet/ClassFile
#4 = Class #18 // java/lang/Object
#5 = Utf8 q
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 add
#12 = Utf8 (I)I
#13 = Utf8 SourceFile
#14 = Utf8 ClassFile.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // q:I
#17 = Utf8 com/rainbow/pet/ClassFile
#18 = Utf8 java/lang/Object
1.7、接口数、接口索引
在类索引和父类索引之后,为接口数和对应的接口的索引,接口数为这个类实现了多少个接口,如果有N个,那么接下来就由n个长度为u2的接口的索引,接口索引的查找同类索引。从 字节码中可知道,两个字节的接口数为0x0000,及这个类没有实现任何接口,因此接口索引也就没有了。
1.8、字段数和字段表
接下来的是长度为两个字节长度(u2)字段数量(field_count)和field_count个字段表(field_info)。字段表用于描述类级别的变量,但是不包括方法内部的局部变量。
类级别的变量数量field_count为0x0001,说明这个类有一个成员变量,类的成员变量的数据结构如下
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
u2 | attributes_info | attributes_count |
access_flags用于描述这个类变量前面的修饰符,例如是Public还是Private,是否是static等等,可选项如下表
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否为public |
ACC_PRIVATE | 0x0002 | 字段是否为private |
ACC_PROTECTED | 0x0004 | 字段是否为protected |
ACC_STATIC | 0x0008 | 字段是否为static |
ACC_FINAL | 0x0010 | 字段是否为final |
ACC_VOLATILE | 0x0040 | 字段是否为volatile |
ACC_TRANSIENT | 0x0080 | 字段是否为transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动产生 |
ACC_ENUM | 0x4000 | 字段是否为enum |
name_index和descriptor_index均是对常量池的引用,分别表示和这个变量的简单名称和描述符。简单名称顾名思义就是定义的字段的名字,描述符是用来描述这个字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值,描述符的含义如下表:
标识字符 | 含义 |
---|---|
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型long |
S | 基本类型short |
Z | 基本类型boolean |
V | 特殊类型void |
L | 对象类型,例如Ljava/lang/object |
[ | 数组,例如int[]会被记录成[I, int[][]会被记录成[[I |
attributes_count和attributes_info为属性表,表示一些额外的信息,在后续的属性表中介绍。
第1、2个字节为access_flags,为0x0001,查看下表可指标志为ACC_PUBLIC,这个变量是public的,第3、4个字节name_index是名字的索引为0x0005,指向常量池的第5个常量(q),第5、6个字节为descriptor_index是描述的索引为0x0006,指向常量池的第6个常量(I),是一个int类型。第7、8个字节是属性表的数量attributes_count为0x0000,这个变量没有额外的属性,故后续也就没有attributes_info这个字段了。
#5 = Utf8 q
#6 = Utf8 I
{
public int q;
descriptor: I
flags: (0x0001) ACC_PUBLIC
}
1.9、方法数和方法表
接下来的是长度为两个字节长度(u2)方法数量(method_count)和method_count个方法表(method_info)。方法表的结构基本等同于字段表:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
u2 | attributes_info | attributes_count |
因为volatile和transient不能修复方法,故在access_flags里面没有这两个可选项,但是会多出来一个方法特有的可选项,如synchronized、native、strictfp和abstract。
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否为public |
ACC_PRIVATE | 0x0002 | 方法是否为private |
ACC_PROTECTED | 0x0004 | 方法是否为protected |
ACC_STATIC | 0x0008 | 方法是否为static |
ACC_FINAL | 0x0010 | 方法是否为final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否为如synchronized |
ACC_BRIDGE | 0x0040 | 方法是否为由编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否为native |
ACC_ABSTRACT | 0x0400 | 方法是否为abstract |
ACC_STRICT | 0x0800 | 方法是否为stirctfp,精确浮点数 |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动产生 |
方法表中可能会出现编译器自动添加的方法,最常见的便是类构造器<clinit>()方法和实例构造器<init>()方法
可知道method_count的值为0x0002,说明这个类有两个方法,但是实际我们只写了一个add()方法,还有一个方法是编译器自动添加的构造方法。
接下来分析第一个方法,按照上面method_info的数据接口,第1个u2 access_flags 为0x0001为ACC_PUBLIC,第2个 u2 name_index 0x0007 为索引指向常量池的第7个(<init>),第3个 u2 descriptor_index 0x0008 为索引指向常量池的第8个(()V), 由此我们已经可以知道,第一个方法为这个类的构造方法,可见性为public,返回值为void。
#7 = Utf8 <init>
#8 = Utf8 ()V
{
public com.rainbow.pet.ClassFile();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field q:I
9: return
LineNumberTable:
line 3: 0
line 5: 4
}
第4个 u2 attributes_count 为0x0001 说明这个方法带有一个属性,具体的属性的数据结构可以先查看1.2.0。
第二个方法,按照上面的分析的方法,可以知道第二个方法为定义的add方法,其中第12个常量,表示这个方法接受一个int类型的参数,返回值也是一个int值。
#11 = Utf8 add
#12 = Utf8 (I)I
{
public int add(int);
descriptor: (I)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=2
0: iconst_2
1: istore_2
2: iload_2
3: iload_1
4: iadd
5: ireturn
LineNumberTable:
line 8: 0
line 9: 2
}
1.2、属性数和属性表
整个class文件最后两个字段就是属性数(attributes_count)和attributes_count个属性表(attributes_info)了,在字段表和方法表中,最后两个字段也是属性数和属性表,以描述某些场景下的额外的信息。
属性表类似于常量表,每个属性的具体数据结构是不同的,但是所有的属性表的前两个字段是一样的,通用的属性表可以表示成如下结构:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | attribute_name_index | 1 | 属性表名字的索引,查找常量池中具体的名字,根据名字确定是哪个属性 |
u4 | attribute_length | 1 | 属性表具体的长度,整个属性表的长度为attribute_length + 6个字节 |
u1 | info | attribute_length | 具体的某个属性的信息,长度为attribute_length个字节 |
属性名称 | 使用位置 | 含义 | |
---|---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 | |
ConstantValue | 字段表 | 由final关键字定义的常量值 | |
Deprecated | 类,方法,字段表 | 被声明为deprecated的方法和字段 | |
Exceptions | 方法表 | 方法抛出的异常 | |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 | |
InnerClass | 类文件 | 内部类列表 | |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 | |
LocalVariableTable | Code属性 | 方法的局部变量描述 | |
StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 | |
Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 | |
SourceFile | 类文件 | 记录源文件名称 | |
SourceDebugExtension | 类文件 | 用于存储额外的调试信息 | |
Synthetic | 类,方法表,字段表 | 标志方法或字段为编译器自动生成的 | |
LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 | |
RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 | |
RuntimeInvisibleAnnotations | 表,方法表,字段表 | 用于指明哪些注解是运行时不可见的 | |
RuntimeVisibleParameterAnnotation | 方法表 | 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法 | |
RuntimeInvisibleParameterAnnotation | 方法表 | 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数 | |
AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 | |
BootstrapMethods | 类文件 | 用于保存invokeddynamic指令引用的引导方式限定符 | |
RuntimeVisibleTypeAnnotations | 类,方法表,字段表,Code属性 | 指明哪些类注解是运行时可见的 | |
RuntimeInvisibleTypeAnnotations | 类,方法表,字段表,Code属性 | 指明哪些类注解是运行时不可见的 | |
MethodParameters | 方法表 | 用于支持将方法名称编译进Class文件中,并可运行时获取 | |
Module | 类 | 用于记录一个Module的名称和相关信息(requires、export、opens、uses、provides) | |
ModulePackages | 类 | 用于记录一个模块中所有被exports或者opens的包 | |
ModuleMainClass | 类 | 用于指定一个模块的主类 | |
NestHost | 类 | 用于支持嵌套类的反射和访问控制的api,一个内部类通过该属性知道自己的宿主类 | |
NestMembers | 类 | 用于支持嵌套类的反射和访问控制的api,一个宿主类通过该属性得知自己有哪些内部类 |
常用的属性表
以Code属性为例,看一个具体的属性的数据结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
其中attribute_name_index指向常量池中一个CONSTATN_Uft8_info的常量,固定为“Code”,代表了这个属性的名称,接下来是属性值的长度attribute_length,及除了attribute_name_index 和 attribute_length 之后,所有的其他字段加起来是 attribute_length 个字节,整个Code属性表的长度为attribute_length + 6 个字节长。
接下来的max_stack和max_locals表示了操作数栈(Operand Stack)深度的最大值和局部变量所需要的存储空间。code_length和code表示了方法具体的代码翻译成指令之后有多少条,以及具体的指令是什么。
exception_table_length 和 exception_info 是代码里的try{}catch(){}finally{},翻译过来的异常表,在最后这个Code的属性表又带了一个属性表数量和属性表的字段,意思是属性表里面还可以套其他属性表,Code下面可以带其他的属性。
这里只展示了一个具体的属性的数据结构,因为属性太多了,具体分析的时候,可以根据attribute_name_index确认了是哪个属性,然后去查找对应的属性的数据结构,按照Code属性一样的分析方式即可。
属性表的分析跟常量池的分析类似,先根据前两个字节,确认是哪个属性,再去查具体的属性的数据结构,在上一节,两个方法都带有属性表,依次分析一下
前两个字节0x0009表示attribute_name_index为指向常量池的索引,查找第9个常量为Code,可以知道这个属性为Code,查看上述的Code属性的数据结构,接下来的四个字节表示attribute_length为0x00000026,转成十进制为38,后续就还有38个字节长度。
#9 = Utf8 Code
按照Code属性的数据结构,接下来的2个字节为max_stack 0x0002 表示此方法操作数栈(Operand Stack)深度的最大值为2,接着的2个字节为max_locals 0x0001 表示此方法局部变量所需要的存储空间为1的槽,接下来的4个字节为code_length 0x0000000A 表示此方法的指令码的长度为10个字节,根据文末的虚拟机字节码指令表查找这些指令,分别为0x2A(aload_0) 、0xB7(invokespecial) 0x0001 (#1索引,指向第1个常量)、0x2A(aload_0)、0x04(iconst_1)、0xB5(putfield) 0x0002 (#2索引,指向第2个常量) 、0xB1(return),有些指令码带有操纵,就会多占一些字节。
接下来的2个字节是exception_table_length异常表的长度为0,及没有异常处理。
这个Code属性表,最后又套娃了1一个属性表,attributes_count 长度为 1,又按照属性表的属性结构查看后续的两个字节为attribute_name_index 0x000A 指向第10个常量 LineNumberTable,接着又是4个字节长度的attribute_length 0x0000000A 为10个字节长度。
line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合, line_number_info表包含start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源 码行号。
接下来的0x0002为line_number_table_length,表示这个line_number_table里面有两个line_number_info。
查看第1个line_number_info,0x0000 为 start_pc 字节码行号0,0x0003 为 line_number 为Java源代码行号
查看第2个line_number_info,0x00004 为 start_pc 字节码行号0,0x0005 为 line_number 为Java源代码行号。
上述一个方法就分析完成了,可以看到跟下面的代码是匹配一致的。
#10 = Utf8 LineNumberTable
{
public int q;
descriptor: I
flags: (0x0001) ACC_PUBLIC
public com.rainbow.pet.ClassFile();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field q:I
9: return
LineNumberTable:
line 3: 0
line 5: 4
}
{
public int add(int);
descriptor: (I)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=2
0: iconst_2
1: istore_2
2: iload_2
3: iload_1
4: iadd
5: ireturn
LineNumberTable:
line 8: 0
line 9: 2
}
第2个自定义的add()方法,可以按照上述的第1个方法进行分析,不再赘述。
整个这个类的Java的字节码,就剩最后一点点没有分析啦,按照Class文件格式,在方法表结束之后,是这个类的属性表,也就是1.1里面的第15项和第16项,第15项是两个字节长度,也就是0x0001,说明后续只有一个属性了,继续按照属性表的通用格式接下来的两个字节attribute_name_index 0x000D,指向常量池中的第13项为SourceFile,查看SourceFile属性的数据结构如下。
#13 = Utf8 SourceFile
#14 = Utf8 ClassFile.java
SourceFile: "ClassFile.java"
接下来的4个字节为attribute_length 0x00000002,表示后续还有两个字节长度的sourcefile_index 0x000E 指向常量池中的第14个常量ClassFile.java。
第一次完整的挨着一个字节一个字节的分析完了一个Class文件,比较麻烦的就是字段表、方法表、属性表,因为这几个表都可以套娃属性表,就会比较冗长一些。
虽然已经很多人写过了,但是自己去分析的时候,才能更深入的进行一下,但是由于示例很简单,还有很多的属性、字段都还没有分析到,不过掌握了分析的方法,没有遇到的属性、字段等,也可以按照相同的方式方法进行分析即可。
2、虚拟机字节码指令表
3、参考文档
《深入理解Java虚拟机》
转载自:https://juejin.cn/post/7216613455192997948