Java Class文件解析实例

来源:互联网 发布:高意峰 知乎 高意伟 编辑:程序博客网 时间:2024/06/05 08:33

最近在学习ASM,顺便把JavaClass文件内容看了一遍。下面显示出自己的一些学习成果,如果有错误,欢迎大家指出纠正。在Java Class文件中的各项是按照一定的包含关系和次序关系存储的,因此Class文件可以从头到尾地被解析为各个项。

对于Java Class文件的ClassFile表的格式如下:

ClassFile{
    u4  magic;
    u2  minor_version;
    u2  major_version;
    u2  constant_pool_count;
    cp_info constant_pool[];
    u2  access_flags;
    u2  this_class;
    u2  super_class;
    u2  interfaces_count;
    u2  interfaces[];
    u2  fields_count;
    field_info  fields;
    u2  methods_count;
    method_info methods;
    u2  attributes_count;
    attribute_info  attributes;
}

其中u2代表2个字节,无符号类型。同理u4代表4个字节,无符号类型。

下面是自己定义的一个分析例子,Hello ASM例子的源代码如下:

package test;public class HelloASM{    private static String HELLO = "Hello";        private static String sayHelloASM(String result){        return HELLO + " " + result;    }        public static void main(String[] args)    {        sayHelloASM("ASM");    }}

本人的开发工具是Eclipse3.5.1版本,Eclipse SDK自带的JDT工具内置了增量式Java编译器,这个编译器与javac完全兼容。HelloASM.java文件经过Eclipse编译器编译之后,得到一个HelloASM.class文件,得到的内容如下:

00000000h: CA FE BA BE 00 00 00 32  00 37 07 00 02 01 00 0D   //魔数 版本号  常量数  第一号变量池常项class,第二号变量池常项utf800000010h: 74 65 73 74 2F 48 65 6C  6C 6F 41 53 4D 07 00 04   //第三号变量池常项class00000020h: 01 00 10 6A 61 76 61 2F  6C 61 6E 67 2F 4F 62 6A   //第4号变量池常项utf800000030h: 65 63 74 01 00 05 48 45  4C 4C 4F 01 00 12 4C 6A   //第5号变量池常项utf8,第6号变量池常项utf800000040h: 61 76 61 2F 6C 61 6E 67  2F 53 74 72 69 6E 67 3B00000050h: 01 00 08 3C 63 6C 69 6E  69 74 3E 01 00 03 28 29   //第7号变量池常项utf8,第8号变量池常项utf8,00000060h: 56 01 00 04 43 6F 64 65  08 00 0B 01 00 05 48 65   //第9号变量池常项utf8,第11号变量池常项utf800000070h: 6C 6C 6F 09 00 01 00 0D  0C 00 05 00 06 01 00 0F   //第12号变量池常项00000080h: 4C 69 6E 65 4E 75 6D 62  65 72 54 61 62 6C 65 01   //第15号变量池常项00000090h: 00 12 4C 6F 63 61 6C 56  61 72 69 61 62 6C 65 54   000000a0h: 61 62 6C 65 01 00 06 3C  69 6E 69 74 3E 0A 00 03   //第16号变量池常项000000b0h: 00 12 0C 00 10 00 08 01  00 04 74 68 69 73 01 00   //000000c0h: 0F 4C 74 65 73 74 2F 48  65 6C 6C 6F 41 53 4D 3B000000d0h: 01 00 0B 73 61 79 48 65  6C 6C 6F 41 53 4D 01 00   //第21号变量池常项000000e0h: 26 28 4C 6A 61 76 61 2F  6C 61 6E 67 2F 53 74 72000000f0h: 69 6E 67 3B 29 4C 6A 61  76 61 2F 6C 61 6E 67 2F00000100h: 53 74 72 69 6E 67 3B 07  00 18 01 00 17 6A 61 7600000110h: 61 2F 6C 61 6E 67 2F 53  74 72 69 6E 67 42 75 6900000120h: 6C 64 65 72 0A 00 1A 00  1C 07 00 1B 01 00 10 6A00000130h: 61 76 61 2F 6C 61 6E 67  2F 53 74 72 69 6E 67 0C00000140h: 00 1D 00 1E 01 00 07 76  61 6C 75 65 4F 66 01 0000000150h: 26 28 4C 6A 61 76 61 2F  6C 61 6E 67 2F 4F 62 6A00000160h: 65 63 74 3B 29 4C 6A 61  76 61 2F 6C 61 6E 67 2F00000170h: 53 74 72 69 6E 67 3B 0A  00 17 00 20 0C 00 10 00   //第31号变量池常项00000180h: 21 01 00 15 28 4C 6A 61  76 61 2F 6C 61 6E 67 2F00000190h: 53 74 72 69 6E 67 3B 29  56 08 00 23 01 00 01 20000001a0h: 0A 00 17 00 25 0C 00 26  00 27 01 00 06 61 70 70000001b0h: 65 6E 64 01 00 2D 28 4C  6A 61 76 61 2F 6C 61 6E000001c0h: 67 2F 53 74 72 69 6E 67  3B 29 4C 6A 61 76 61 2F000001d0h: 6C 61 6E 67 2F 53 74 72  69 6E 67 42 75 69 6C 64000001e0h: 65 72 3B 0A 00 17 00 29  0C 00 2A 00 2B 01 00 08   //第41号变量池常项000001f0h: 74 6F 53 74 72 69 6E 67  01 00 14 28 29 4C 6A 6100000200h: 76 61 2F 6C 61 6E 67 2F  53 74 72 69 6E 67 3B 0100000210h: 00 06 72 65 73 75 6C 74  01 00 04 6D 61 69 6E 0100000220h: 00 16 28 5B 4C 6A 61 76  61 2F 6C 61 6E 67 2F 5300000230h: 74 72 69 6E 67 3B 29 56  08 00 30 01 00 03 41 5300000240h: 4D 0A 00 01 00 32 0C 00  15 00 16 01 00 04 61 7200000250h: 67 73 01 00 13 5B 4C 6A  61 76 61 2F 6C 61 6E 67   //第51号变量池常项00000260h: 2F 53 74 72 69 6E 67 3B  01 00 0A 53 6F 75 72 6300000270h: 65 46 69 6C 65 01 00 0D  48 65 6C 6C 6F 41 53 4D   //第54号变量池常项00000280h: 2E 6A 61 76 61 00 21 00  01 00 03 00 00 00 01 00   // 接下来的是access_flag,this_class,super_class以及第一个Fieldinfo00000290h: 0A 00 05 00 06 00 00 00  04 00 08 00 07 00 08 00   //第一个method_info000002a0h: 01 00 09 00 00 00 2A 00  01 00 00 00 00 00 06 12000002b0h: 0A B3 00 0C B1 00 00 00  02 00 0E 00 00 00 0A 00   000002c0h: 02 00 00 00 05 00 05 00  03 00 0F 00 00 00 02 00000002d0h: 00 00 01 00 10 00 08 00  01 00 09 00 00 00 2F 00     //第二个method_info000002e0h: 01 00 01 00 00 00 05 2A  B7 00 11 B1 00 00 00 02000002f0h: 00 0E 00 00 00 06 00 01  00 00 00 03 00 0F 00 0000000300h: 00 0C 00 01 00 00 00 05  00 13 00 14 00 00 00 0A    //第三个method_info00000310h: 00 15 00 16 00 01 00 09  00 00 00 44 00 03 00 0100000320h: 00 00 00 1A BB 00 17 59  B2 00 0C B8 00 19 B7 0000000330h: 1F 12 22 B6 00 24 2A B6  00 24 B6 00 28 B0 00 0000000340h: 00 02 00 0E 00 00 00 06  00 01 00 00 00 08 00 0F00000350h: 00 00 00 0C 00 01 00 00  00 1A 00 2C 00 06 00 0000000360h: 00 09 00 2D 00 2E 00 01  00 09 00 00 00 35 00 01    //第4个method_info00000370h: 00 01 00 00 00 07 12 2F  B8 00 31 57 B1 00 00 0000000380h: 02 00 0E 00 00 00 0A 00  02 00 00 00 0D 00 06 0000000390h: 0E 00 0F 00 00 00 0C 00  01 00 00 00 07 00 33 00000003a0h: 34 00 00 00 01 00 35 00  00 00 02 00 36             //类的结尾部分


:该Class的内容是经过解析的,即将.class文件的二进制字节流转换为16进制的数字字符流形式。

下面开始解析这个Class文件

1)对于没一个Class文件的前4个字节0xCAFEBABEmagic项的内容,该Magic的内容是为了方便虚拟机识别文件是否是正确的Java Class格式的文件。

2)接下来的2个字节0x0000是minor_version项的内容,即该Class文件的次版本号0。

3)接下来的2个字节0x0032是major_version项的内容,即该Class文件的主版本号为50。这个主版本号对应于J2SE6.0的编译器的编译结果。如果是使用J2SE5.0的编译器编译的结果应该是49。

4) 接下来的2个字节0x0037表示的constant_pool_count项的内容,即该Class文件中具有的常量的数量是3*16+7-1=54个。因为Constant_pool列表中没有索引值为0的入口,所以实际的常量池的数量是constant_pool_count项的内容-1。

5)接下来的是描述这54个常量列表项。常量池列表项的长度是可变的,这是因为不同的常量池表项的格式是不一样的。每个常量池表项的具体格式是要根据其tag项(即该常量池表项的第一个字节)来决定。

5.1)在Java Class文件中的常量池tag项如下:

常量池tag项表{
        入口类型                       标志值                 描述
        CONSTANT_UTF8                   1                 UTF-8编码的Unicode字符
        CONSTANT_Integer                3                 int类型的字面值
        CONSTANT_Float                  4                 float类型的字面值
        CONSTANT_Long                   5                 long类型的字面值
        CONSTANT_Double                 6                 double类型的字面值
        CONSTANT_Class                  7                 对于一个类或者借口的符号引用
        CONSTANT_String                 8                 String类型的字面值
        CONSTANT_Fieldref               9                 对于一个字段的符号引用
        CONSTANT_Methodref              10                对一个类中声明的方法的符号引用
        CONSTANT_InterfaceMethodref     11                对一个借口中声明的方法的符号引用
        CONSTANT_NameAndType            12                对一个字段或者方法的部分符号应用
}

根据上表,从而可以看出,接下来的一个字节是0x07,tag值为0x07的对应CONSTANT_Class结构的常量池表项。

CONSTANT_CLASS_Info对应的表项结构如下:

Constant_class_info{

    u1  tag;
    u2  name_index;
}

根据《JVM规范》中对此表的说明可知,1个字节的tag项就是刚才读取的值0x07,而后的2个字节的name_index项表示对一个CONSTANT_Utf8_info表的索引,该索引项包含了类或者接口的完全限定名称。对于HelloASM.class这个类来说,第一个常量池表项就是一个CONSTANT_Class_info表的结构,其tag项的值是0x07,其name_index项的值是0x0002,即该常量池表项指向第2个常量池表项中的值。

5.2)再来分析第二个常量池表项。其tag项值为0x01,根据常量池tag项表可知,该常量池表项是一个CONSTANT_ Utf8_info表的结构,然后我们查阅CONSTANT_ Utf8_info表,该表结构如下:

CONSTANT_Utf8_info {
    u1  tag;
    u2  length;
    u1  bytes[length];
}

根据《JVM规范》(2nded)中对此表的说明可知,1个字节的tag项的值就是0x01,2个字节的length项给出了后续的bytes数组的长度(字节数),在这里为0x000D,即其后续的13个字节均是该bytes数组的内容,它包含了按照变体UTF-8格式存储(不是标准的UTF-8格式啊,具体的差别请查阅那两个参考资料,都有说明)的字符串中的字符。按照此变体格式可以将这13个字节解析为“test/HelloASM”,这13个字符实际上就是该ClassFile的完全限定名称,这是为什么呢?因为第一个常量池表项是CONSTANT_Class_info结构的,其name_index项指向的常量池表项包含了类或者接口的完全限定名称,第二个常量池表项正是第一个常量池表项中name_index项所指向的常量池表项,因此“test/HelloASM”这13个字符就是该ClassFile的完全限定名称。

5.3)再来分析第三个常量池表项。第二个常量池表项后的第一个字节为0x07,由常量池tag项表可知,tag值为0x07的对应CONSTANT_Class结构的常量池表项。类似于对第一个常量池表项的分析,0x07后面的两个字节为name_index项,其值为0x0004,即该常量池表项指向索引为0x0004的常量池表项,且该常量池是CONSTANT_Utf8_info表的结构。

5.4)下面来分析第四个常量池表项。该常量池表项的tag项值又是0x01,类似地,同第二个常量池表项的分析,该常量池表项也是一个CONSTANT_ Utf8_info表的结构,其length项值为0x0010,即其后续的bytes数组的长度为0x0010个字节。按照《JVM规范》(2nded)中关于变体UTF-8格式的定义,可以将这16个字节解析为“java/lang/Object”,这是Java类层次结构的根类Object类的完全限定名称。

5.5)其他常量池表项的分析原理是类似的,这里就不赘述了。下面列出其他的表项结构:

CONSTANT_Double_info {
    u1  tag;
    u4  bytes;

}

CONSTANT_String_info {
    u1  tag;
    u2  string_index;   指向第几个常量池

}

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_InterfaceMethodref_info {
    u1  tag;
    u2  class_index;

    u2  name_and_type_index;

}

CONSTANT_NameAndType_info {
    u1  tag;
    u2  name_index;

    u2  descriptor_index;

}

CONSTANT_Integer_info {
    u1  tag;
    u4  bytes;

}

CONSTANT_Float_info {
    u1  tag;
    u4  bytes;

}

CONSTANT_Long_info {
    u1  tag;
    u4  bytes;

}

6)在常量池列表项后面的两个字节是该Java类型的access_flags项,这里为0x0021。根据access_flags表可以查到,该值是0x0020和0x0001两者的和,即该类的修饰符为ACC_PUBLIC+ACC_SUPER,前者表示该类是public类型,后者表示采用invokespecial指令特殊处理对超类的调用。具体可以查阅两本参考资料中关于JVM指令集的描述

7) 接下来的两个字节是this_class项,它是一个对常量池表项的索引,在这里值为0x0001,即它指向1号常量池表项,而1号常量池表项是一个CONSTANT_Class_info结构,它指向2号常量池表项,2号常量池表项的值为test/HelloASM,前面提到这是该Class文件的完全路径名称的内部形式,因此this_class即指test/HelloASM。

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项,这里的值为0x0001,即该类声明了两个字段(变量),亦即该项之后的fields列表项的元素个数为1。由于fields列表项的类型为field_info,所以在fields_count项下面的字节是1个的field_info结构,下面来详细分析具体的field_info结构;
field_info{
u2access_flags;
u2name_index;
u2descriptor_index;
u2attributes_count;
attribute_infoattributes;
}

10.1) 首先的两个字节是第一个field的access_flags项,在这里的值为0x000A,查阅field_access_flags表可知该access_flags项表示的是ACC_PRIVATE+ACC_STATIC,即该字段是由private和static修饰的。

10.2) 接下来的两个字节是name_index项,在这里的值为0x0005,即该字段的简单名称由第5个常量池表项描述的,根据分析可知,该常量池包含的信息为HELLO,即该字段的名称为HELLO。

10.3) 接下来的两个字节是descriptor_index项,在这里的值为0x0006,即该字段的描述符存储在第6个常量池表项,分析可知,这个字段的类型为“Ljava/lang/String”。

10.4) 接下来的两个字节是attributes_count项,在这里的值为0x0000,即该字段没有附加的属性列表。因而也就不用讨论attributes[]项了。

11) 接下来的字节应该是method项的内容了。首先的两个字节是methods_count项,这里的值为0x0004,即该类声明了4个方法,亦即该项之后的methods列表项的元素个数为4。由于methods列表项的类型为method_info,所以在methods_count项下面的字节是4个连续的method_info结构,下面来详细分析这4个具体的method_info结构,首先介绍Method_info的具体结构:
method_info{
u2access_flags;
u2name_index;
u2descriptor_index;
u2attributes_count;
attribute_infoattributes;
}

11.1) access_flags项,值为0x0008,即给方法的访问修饰符为ACC_STATIC,它表示这是一个static方法。

11.2) name_index项,值为0x0007,第7号常量池表项存储的信息为<clinit>即该方法的名称为<clinit>。这是一个类与接口初始化方法,这个方法是由Java编译器编译源代码的时候产生的,Java编译器将该类的所有类变量初始化语句和所有类型的静态初始化器收集到一起,放到<clinit>方法中,该方法只能被JVM隐式地调用,专门用于把类型的静态变量设置为它们正确的初始值。

11.3) descriptor_index项,值为0x0008,第8号常量池表项存储的信息为()V,这表示该方法的没有参数,返回值为void。

11.4) attributes_count项,值为0x0001,即该方法有一个属性。查阅属性信息表的结构,如下所示:
attribute_info {

u2attribute_name_index;
u4attribute_length;
u1info[attribute_length];
}

11.5) 由这个表,我们可以知道attributes_count项后面的是这个属性的attribute_name_index项,该项的值为0x0009,该属性的名字信息存储在第9号常量池表项里。查阅第9号常量池表项可知,该属性的名字为“Code”,然后我们查阅Code_attribute表,结构如下:
Code_attribute {
u2attribute_name_index;
u4attribute_length;
u2max_stack;
u2max_locals;
u4code_length;
u1code[code_length];
u2exception_table_length;
exception_table[exception_table_length];
{u2start_pc;
u2end_pc;
u2handler_pc;
u2catch_type;
}
u2 attributes_count;
attribute_info attributes[attributes_count];
}

11.6) Code_attribute项,该项包含了一个Java方法,或者实例初始化方法,或者类或接口初始化方法的JVM指令和辅助信息。每个JVM的实现都必须要识别Code属性,在每个method_info结构也必须确切地有一个Code属性。下面来具体分析这个属性;

11.7) attribute_name_index项,2个字节,该项的值为0x0009,查阅第9号常量池表型包含的信息后知该属性的名字为“Code”。

11.8) attribute_length项,4个字节,值为0x0000002A,这说明该属性的长度,出去初始的6个字节,还有0x2A=42个字节。

11.9) max_stack项,2个字节,值为0x0001,这表示该方法执行中任何点操作数栈上字的最大个数为1。

11.10) max_locals项,2个字节,值为0x0000,这表示该方法使用的局部变量个数为0。

11.11) code_length项,4个字节,值为0x00000006,这表示该方法的code数组中字节的总个数为6。

11.12) code[]项,该方法的code数组共占6个字节。该code[]项给出了实现该方法的JVM代码的实际字节。例如第一个指令是0x12,这是ldc指令,这个指令表示将一个常量池表项压入栈,它需要一个操作数,而它后面的一个字节是0x0A,因此这条指令加上其操作数就表示将常量池中的第0x0A号表项压入栈。接下来的一个指令是0xB3,这是putstatic指令,这条指令表示设置类中静态变量的值。它需要两个操作数indexbyte1和indexbyte2,这两个操作数均占一个字节,JVM执行putstatic执行时,会通过计算(indexbyte1<<8)|indexbyte2生成一个对常量池表项的索引,这里的参数为0x00和0x0C,运算结果是0x0C,因此这条指令的意思就是将操作数栈的当前栈顶元素赋值给0x0C号常量池表项所存储的字段(HELLO),即完成对字段HELLO的赋值。。该Code数组的最后一个字节是0xB1,这是一条不带操作数的指令return,它表示从方法中返回,返回值为void。

11.13) exception_table_length项,2个字节,值为0x0000,这表示该方法的异常处理器的个数为0。因此exception_table[ ]就没有必要讨论了。

11.14) attributes_count项,2个字节,值为0x0002,这表示该方法Code属性具有两个属性。当前由Code属性定义和使用的两个属性是LineNumberTable和LocalVariableTable属性。LineNumberTable是一个表示方法的行号与字节码的映射关系。而LocalVariableTable属性表示方法的局部变量的描述。接下来就是attributes[ ]项,由于LineNumberTale和LocalVariableTable两个属性都包含了一些调试信息,但是两者都是可选属性。

下面的第2,3,4个方法的描述跟第一个方法的描述是一样的,所以下面不加讲述了。

12) 接下来是类得结尾部分attributes_count项,2个字节,该ClassFile的属性计数项,它的值为0x0001,表示在后续的attributes列表中的attributes_info表的总个数为1

13)和attributes[ ]项,该ClassFile的属性列表项,这是Class文件的最后一项了,该列表项只有一个表项。由attribute_info表结构
attribute_info {
                   u2 attribute_name_index;
                   u4 attribute_length;
                   u1 info[attribute_length];
}

14)可知,attributes_count项后面的两个字节是attribute_name_index项,它的值为0x0035,它表示对常量池编号为0x0035的表项的一个索引。这个索引表项存储的信息为”SourceFile”,即该ClassFile属性的名称为SourceFile,该属性是一个可选的定长属性,对于给定的ClassFile结构的attributes列表中不能有多于一个的SourceFile属性;查阅SourceFile_attribute表可知,下面的4个字节为attribute_length项,其值为0x00000002,它表示在该项后面还有2个字节的信息。根据SourceFile_attribute表,最后的这两个字节是sourcefile_index项,该项的值是一个对CONSTANT_Utf8_info结构的常量池表项的索引,其信息表示的是该Class文件的源文件名称。在这里值为0x0036,根据上一篇博客(Java Class文件解析)的分析,第48号常量池表项存储的信息可解析为“HelloASM.java”,这是该Class文件的源文件名称(不包括路径)。

15)好了,到此为止,该Class文件的实例已经全部解析完毕.

参考《深入Java虚拟机》

原创粉丝点击