从上一篇博客(Java Class文件解析)的分析可以看出,Class文件中的各项是按照一定的包含关系和次序关系存储的,因此Class文件可以从头到尾地被解析为各个项。下面请看一个解析实例:
这里我们以非常著名的HelloWorld为实例来分析Java Class文件。HelloWorld的源代码如下:
package bytecodeResearch;
public class HelloWorld {
//定义两个静态变量
private static String str_1 = "Hello";
private static String str_2 = "World";
/** *//**
* 静态方法
* @param str
*/
private static void Hello(String str)
{
System.out.println(str);
}
/** *//**
* 静态方法
* @param str
*/
private static void World(String str)
{
System.out.println(str);
}
/** *//**
* 程序入口方法
* @param args
*/
public static void main(String[] args)
{
Hello(str_1);
World(str_2);
}
}
编译单元HelloWorld.java文件经过编译器编译之后,将得到一个HelloWorld.class文件。需要说明的是,经过不同的编译器编译之后得到的HelloWorld.class文件可能不一样,本人选用的开发工具是Eclipse3.3-europa版本,Eclipse SDK自带的JDT工具内置了增量式Java编译器,这个编译器与javac完全兼容。本人以下的分析都是基于Eclipse自带的编译器编译得到的Class文件,如果有人用了Jikes编译器、GNU的编译器或者其它版本的javac编译器的话,得到的Class文件跟我的可能不完全一样的话,但是Class文件的格式肯定是一样的,因此分析的原理也都是一样的,即都是基于上一篇博客(Java Class文件解析)给出的ClassFile结构图示。
经过Eclipse3.3编译得到的Class文件内容如下:
注:该Class的内容是经过解析的,即将.class文件的二进制字节流转换为16进制的数字字符流形式。由于UE查看Class文件时,用一个8位的16进制数来表示每一行起始字节对应于Class文件中的字节号,为了不造成视觉差异,本人写了一段程序来读写Class文件内容,其输出结果就是上面这段内容,每16个字节占一行,每个字节均拆解成2位16进制整数字符,每一行左端的8位的16进制整数就表示该行的起始字节对应于原Class文件中的字节号。这个程序贴在了我第一篇blog里--一个读取Class文件的示例程序了。
好了,闲言少叙!下面开始正式解析这个Class文件。
按照上一篇博客(Java Class文件解析)介绍的ClassFile结构图示以及对ClassFile结构的详细分析,可以将HelloWorld.class文件的各个项顺序地解析如下:
(1) 前4个字节0xCAFEBABE是magic项的内容。
(2) 接下来的2个字节0x0000是minor_version项的内容,即该Class文件的次版本号0。
(3) 接下来的2个字节0x0031是major_version项的内容,即该Class文件的主版本号为49。这个主版本号对应于J2SE5.0的编译器的编译结果。如果你用J2SE6.0的编译器来编译此HelloWorld.java程序的话,得到的主版本号应该是0x0032,即50。
(4) 接下来的2个字节0x0031是constant_pool_count项的内容,它表示接下来共有连续的49-1=48个constant_pool表项。
(5) 紧接着constant_pool_count项后面的是常量池列表项。常量池列表项的长度是可变的,这是因为不同的常量池表项的格式是不一样的。
5.1)先来分析第一个常量池表项。上一篇博客(Java Class文件解析)中提到,每个常量池表项的具体格式是要根据其tag项(即该常量池表项的第一个字节)来决定。因此,constant_pool_count项后面的第一字节就是第一个常量池表项的tag项的内容,在这里为0x07,cp_type表可知,tag值为0x07的对应CONSTANT_Class结构的常量池表项。再查阅CONSTANT_Class_info表,该表结构如下:
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
根据《JVM Spec》(2nded)中对此表的说明可知,1个字节的tag项就是刚才读取的值0x07,而后的2个字节的name_index项表示对一个CONSTANT_Utf8_info表的索引,该索引项包含了类或者接口的完全限定名称。对于HelloWorld.class这个类来说,第一个常量池表项就是一个CONSTANT_Class_info表的结构,其tag项的值是0x07,其name_index项的值是0x0002,即该常量池表项指向索引为0x0002的常量池表项。
5.2)再来分析第二个常量池表项。其tag项值为0x01,查阅cp_type表可知,该常量池表项是一个CONSTANT_ Utf8_info表的结构,然后我们查阅CONSTANT_ Utf8_info表,该表结构如下:
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
根据《JVM Spec》(2nded)中对此表的说明可知,1个字节的tag项的值就是0x01,2个字节的length项给出了后续的bytes数组的长度(字节数),在这里为0x001B,即其后续的27个字节均是该bytes数组的内容,它包含了按照变体UTF-8格式存储(不是标准的UTF-8格式啊,具体的差别请查阅那两个参考资料,都有说明)的字符串中的字符。按照此变体格式可以将这27个字节解析为“bytecodeResearch/HelloWorld”,这27个字符实际上就是该ClassFile的完全限定名称,这是为什么呢?因为第一个常量池表项是CONSTANT_Class_info结构的,其name_index项指向的常量池表项包含了类或者接口的完全限定名称,第二个常量池表项正是第一个常量池表项中name_index项所指向的常量池表项,因此“bytecodeResearch/HelloWorld”这27个字符就是该ClassFile的完全限定名称(注:是完全限定名称的内部形式,下同)。
5.3)再来分析第三个常量池表项。第二个常量池表项后的第一个字节为0x07,由cp_type表可知,tag值为0x07的对应CONSTANT_Class结构的常量池表项。类似于对第一个常量池表项的分析,0x07后面的两个字节为name_index项,其值为0x0004,即该常量池表项指向索引为0x0004的常量池表项,且该常量池是CONSTANT_Utf8_info表的结构
5.4)下面来分析第四个常量池表项。该常量池表项的tag项值又是0x01,类似地,同第二个常量池表项的分析,该常量池表项也是一个CONSTANT_ Utf8_info表的结构,其length项值为0x0010,即其后续的bytes数组的长度为0x0010个字节。按照《JVM Spec》(2nded)中关于变体UTF-8格式的定义,可以将这16个字节解析为“java/lang/Object”,这是Java类层次结构的根类Object类的完全限定名称。
类似地,可以分析第5,6,7,8,9,10都是一个CONSTANT_ Utf8_info结构,分别表示“str_1”,“Ljava/lang/String”,“str_2”,“<clinit>”,“()V”,“Code”
5.5)…….其他常量池表项的分析原理是类似的,这里就不赘述了。
5.6)第四十八个常量池表项的解析。经过分析,第48个常量池起始于第000001f0h字节,终止于第00000202h字节。该常量池表项又是一个CONSTANT_Utf8_info表的结构,其15字节的bytes数组项的值可解析为“HelloWorld.java”。由后面的分析说明该常量池表项存储的是该类文件的SourceFile属性的信息。
好了,该Class文件的常量池部分已经解析结束了!下面开始其它部分的解析:
(6) 在常量池列表项后面的两个字节是该Java类型的access_flags项,这里为0x0021。根据access_flags表可以查到,该值是0x0020和0x0001两者的和,即该类的修饰符为ACC_PUBLIC+ACC_SUPER,前者表示该类是public类型,后者表示采用invokespecial指令特殊处理对超类的调用。具体可以查阅两本参考资料中关于JVM指令集的描述J
(7)接下来的两个字节是this_class项,它是一个对常量池表项的索引,在这里值为0x0001,即它指向1号常量池表项,而1号常量池表项是一个CONSTANT_Class_info结构,它指向2号常量池表项,2号常量池表项的值为bytecodeResearch/HelloWorld,前面提到这是该Class文件的完全路径名称的内部形式,因此this_class即指bytecodeResearch/HelloWorld。
(8) 接下来的两个字节是super_class项,它是一个对常量池表项的索引,在这里值为0x0003, 即它指向3号常量池表项,查一下上面对3号常量池表项的分析,它指向4号常量池表项,而4号常量池表项包含的值为“java/lang/Object”,即super_class的实际值为“java/lang/Object”。说明我们分析的这个Class文件的超类是java.lang.Object。
(9) 下面的两个字节是interfaces_count项,在这里的值为0x0000,这表示由该类直接实现或者由该接口所扩展的超接口的数量为0,因此该Class文件中的interfaces列表项也就不存在了。
(10)接下来的字节应该是field项的内容了。首先的两个字节是fields_count项,这里的值为0x0002,即该类声明了两个字段(变量),亦即该项之后的fields列表项的元素个数为2。由于fields列表项的类型为field_info,所以在fields_count项下面的字节是两个连续的field_info结构,下面来详细分析这两个具体的field_info结构;
10.1)第一个field_info,即第一个字段的相关信息。
10.1.1)首先的两个字节是第一个field的access_flags项,在这里的值为0x000A,查阅field_access_flags表可知该access_flags项表示的是ACC_PRIVATE+ACC_STATIC,即该字段是由private和static修饰的。
10.1.2)接下来的两个字节是name_index项,在这里的值为0x0005,即该字段的简单名称由第5个常量池表项描述的,根据上一篇博客(Java Class文件解析)的分析可知,该常量池包含的信息为str_1,即该字段的名称为str_1。
10.1.3)接下来的两个字节是descriptor_index
项,
在这里的值为0x0006,即该字段的描述符存储在
第6个常量池表项,根据上一篇博客(Java Class文件解析)的分析可知,这个字段的类型为“Ljava/lang/String”。在Class文件中,“L<classname>”表示一个类的实例,其中<classname>是这个内部形式的完全限定类名。
10.1.4)接下来的两个字节是attributes_count
项,
在这里的值为0x0000,即该字段没有附加的属性列表。因而也就不用讨论attributes[]
项了。
10.2)第二个field_info,即第二个字段的相关信息。类似地,参照第一个字段信息的分析,我们很快就可以知道该字段的access_flags项为ACC_PRIVATE+ACC_STATIC,名字为str_2,类型描述符为“Ljava/lang/String”,attributes_count
项
的值为0x0000。
(11)接下来的字节应该是method项的内容了。首先的两个字节是methods_count项,这里的值为0x0005,即该类声明了5个方法,亦即该项之后的methods列表项的元素个数为5。由于methods列表项的类型为method_info,所以在methods_count项下面的字节是5个连续的method_info结构,下面来详细分析这5个具体的method_info结构:
11.1)第1个method_info结构,即第一个方法的相关信息,如方法名、描述符(即方法的返回值及参数类型)以及一些其它信息。根据method_info表分析接下来的字节码可以得到:
11.1.1)access_flags项,值为0x0008,即给方法的访问修饰符为ACC_STATIC,它表示这是一个static方法。
11.1.2)name_index项,值为0x0008,第8号常量池表项存储的信息为<clinit>即该方法的名称为<clinit>。这是一个类与接口初始化方法,这个方法是由Java编译器编译源代码的时候产生的,Java编译器将该类的所有类变量初始化语句和所有类型的静态初始化器收集到一起,放到<clinit>方法中,该方法只能被JVM隐式地调用,专门用于把类型的静态变量设置为它们正确的初始值。
11.1.3)descriptor_index项,值为0x0009,第9号常量池表项存储的信息为()V,这表示该方法的没有参数,返回值为void。
11.1.4)attributes_count项,值为0x0001,即该方法有一个属性。查阅属性信息表的结构,如下所示:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
由这个表,我们可以知道attributes_count项后面的是这个属性的attribute_name_index
项,该项的值为0x000A,该属性的名字信息存储在第10号常量池表项里。查阅第10号常量池表项可知,该属性的名字为“Code”,然后我们查阅Code_attribute表,结构如下:
Code_attribute {
u2 attribute_name_index;
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];
}
11.1.5)Code_attribute
项,该项包含了一个Java方法,或者实例初始化方法,或者类或接口初始化方法的JVM指令和辅助信息。每个JVM的实现都必须要识别Code属性,在每个method_info结构也必须确切地有一个Code属性。下面来具体分析这个属性;
11.1.5.1) attribute_name_index项,2个字节,该项的值为0x000A,查阅第10号常量池表型包含的信息后知该属性的名字为“Code”。
11.1.5.2)attribute_length项,4个字节,值为0x00000033,这说明该属性的长度,出去初始的6个字节,还有0x33=51个字节。如果不愿意讨论接下去的51字节的话,可以直接跳过这51字节(00000226h-0000025eh字节),讨论下一个方法。
11.1.5.3)max_stack项,2个字节,值为0x0001,这表示该方法执行中任何点操作数栈上字的最大个数为1。
11.1.5.4)max_locals项,2个字节,值为0x0000,这表示该方法使用的局部变量个数为0。
11.1.5.5)code_length项,4个字节,值为0x0000000B,这表示该方法的code数组中字节的总个数为11。
11.1.5.6)code[]项,由11.1.5.5)知,该方法的code数组共占11个字节。该code[]项给出了实现该方法的JVM代码的实际字节。例如第一个指令是0x12,这是ldc指令,这个指令表示将一个常量池表项压入栈,它需要一个操作数,而它后面的一个字节是0x0B,因此这条指令加上其操作数就表示将常量池中的第0x0B号表项压入栈。接下来的一个指令是0xB3,这是putstatic指令,这条指令表示设置类中静态变量的值。它需要两个操作数indexbyte1和indexbyte2,这两个操作数均占一个字节,JVM执行putstatic执行时,会通过计算(indexbyte1<<8)|indexbyte2生成一个对常量池表项的索引,这里的参数为0x00和0x0D,运算结果是0x0D,因此这条指令的意思就是将操作数栈的当前栈顶元素赋值给0x0D号常量池表项所存储的字段(str_1),即完成对字段str_1的赋值。。同样,下面的五个字节的意思,就是将索引为0x0F的常量池表项压入操作数栈,并赋值给(0x00<<)|0x11=0x11号常量池表项中所存储的字段(str_2),即完成对字段str_2的赋值。该Code数组的最后一个字节是0xB1,这是一条不带操作数的指令return,它表示从方法中返回,返回值为void。
11.1.5.7)exception_table_length项,2个字节,值为0x0000,这表示该方法的异常处理器的个数为0。因此exception_table[ ]就没有必要讨论了。
11.1.5.8)attributes_count项,2个字节,值为0x0002,这表示该方法Code属性具有两个属性。当前由Code属性定义和使用的两个属性是LineNumberTale和LocalVariableTable属性。
11.1.5.9)attributes[ ]项,由于LineNumberTale和LocalVariableTable两个属性都包含了一些调试信息,但是两者都是可选属性,因此这里就不多讨论了。
11.2)第2个method_info结构,即第2个方法的相关信息。第2个方法是实例初始化方法<init>,这段方法在Class文件中的字节编号为:0000025fh-0000029bh字节,感兴趣的朋友请继续分析下去,原理和第一个方法的分析是一样的。
11.3)第3个method_info结构,即第3个方法的相关信息。第3个方法是该类的静态方法Hello,这段方法在Class文件中的字节编号为:0000029ch-000002dfh字节,感兴趣的朋友请继续分析下去,原理和第一个方法的分析是一样的。
11.4)第4个method_info结构,即第4个方法的相关信息。第4个方法是该类的静态方法World,这段方法在Class文件中的字节编号为:000002e0h-00000323h字节,感兴趣的朋友请继续分析下去,原理和第一个方法的分析是一样的。
11.5)第5个method_info结构,即第5个方法的相关信息。第5个方法是该类文件的入口main方法,这段方法在Class文件中的字节编号为:00000324h-00000370h字节,感兴趣的朋友请继续分析下去,原理和第一个方法的分析是一样的。
(12) attributes_count项,2个字节,该ClassFile的属性计数项,它的值为0x0001,表示在后续的attributes列表中的attributes_info表的总个数为1。
(13) 和attributes[ ]项,该ClassFile的属性列表项,这是Class文件的最后一项了!由(12)知,该列表项只有一个表项。由attribute_info表结构
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
可知,attributes_count项后面的两个字节是attribute_name_index项,它的值为0x002F,它表示对常量池编号为0x002F的表项的一个索引。这个索引表项存储的信息为”SourceFile”,即该ClassFile属性的名称为SourceFile,该属性是一个可选的定长属性,对于给定的ClassFile结构的attributes列表中不能有多于一个的SourceFile属性;查阅SourceFile_attribute表可知,下面的4个字节为attribute_length项,其值为0x00000002,它表示在该项后面还有2个字节的信息。根据SourceFile_attribute表,最后的这两个字节是sourcefile_index项,该项的值是一个对CONSTANT_Utf8_info结构的常量池表项的索引,其信息表示的是该Class文件的源文件名称。在这里值为0x0030,根据上一篇博客(Java Class文件解析)的分析,第48号常量池表项存储的信息可解析为“HelloWorld.java”,这是该Class文件的源文件名称(不包括路径)。
好了,到此为止,该Class文件的实例已经全部解析完毕,大功告成:)
posted on 2008-02-03 13:03
独孤求败 阅读(4848)
评论(33) 编辑 收藏 所属分类:
Java ByteCode