Java二进制Class文件格式解析
一、Java Class文件是什么
《The JavaTM Virtual Machine Specification》(Second Edtion)中有表述:Java Class文件由8位字节流组成,所有的16位、32位和64位数据分别通过读入2个、4个和8个字节来构造,多字节数据总是按照Big-endian顺序来存放,即高位字节在前(放在低地址)。每个Class文件都包含且仅包含一个Java类型(类或者接口)。
或许,《The JavaTM Virtual Machine Specification》中的表述不够明确,那么我们可以参考一下《Inside the Java Virtual Machine》(Second Edtion)中的表述:Java Class文件特指以.class为后缀名的Java虚拟机可装载的文件。
分析一下两者的表述,我觉得都不够全面、不够明确。我是这么定义的:Java Class文件就是指符合特定格式的字节流组成的二进制文件。这个特定的格式就是指第二节要讨论的Class文件格式,亦即在《The JavaTM Virtual Machine Specification》中定义的Class文件格式。从另一个角度来说,这个特定格式就是指JVM能够识别、能够装载的格式。为什么这么说呢?因为JVM在装载class文件时,要进行class文件验证,以保证装载的class文件内容符合正确的内部结构。这个内部结构指的就是这个特定格式,只要是符合这个特定格式的Class文件都是合法的、规范的Class文件,都是JVM能够装载的Class文件。如果觉得这样的表述还是不够明确,我只能建议你读完这篇
文章之后再回头来理解看看了J
为了讨论方便,在下文中将对这两个参考资料做个简记:
1)《The Java Virtual Machine Specification》(Second Edtion)简记为《JVM Spec》(2nded)。
2)《Inside the Java Virtual Machine》(Second Edtion) 简记为《Inside JVM》(2nded)。
二、Java Class文件的格式
在讲Class文件的格式之前,要介绍三个概念:
1)数据类型:《JVM Spec》(2nded)中指出,Java Class文件的数据用自己定义的一个数据类型集来表示,即u1,u2,u4,分别用于表示一个无符号类型的、占1,2,4个字节的数据。在《Inside JVM》(2nded)一书中,作者把这个数据类型集称之为Class文件的基本类型,本人觉得比较形象,便于理解。所以,在本文中,我们也用基本类型来表示Java Class文件的数据。
2)表:根据《JVM Spec》(2nded)中的定义,表(table)由项(定义见3)组成,用于几种Class文件结构中。《JVM Spec》(2nded)中指出,Java Class文件格式用一个类似于C结构的记号编写的伪结构来表示。这个伪结构指的就是这里的表,例如下面的ClassFile表就是这种伪结构的一个典型例子,下文中所有的表都是指这种伪结构的表。表的大小是可变的,这是因为它的组成部分项是可变的。注意;这里的可变是针对Class层次而言的,即在不同的Class文件中该项的大小可能不一样的,但是对于每一个具体的Class文件来说,这个项的大小又是一定的,因而这个表的大小也是一定的。那么,项为什么是可变的呢?请看下面的分析。
3)项:描述Java Class文件格式的结构的内容称为项(items)。每个项都有自己的类型和名称。项的类型可能是基本类型,也可能是一个表的名字,这种项都是一些数组项。数组项的每一个元素都是一个表,这个表同顶层的ClassFile表一样,也都是一种伪结构,也都是由一些项构成的,而且这些表不一定是同一种格式的,因此数组项也可以看作一个可变大小的结构流J。这些表对于该数组项来说就是子项,当然子项可能还有子项(目前子项的深度最多就两层)。项的名称,没有什么好说的,就是《JVM Spec》(2nded)中指定的一些名称。另外,项也是有大小的,对于没有子项的项来说,其大小是固定的;对于有子项的项来说,其大小是可变的。在一个具体的Class文件中,一个可变项(数组)的大小都会在其前一项中指定,为什么会是这样的呢?因为《JVM Spec》(2nded)中就是这么定义的!在Class文件中,每个项按规范中定义好的顺序存储在Class文件中,相邻的项之间没有任何间隔,连续的项(数组)也是按顺序存储,不进行填充或者对齐,这样可以使Class文件紧凑。
好了,我想这三个概念我已经解释地比较清楚了,下面开始正式解析Class文件的格式。
首先要来解析一下ClassFile表结构,这是《JVM Spec》(2nded)中定义的Class文件最外层的结构,换言之,就是Class文件的格式。
ClassFile表结构
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];
}
|
ClassFile表结构由16个不同的项组成,其中的各项可以简要地分析如下:
(1) magic
每个Class文件的前4个字节被称为它的魔数(magic number): 0xCAFEBABE。魔数的作用在于:可以轻松地分辨出Java Class文件和非Java Class文件。(如果一个文件不是以0xCAFEBABE开头,它就肯定不是Java Class文件,因为它不符合规范J)。当Java还称为“Oak”的时候,这个魔数就已经定下来了,它预示了Java这个名字的出现。魔数的来历请大家自己查阅J
(2) minor_version和major_version
Class文件的下面4个字节包含了次、主版本号。通常只有给定主版本号和一系列次版本号后,Java虚拟机才能够读取Class文件。如果Class文件的版本号超出了Java虚拟机所能够处理的有效范围,Java虚拟机将不会处理该Class文件。例如J2SE5.0版本的虚拟机就不能执行由J2SE6.0版本的编译器编译出来的Class文件。
(3) constant_pool_count
版本号后面的项是constant_pool_count即常量池计数项,该项的值必须大于零,它给出该Class文件中常量池列表项的元素个数,这个计数项包括了索引为0的constant_pool表项,但是该表项不出现在Class文件的constant_pool列表中,因为它被保留为Java虚拟机内部实现使用了,因此常量池列表的元素个数constant_pool_count-1,各个常量池表项的索引值分别为1到constant_pool_count-1。
注:在这里,有几个术语需要解释一下,常量池即为constant_pool,常量池列表就是指constant_pool[ ],常量池表项即指常量池列表中的某一个具体的表项(元素)。这些常量池表项的可能类型如下述的cp_type表所示:
cp_type
入口类型 标志值
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
(4) constant_pool[ ]
constant_pool_count项下面是constant_pool[ ]项,即常量池列表,其中存储了该ClassFile结构及其子结构中引用的各种常量,诸如文字字符串、final变量值、类名和方法名等等。在Java Class文件中,常量池表项是用一个cp_info结构来描述的,常量池列表就是由constant_pool_count-1个连续的、可变长度的cp_info表结构构成的constant_pool[ ]数组。为什么是constant_pool_count-1个constant_pool的原因,在上面已经解释了。每一个常量池表项都是一个变长结构,其通常格式如下所示:
cp_info
cp_info表的tag项是一个无符号的byte类型值,它表明了cp_info表的类型和格式,具体的tag类型见上表。
需要说明的是,cp_info只是一个抽象的概念,在Class文件中,它表现为一系列具体的、形如CONSTANT_Xxxx_info的constant_pool结构,其具体的格式由cp_info表的tag项(即第一个字节)来确定。不同的cp_info表,其info[]项也是不一样的,例如,CONSTANT_Class_info表的info[]项为“u2 name_index”,而CONSTANT_Utf8_info表的info[]项为“u2 length; u1 bytes[length];”,显然,这两个cp_info表是不一样的,大小更是不一样的,因而常量池表项的大小是可变的。由于常量池列表中的每个常量池表项的结构是不一样,因此常量池列表的大小也是可变的。在Class文件中,常量池列表项是一个可变长度的结构流。
由cp_info表以及cp_type表我们可以知道,若cp_info表中tag(标志)项的值为1时,当前的cp_info就是一个CONSTANT_Utf8_info表结构,若cp_info表中tag项的值为3,当前的cp_info就是一个CONSTANT_Integer_info表结构,其它情况类推。这些表的结构可以查阅《JVM Spec》(2nded)的第四章或者《Inside JVM》(2nded)的第六章。
(5) access_flags
紧接常量池后的两个字节称为access_flags,access_flags项描述了该Java类型的一些访问标志信息。例如,访问标志指明文件中定义的是类还是接口;访问标志还定义了在类或接口的声明中,使用了哪些修饰符;类和接口是抽象的还是公共的等等。实际上,access_flags项的值是Java类型声明中使用的访问标志符的掩码(mask,这里掩码指的是access_flags的值是所有访问标志值的总和,当然,未被使用的标志位在Class文件中都被设置为0。例如,若access_flags的值就是0x0001,就表示该Java类型的访问标志符是ACC_PUBLIC;若access_flags的值是0x0011,就表示该Java类型的访问标志符是ACC_PUBLIC和ACC_FINAL,因为只有这两个标志位的和才可能是0x0011;其它情况类推)。
一个Java类型的所有access_flags标志符如下表所示:
access_flags
标志名称 值 含义
ACC_PUBLIC 0x0001 声明为public,可以从它的包外访问
ACC_FINAL 0x0010 声明为final,不允许有子类
ACC_SUPER 0x0020 用invokespecial指令处理超类的调用
ACC_INTERFACE 0x0200 表明是一个接口,而不是一个类
ACC_ABSTRACT 0x0400 声明为abstract,不能被实例化
需要说明的是,这是针对一个Java类型的访问标志符列表,有的标志符只有类可以使用,有的标志符只有接口才可以使用,详情请查阅《JVM Spec》(2nded)。
ClassFile表结构由16个不同的项组成,其中的各项可以简要地分析如下:
(1) magic
每个Class文件的前4个字节被称为它的魔数(magic number): 0xCAFEBABE。魔数的作用在于:可以轻松地分辨出Java Class文件和非Java Class文件。(如果一个文件不是以0xCAFEBABE开头,它就肯定不是Java Class文件,因为它不符合规范J)。当Java还称为“Oak”的时候,这个魔数就已经定下来了,它预示了Java这个名字的出现。魔数的来历请大家自己查阅J
(2) minor_version和major_version
Class文件的下面4个字节包含了次、主版本号。通常只有给定主版本号和一系列次版本号后,Java虚拟机才能够读取Class文件。如果Class文件的版本号超出了Java虚拟机所能够处理的有效范围,Java虚拟机将不会处理该Class文件。例如J2SE5.0版本的虚拟机就不能执行由J2SE6.0版本的编译器编译出来的Class文件。
(3) constant_pool_count
版本号后面的项是constant_pool_count即常量池计数项,该项的值必须大于零,它给出该Class文件中常量池列表项的元素个数,这个计数项包括了索引为0的constant_pool表项,但是该表项不出现在Class文件的constant_pool列表中,因为它被保留为Java虚拟机内部实现使用了,因此常量池列表的元素个数constant_pool_count-1,各个常量池表项的索引值分别为1到constant_pool_count-1。
注:在这里,有几个术语需要解释一下,常量池即为constant_pool,常量池列表就是指constant_pool[ ],常量池表项即指常量池列表中的某一个具体的表项(元素)。这些常量池表项的可能类型如下述的cp_type表所示:
cp_type
入口类型 标志值
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
(4) constant_pool[ ]
constant_pool_count项下面是constant_pool[ ]项,即常量池列表,其中存储了该ClassFile结构及其子结构中引用的各种常量,诸如文字字符串、final变量值、类名和方法名等等。在Java Class文件中,常量池表项是用一个cp_info结构来描述的,常量池列表就是由constant_pool_count-1个连续的、可变长度的cp_info表结构构成的constant_pool[ ]数组。为什么是constant_pool_count-1个constant_pool的原因,在上面已经解释了。每一个常量池表项都是一个变长结构,其通常格式如下所示:
cp_info
cp_info表的tag项是一个无符号的byte类型值,它表明了cp_info表的类型和格式,具体的tag类型见上表。
需要说明的是,cp_info只是一个抽象的概念,在Class文件中,它表现为一系列具体的、形如CONSTANT_Xxxx_info的constant_pool结构,其具体的格式由cp_info表的tag项(即第一个字节)来确定。不同的cp_info表,其info[]项也是不一样的,例如,CONSTANT_Class_info表的info[]项为“u2 name_index”,而CONSTANT_Utf8_info表的info[]项为“u2 length; u1 bytes[length];”,显然,这两个cp_info表是不一样的,大小更是不一样的,因而常量池表项的大小是可变的。由于常量池列表中的每个常量池表项的结构是不一样,因此常量池列表的大小也是可变的。在Class文件中,常量池列表项是一个可变长度的结构流。
由cp_info表以及cp_type表我们可以知道,若cp_info表中tag(标志)项的值为1时,当前的cp_info就是一个CONSTANT_Utf8_info表结构,若cp_info表中tag项的值为3,当前的cp_info就是一个CONSTANT_Integer_info表结构,其它情况类推。这些表的结构可以查阅《JVM Spec》(2nded)的第四章或者《Inside JVM》(2nded)的第六章。
(5) access_flags
紧接常量池后的两个字节称为access_flags,access_flags项描述了该Java类型的一些访问标志信息。例如,访问标志指明文件中定义的是类还是接口;访问标志还定义了在类或接口的声明中,使用了哪些修饰符;类和接口是抽象的还是公共的等等。实际上,access_flags项的值是Java类型声明中使用的访问标志符的掩码(mask,这里掩码指的是access_flags的值是所有访问标志值的总和,当然,未被使用的标志位在Class文件中都被设置为0。例如,若access_flags的值就是0x0001,就表示该Java类型的访问标志符是ACC_PUBLIC;若access_flags的值是0x0011,就表示该Java类型的访问标志符是ACC_PUBLIC和ACC_FINAL,因为只有这两个标志位的和才可能是0x0011;其它情况类推)。
一个Java类型的所有access_flags标志符如下表所示:
access_flags
标志名称 值 含义
ACC_PUBLIC 0x0001 声明为public,可以从它的包外访问
ACC_FINAL 0x0010 声明为final,不允许有子类
ACC_SUPER 0x0020 用invokespecial指令处理超类的调用
ACC_INTERFACE 0x0200 表明是一个接口,而不是一个类
ACC_ABSTRACT 0x0400 声明为abstract,不能被实例化
需要说明的是,这是针对一个Java类型的访问标志符列表,有的标志符只有类可以使用,有的标志符只有接口才可以使用,详情请查阅《JVM Spec》(2nded)。