Java中的类文件结构之一:如何分析一个.class文件的二进制码内容

该文为作者原创,请转载者注明出处


以下为一个Java类--Temp4Test

package com.demo;


public class Temp4Test extends Temp3Test {


private int i = 1;

public float f;

public static String thisstr = "";

public Temp4Test(int ii, String str, float ff) {

i = ii;

thisstr = str;

f = ff;

}

public static void main(String[] args) {

Temp4Test t4 = new Temp4Test(100, "hello", 5.5f);

System.out.println(t4);

}

@Override

public String toString() {

return "[" + i + " , " + thisstr + " , " + f + "]";

}

}

其父类Temp3Test.java的实现为:

package com.demo;


public class Temp3Test implements Cloneable {

private int ii;

public static String str = "hi";


public static void main(String[] args) {

int i = 123;

}

}


所生成的Temp4Test.class的二进制文件为:


感兴趣的同学可以自己编译下就可以看到上述结果。

下面逐位分析一下二进制文件中各位的含义

a.象征是.class文件的魔数:头4个Byte,看到这4个Byte就可以基本确认为一个.class文件,固定值:0xCAFEBABE。

b.class文件版本号,第5、6个字节是次版本号,第7、8个字节为主版本号,在上述二进制码为0x00000034,代表版本号为52.0即JDK1.8.0

c.常量池,从0x0052开始(含0x0052)

    c.1 首先的两个字节为常量池中所含常量的数量,本例中即为0x0052,所代表的数为:0x0052==5*16+2-1==81(换为十进制为81个常量)

    c.2 从0x07开始为1-81个常量的二进制位表,开始位如下图所示:

    

    常量池共有14种类型的常量,如下图所示,截图来源于《深入理解Java虚拟机》

                       

    每一个常量,第一位均为该常量类型,即上述表中14项之一,以第一个常量为例0x07表示类或接口的符号引用,即CONSTANT_Class_info,此类型结构为:

    

因此第一个常量即为0x070002,0x0002是一个索引,表示指向第二个常量,第二个常量类型为0x01,为下图所示二进制位

    

0x01表示(上面有表)CONSTANT_Utf8_info,即字符串,CONSTANT_Utf8_info类型的常量结构为下图:

    

可知,第二个常量对应的二进制码为:0x010012(共计1*16+2=18位)636F6D2F64656D6F2F54656D703454657374,后面的字符串所对应的文本为:com/demo/Temp4Test,以此类推,可以推出上述二进制文件中的所有常量池常量,以下是我手写的一个推导图,有点粗漏,^_^



以下为14种常量项的结构总表



d.访问标志,紧挨着常量池的两个字节,含义如下表:


在上述示例中为:


0x0021==0x0001|0x0020表示是一个由用户定义的public的类

e.接下来的二进制表示该类的继承关系,共有三项内容:

    e.1 类索引,两个字节,常量池中的对本类的描述

    e.2 父类索引,两个字节,常量池中对父类的描述

    e.3 接口索引,头两个字节表示接口数,然后,紧跟进接口列表

    本例中的二进制码为:

    

    0x0001表示常量池中第一个常量为类索引,0x0003为表示常量池中第三个常量为父类索引,0x0000表示实现的接口个数为0个

f.接着的二进制表示字段表,头两位为个数,从上图可以看出,有三个字段(0x0003),每个字段规则如下图

    

    f.1 access_flags的取值如下图

    

    f.2 name_index的含义,同上,映射至常量池中的常量索引

    f.3 descriptor_index,描述符,表示该变量的类型,此处插一下描述符的表示规则:

        f.3.* 描述符中的字符含义如下:

        

        f.3.** 表示数组时,每一个维度前用“[”表示

        f.3.*** 描述方法,先参后返回值

        下面举一个描述符的例子:

        上面的例子中,Temp4Test.java类中的构造函数的返回值及参数描述为:(ILjava/lang/String;F)V

    本例中的字段表,三个字段分别为:

    0x0002(private)00050006(查看第五常量、第六常量)0000

    0x0001(public)00070008(查看第七常量、第八常量)0000

    0x0009(0x0001|0x0008 public static)0009000A(查看第九常量、第十常量)0000

g.接着的二进制表示方法表,头两位为个数,从上图可以看出,有四个字段(0x0004),每个方法表述规则如下图

    

    g.1 access_flags的含义与字段表有所差别,具体如下图:

    

    其他的name_index、descriptor_index同字段表

    g.2 在本例中,方法1-方法4的二进制块如下图:

    方法1:

    

    两个红色竖线中间的部分

    方法2:

    

    方法3:

    

    方法4:

    

    g.3 下面找一个方法来说明一下方法的二进制怎么看,以方法2为例

    0x0001 -- public

    0x0014 -- 第二十个常量,方法名

    0x0015 -- 第二十一个常常,方法描述

    0x0001 -- 含有一个属性表

    后面一直到方法结束均为属性表的二进制内容

    g.4 属性是一个特别复杂的二进制规则,之后会写一篇文章专门说述一下属性表的读法,在此不一一展开描述,只说一下当前例子方法(即方法2)中的属性表读取规则

        0x000D -- 第十三个常量,查常量表,可知,表示该属性为Code,Code的属性规则为:

        

        attribute_name_index,即0x000D,表示Code

    attribute_length,表示该属性所占字节数(不包括attribute_name_index和attribute_length),方法2中为0x00000074==7*16+4==116    

        max_stack,最大堆栈数为0x0002

        max_locals,最大临时变量数为0x0004

        code_length,字节码数量为0x00000018 == 16+8 == 24Byte

        code,如下图:

        

        字节码的阅读不在该文中讨论

        exception_table_length,0x0000,没有

        attributes_count,0x0002,下面内嵌了两个属性表,内嵌的属性表的读取方式同上面所述,和外层的读取规则是一样的,只是属性不为Code了。因属性表的全部分类并未罗列,也就不再读了从0x0012开始直到方法2结束,均为这两个属性表的二进制码。

h.方法表的二进制码结束后,对于本例来讲,基本二进制码就进入最后了,还剩一点是SourceFile的属性,读法,如下:

        

    对应的二进制为:

    0x0050,第八十位常量--SourceFile

    0x00000002,后面还有两位,这个值在SourceFile类型中是定值,只有2位,原因就是上图的规则定义

    0x0051,第八十一位常量


结语:终于把类的二进制文件读完了,当然,上述例子比较简单,但麻雀虽小五脏俱全,复杂的文件只是多了一些其他类别,读法是和上述例子一致的。上面有一些内容将在后续文章中继续详述,会有一篇文章专门讲述文本化的常量池是如何读的,还会有一篇文章讲述属性表,再有一篇文章详述字节码,文中的截图来源于《深入理解Java虚拟机》,在此向作者致以最深的敬意,同时,图片若侵权,请联系我,将第一时间删除。

猜你喜欢

转载自blog.csdn.net/kcstrong/article/details/79460262