class文件分析
以最简单的helloworld入手进行分析:
java代码:
public class ClassTest{
public static void main(String args[]){
System.out.println("Hello World.");
}
}
javac编译,使用文本编辑器打开.class文件
cafe babe 0000 0034 001d 0a00 0600 0f09
0010 0011 0800 120a 0013 0014 0700 1507
0016 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 046d 6169
6e01 0016 285b 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 2956 0100 0a53 6f75
7263 6546 696c 6501 000e 436c 6173 7354
6573 742e 6a61 7661 0c00 0700 0807 0017
0c00 1800 1901 000c 4865 6c6c 6f20 576f
726c 642e 0700 1a0c 001b 001c 0100 0943
6c61 7373 5465 7374 0100 106a 6176 612f
6c61 6e67 2f4f 626a 6563 7401 0010 6a61
7661 2f6c 616e 672f 5379 7374 656d 0100
036f 7574 0100 154c 6a61 7661 2f69 6f2f
5072 696e 7453 7472 6561 6d3b 0100 136a
6176 612f 696f 2f50 7269 6e74 5374 7265
616d 0100 0770 7269 6e74 6c6e 0100 1528
4c6a 6176 612f 6c61 6e67 2f53 7472 696e
673b 2956 0021 0005 0006 0000 0000 0002
0001 0007 0008 0001 0009 0000 001d 0001
0001 0000 0005 2ab7 0001 b100 0000 0100
0a00 0000 0600 0100 0000 0100 0900 0b00
0c00 0100 0900 0000 2500 0200 0100 0000
09b2 0002 1203 b600 04b1 0000 0001 000a
0000 000a 0002 0000 0003 0008 0004 0001
000d 0000 0002 000e
参照:java虚拟机规范:https://docs.oracle.com/javase/specs/jvms/se8/html/ 第四节
class文件结构为:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
u2 u4 分别代表2个字节、4个字节
开始对class文件进行分析,class文件是16进制的。
我们先javap拿到常量池中的内容:
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello World.
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // ClassTest
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 ClassTest.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello World.
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 ClassTest
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
1-4字节:u4: 表示魔数:cafe babe 据说是java撰写者对咖啡的喜爱命名的,如果修改魔术为其他值会报错
5-6字节:表示class文件次要版本:
7-8字节:class主版本号:
可以看出,上述的class是基于jdk1.8的
9-10字节:表示常量池的数量,这里是001d 换算成10进制即为29
后面cp_info 数量为 constant_pool[constant_pool_count-1],所以常量池真正为29-1=28个
接下来是cp_info即常量池的内容,常量池的表示通常是一个字节表示类型,根据每个类型有后续的描述,对照为:
cp_info {
u1 tag;
u1 info[];
}
Constant Type | Value |
---|---|
CONSTANT_Class |
7 |
CONSTANT_Fieldref |
9 |
CONSTANT_Methodref |
10 |
CONSTANT_InterfaceMethodref |
11 |
CONSTANT_String |
8 |
CONSTANT_Integer |
3 |
CONSTANT_Float |
4 |
CONSTANT_Long |
5 |
CONSTANT_Double |
6 |
CONSTANT_NameAndType |
12 |
CONSTANT_Utf8 |
1 |
CONSTANT_MethodHandle |
15 |
CONSTANT_MethodType |
16 |
CONSTANT_InvokeDynamic |
18 |
所以:
第1个常量:11字节为:0a 表示为10进制为10,其为:CONSTANT_Methodref 方法引用类型的常量
CONSTANT_Methodref_info { u1 tag; u2 class_index; 指向声明方法的类苗舒服Constant_Class_info的索引项 u2 name_and_type_index; 指向名称及类型描述符Constant——nameandtype的索引项 }
在这里我们取11-15字节内容:0a00 0600 0f : 0a:类型 00 06 表示指向常量池第六个常量所表示的信息,对照常量池,第六个是java/lang/Object , 00 0f表示指向常量池第 15 个常量所表示的信息,对照得:"<init>":()V 组合起来就是 java/lang/Object.<init>:V 即Object的init初始化方法。
继续分析后面的内容
第2个常量:16字节 09 对应CONSTANT_Fieldref
CONSTANT_Fieldref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
拿出16-20字节:09 0010 0011 其中0010 为常量池第16个常量 “java/lang/System” ,0011为第17个:“out:Ljava/io/PrintStream;”,即“java/lang/System.out:Ljava/io/PrintStream;”
第3个常量:21字节08 为CONSTANT_String
CONSTANT_String_info { u1 tag; u2 string_index; }
拿出21-23字节:0800 12 其中00 12为第18个常量:Hello World.
第4个常量:24字节0a 为11 :CONSTANT_InterfaceMethodref
CONSTANT_InterfaceMethodref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
拿出24-28 :0a 0013 0014 其中00 13为第19个常量:“java/io/PrintStream”,0014为第20个:“println:(Ljava/lang/String;)V”,即“java/io/PrintStream.println:(Ljava/lang/String;)V”;
第5个常量:29字节07:CONSTANT_Class
CONSTANT_Class_info { u1 tag; u2 name_index; }
拿出29-31字节:0700 15 其中00 15 为第21常量:ClassTest
第6个常量:30字节:07 :CONSTANT_Class
拿出31-33字节: 07 0016 其中0016 为第22常量:java/lang/Object
第7个常量:34字节:01:CONSTANT_Utf8 字符串常量
CONSTANT_Utf8_info { u1 tag; u2 length; Utf-8编码的字符串占用的字节数 u1 bytes[length]; 长度为length的utf-8编码的字符串 }
拿出34-37 :0100 063c 696e 6974 3e其中00 06表示长度为6个字节,紧跟的3c 696e 6974 3e为字符串的值。在class文件中字符串是使用ASCII 码进行编码的,将这些16进制字符进行转换成对应的ASCII 码后值为:“<init>”
第8个常量:01 0003 2829 56 长度3字节,转ASCII码后:“()V”
第9个常量:01 0004 436f 6465 长度4字节,转码后:“Code”
第10个常量:0100 0f4c 696e 654e 756d 6265 7254 6162 6c65 长度15,转码后:“LineNumberTable”
第11个常量:0100 046d 6169 6e 长度4,转码后:“main”
第12个常量:01 0016 285b 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 2956 长度22,转码后:“([Ljava/lang/String;)V”
第13个常量:0100 0a53 6f75 7263 6546 696c 65 长度10,转码后“SourceFile”
第14个常量:01 000e 436c 6173 7354 6573 742e 6a61 7661 长度14,转码后:“ClassTest.java”
第15个常量:0c00 0700 08 :CONSTANT_NameAndType
CONSTANT_NameAndType_info { u1 tag; u2 name_index; 字段或方法名称常量的索引 u2 descriptor_index;字段或方法描述符常量项的索引 }
0c:方法引用类型 ,00 07:第七个常量“<init>”,00 08:第八个:“()V”,所以信息是:“"<init>":()V”
第16个常量:07 0017:类信息类型,第23个常量:“java/lang/System”
第17个常量:0c00 1800 19 :方法引用类型,常量池第24个常量:“out”, 第25个:“Ljava/io/PrintStream;”,所以是:“out:Ljava/io/PrintStream;”
第18个常量:01 000c 4865 6c6c 6f20 576f 726c 642e 字符串常量引用“Hello World.”
第19个常量:0700 1a:类信息类型,第26个常量:“java/io/PrintStream”
第20个常量:0c 001b 001c:方法引用类型,001b:第27个常量:“println”,001c:第28个常量:“(Ljava/lang/String;)V” ,所以是:“println:(Ljava/lang/String;)V”
第21个常量:0100 0943 6c61 7373 5465 7374:字符串引用类型:“ClassTest”
第22个常量:0100 106a 6176 612f 6c61 6e67 2f4f 626a 6563 74 字符串:“java/lang/Object”
第23个常量:01 0010 6a61 7661 2f6c 616e 672f 5379 7374 656d 字符串:“java/lang/System”
第24个常量:0100 036f 7574 字符串:“out”
第25个常量:0100 154c 6a61 7661 2f69 6f2f 5072 696e 7453 7472 6561 6d3b 字符串:“Ljava/io/PrintStream;”
第26个常量:0100 136a 6176 612f 696f 2f50 7269 6e74 5374 7265 616d 字符串:“java/io/PrintStream”
第27个常量:0100 0770 7269 6e74 6c6e 字符串:“println”
第28个常量:0100 1528 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 2956 字符串:“(Ljava/lang/String;)V”
以上常量分析完毕,和javap对比,结果是一致的。
接下来是常量池之后的内容
u2 access_flags访问标记:0021 :
access_flags说明:
Flag Name | Value | Interpretation |
---|---|---|
ACC_PUBLIC |
0x0001 | Declared public ; may be accessed from outside its package. |
ACC_PRIVATE |
0x0002 | Declared private ; accessible only within the defining class. |
ACC_PROTECTED |
0x0004 | Declared protected ; may be accessed within subclasses. |
ACC_STATIC |
0x0008 | Declared static . |
ACC_FINAL |
0x0010 | Declared final ; must not be overridden (§5.4.5). |
ACC_SYNCHRONIZED |
0x0020 | Declared synchronized ; invocation is wrapped by a monitor use. |
ACC_BRIDGE |
0x0040 | A bridge method, generated by the compiler. |
ACC_VARARGS |
0x0080 | Declared with variable number of arguments. |
ACC_NATIVE |
0x0100 | Declared native ; implemented in a language other than Java. |
ACC_ABSTRACT |
0x0400 | Declared abstract ; no implementation is provided. |
ACC_STRICT |
0x0800 | Declared strictfp ; floating-point mode is FP-strict. |
ACC_SYNTHETIC |
0x1000 | Declared synthetic; not present in the source code. |
这里标志值是多个值进行或运算的结果,0021由0001和0020或运算的到,也就是访问标志为public且允许使用invokespecial 字节码指令的新语义。
在访问标记后,则是类索引、父类索引、接口索引的数据,这里数据为:00 05 00 06 00 00。
类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class 文件中由这三项数据来确定这个类的继承关系。
类索引。类索引用于确定这个类的全限定名,它用一个 u2 类型的数据表示。这里的类索引是 00 05 表示其指向了常量池中第 5 个常量,通过我们之前的分析,我们知道第 5 个常量其最终的信息是 ClassTest 类。
父类索引。父类索引用于确定这个类的父类的全限定名,父类索引用一个u2类型的数据表示。这里的父类索引是 00 06 表示其指向了常量池中第 6 个常量,通过我们之前的分析,我们知道第 6 个常量其最终的信息是 Object 类。因为其并没有继承任何类,所以 ClassTest 类的父类就是默认的 Object 类。
接口索引。接口索引集合就用来描述哪个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身就是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。对于接口索引集合,入口第一项是 u2 类型的数据为接口计数器(interfaces_count),表示索引表的容量,而在接口计数器后则紧跟着所有的接口信息。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。
字段表集合
field_count 、field_info
字段表集合用于描述接口或者类中声明的变量,这里的数据为:00 00。
这里说的字段包括类级变量和实例级变量,但不包括在方法内部声明的局部变量。在类接口集合后的2个字节是一个字段计数器,表示总有有几个属性字段。在字段计数器后,才是具体的属性数据。
字段表的每个字段用一个名为 field_info 的表来表示,field_info 表的数据结构如下所示:
因为我们并没有声明任何的类成员变量或类变量,所以在 字节码文件中,字段计数器为 00 00,表示没有属性字段。
方法表集合
methods_count、methods
方法结构:
ClassTest类的字节码文件中,
方法计数 0002 有两个方法,
第一个方法:0001 0007 0008 0001 0009 0000 001d 0001 0001 0000 0005 2ab7 0001 b100 0000 0100 0a00 0000 0600 0100 0000 01
其中0001 表示ACC_PUBLIC,即访问标识为public,之后的两个字节0007是放方法名称的索引,执行了第七个常量为:“<init>”,接着的两个字节是方法描述符索引项,0008,表示常量池第八个:“()V”, 0001释放属性计数器,表示该方法的属性表有一个属性,属性表:
前两个是名字索引、之后的4个字节是属性长度、接着是属性的值。这里为0009,第九个常量为:“Code”,说明此属性是方法的字节码描述,Code属性的表结构为:
TODO