你的位置:首页 > Java教程

[Java教程]实例探索Class文件


 class文件是指以.class为文件后缀的Java虚拟机可装载文件。无论该class文件是在linux上进行编译的,还是在windows环境下编译的,无论虚拟机是在何种平台下实现和运行的,class文件使得Java虚拟机可以正确的读取、解释所有的class文件。 在分析和研究class文件之前,先提出有一些问题:

1.类/接口(class文件也可能定义的是接口,所以还是不要理解为类文件为好)内有哪些内容?

2.以上内容分别保存在class文件的什么地方?

3.这些内容在加载过程中又如何被读取和解析?

4.这些内容加载后又会被解析成为什么样的数据结构保存在虚拟机中?

5.这些数据结构在虚拟机的运行过程中又是如何被使用的?

扩展问题:

6.如何防止class文件被劫持?

7.如何防止class文件被反编译?

 

class文件的组织结构定义如下:

ClassFile{magic            u4,minor_version        u2,major_version        u2,constant_pool_count      u2,constant_pool        cp_info*constant_pool_count,access_flags         u2,this_class          u2,super_class         u2,interface_count       u2,interfaces          u2 * interface_count,fields_count         u2,fields           field_info * fields_count,methods_count        u2,methods           method_info * methods_count,attributes_count       u2,attributes          attributes_info * attributes_count}

以如下程序为例,对生成的class文件进行分析:

 1 //TestInterface.java 2 public interface TestInterface { 3   public void interface_method(); 4 } 5  6 //TestClass.java 7 public class TestClass implements TestInterface{ 8   private int private_global = 3; 9   public int public_global;10   private static final int sfi = 127;11   public static final String sfs = "test strings";12   private StringBuilder sb;13   14   public void method_1(){15     private_global = public_global * 2;16     sb.append(private_global);17   }18   19   public void method_2(int pub){20     public_global = pub;21   }22 23   public void method_2(int pub, boolean flag){24     int tmp = 5;25     public_global = pub * 2 + tmp;26   }27   28   @Override29   public void interface_method() {30     method_1();31   }32   33 }

1.magic(魔数) 值为0xcafebabe,没有特别的意义,放在文件头并选取用来标记改文件是一个class文件。

 

2.minor_version/major_version(次版本号和主版本号)

次版本号和主版本号分别为0x0000和0x0032(50),即主版本号位50,次版本号为0

 

3.constant_pool_count/constant_pool(常量池数量和常量池)

常量池保存了文件中类或接口相关的一切常量,字面常量(直接量),如文字字符串、final变量值,以及符号引用,如类或接口的全限定名、方法或字段的简单名称和描述符

其中,全限定名用以在当前命名空间内唯一标志类或接口,在java语言中如java.lang.Object,在class文件中,会将'.'用'/'取代,即表示为java/lang/Object 简单名称就是简单的方法名或变量名的字符串,如java.lang.Object的成员方法wait()的简单名称为"wait"。

而只有简单名称是无法唯一确定调用的方法是哪一个,由于Java语言的特性,方法可能被重写或重载, 所以还需要根据方法的返回值、参数数量、类型、顺序来确定一个方法描述符来唯一标志该方法,字段的描述符则简单得多,只需要给出字段的类型 描述符让我们联想起PE/ELF文件的函数签名,它由上下文无关语法定义:

FieldDescriptor:      FieldTypeComponentType:      FieldTypeFieldType:      BaseType      ObjectType      ArrayTypeBaseType:      B      C      D      F      I      J      S      ZObjectType:      L<classname>;ArrayType:      [ComponentTypeMethodDescriptor:      (ParameterDescriptor*) ReturnDescriptorParameterDescriptor:      FieldTypeReturnDescriptor:      FieldType      V

其终结符号如下:

以深入java虚拟机上的示例作为参考:

     

下面看class文件内常量池部分: 首先是常量池数:即(0x35)53个常量池

Java虚拟机将常量池组织成为列表(可以看做是一个常量池的数组)的形式,常量池内容可能指向其他常量池,并且class文件中其他部分内容也可能指向常量池入口,这些常量池通过该常量池在常量池列表中的索引来定位,常量池列表的0号常量池其实是空的,作为常量池的NULL引用,即常量池列表的第一项实际上是1号常量池,常量池列表实际上只有constant_pool_count - 1个常量池项。 随后是常量池列表,常量池的结构如下:

cp_info{tag,info}

常量池的固定第一个字节是常量值标签,用来描述该常量池保存内容的类型,常量池标志和含义如下:

 

根据常量池标志tag的不同,info有不同的组织方式:

(1).CONSTANT_Utf8结构:

 

(可以看出length由2个字节表示,最大长度就应该是65536字节)

该类型是一个长度可变(长度为length)的常量字符串表,用来存储以下类型的字符串:

  • 文字字符串,如String对象的内容
  • 当前类或接口的全限定名
  • 当前类的超类的全限定名
  • 当前类或接口的父接口的全限定名
  • 字段的简单名称或描述符
  • 方法的简单名称或描述符
  • 引用类或接口的全限定名
  • 引用字段的简单名称和描述符
  • 引用方法的简单名称和描述符

字符的存放: 
对于0x0001-0x007f的字符将使用一个字节(该字节的0-6位,第7位为0)存放 
对于0x080-0x07ff的字符将使用两个字节(依次高字节的0-5位和低字节的0-4位,剩余位分别为10、110)存放 

 

对于0x0800-0xffff的字符将使用3个字节(依次为高字节的0-5中间字节的0-5,和低字节的0-3位,剩余位分别为10、10、1110)存放。

(2).CONSTANT_Integer结构:

TypeNameCount
u4bytes1

 

 

按高位在前的格式存储int型数据

(3).CONSTANT_Float结构:

TypeNameCount
u4bytes1

 

 

按高位在前的格式存储float型数据 
(4).CONSTANT_Doube结构:

TypeNameCount
u8bytes1

 

 

按高位在前的格式存储double型数据 
(5).CONSTANT_Long结构:

TypeNameCount
u8bytes1

 

 

按高位在前的格式存储long型数据 
(6).CONSTANT_Class结构:

TypeNameCount
u2name_index1

 

 

name_index为类或者接口符号引用的CONSTANT_Utf8常量池的索引(全限定名) 
(7).CONSTANT_String结构:

TypeNameCount
u2sring_index1

 

 

string_index为字符串的CONSTANT_Utf8常量池的索引 
(8).CONSTANT_Fieldref结构: 
描述了指向字段的符号引用,其内容分两项表示,一项为被引用字段所在类或接口的CONSTANT_Class常量池索引,一项为字段的简单名称和描述符,指向一个CONSTANT_NameAndType常量池

TypeNameCount
u2class_index1
u2name_and_type_index1

 

 

 

(9).CONSTANT_Methodref结构: 
与CONSTANT_Fieldref类似,描述了指向类中声明的方法的符号引用,其内容分两项表示,一项为被引用方法所在类的CONSTANT_Class常量池索引,一项为方法的简单名称和描述符,指向一个CONSTANT_NameAndType常量池

TypeNameCount
u2class_index1
u2name_and_type_index1

 

 

 

(10).CONSTANT_InterfaceMethodref结构: 
与CONSTANT_Methodref类似,描述了指向接口中声明的方法的符号引用,其内容分两项表示,一项为被引用方法所在接口的CONSTANT_Class常量池索引,一项为方法的简单名称和描述符,指向一个CONSTANT_NameAndType常量池

TypeNameCount
u2class_index1
u2name_and_type_index1

 

 

 

(11).CONSTANT_NameAndType结构: 
可以预见,该常量池提供了所引用字段或方法的简单名称和常量池入口

TypeNameCount
u2class_index1
u2name_and_type_index1

 

 

 

注意区分class_index指向的是对应类的常量池,该CONSTANT_Class常量池指向一个全限定名的CONSTANT_Utf8字符串常量池 
常量池部分的解析可以参考http://note.youdao.com/share/?id=3c1f3fac45837f95cc87fa6694a25b84&type=note 

4.access_flags 
该项2字节标志了所定义类或接口的类型信息

 

该文件中access_flags为0x0021 ,可见该类是public super类型。

5.this_class(当前类) 
该项2字节标志了所定义类或接口的CONSTANT_Class常量池索引,最终指向全限定名”TestClass” 

6.super_class(超类) 
该项2字节标志了所定义类的超类的CONSTANT_Class常量池索引,最终指向全限定名”java/lang/Object”

7.interfaces_count/interfaces(接口数和接口) 
首先2字节是在该类中直接实现或扩展的接口数,后面紧随若干个(接口数)2字节,代表所直接实现或扩展的接口的CONSTANT_Class常量池的索引 

这里只实现了一个接口,就是5号常量池,即全限定名”TestInterface”所定义的接口

8.fields_count/fields(字段数和字段) 
fields_count是类变量(静态变量)和实例变量(非静态变量)的字段数总和,与constant_pool组织形式类似,后面是fields_count个field_info,需要注意的是,当前类的字段不会包含其超类或父接口中继承的字段,也会包含在Java源文件中没有但是在编译时添加的一些字段。field_info结构如下:

field_info{access_flags       u2,name_index        u2,descriptor_index     u2,attributes_count     u2,attributes        attributes_info * attributes_cout }

(1).字段的accesss_flags与描述当前类的access_flags不同:

   

类中声明的字段,只能拥有ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志中的一个。ACC_FINAL 
和ACC_VOLATILE 不能同时设置。所有接口中声明的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL 这三种标志。 
(2).name_index为该字段的简单名称的CONSTANT_Utf8常量池索引 
(3).descriptor_index为该字段的描述符的CONSTANT_Utf8常量池索引 
(4).attributes_count和attributes是attributes_count个attribute_info结构所表述的属性集合。在字段域出现的属性有ConstantValue(final常量)、Deprecated(被禁用的指示符)、Synthetic(编译器产生的指示符)

属性出现在ClassFile、field_info、method_info、Code_attribute中。所有Java虚拟机必须能够识别Code、ConstantValue、Exception。对于能够正常实现Java/Java2平台类库的虚拟机必须能够识别InnerClass和Synthetic属性。

attribute_info的结构如下:

attribute_info{attribute_name_index      u2,attribute_length        u4,info              u1,}

attribute_name_index为描述属性的字符串名称(即上述列出属性名)的CONSTANT_Utf8常量池索引, 
attribute_length为后面属性内容的长度 
这里先介绍将字段可能用到的ConstantValue、Deprecated和Synthetic属性 
(1).ConstantValue

TypeNameCount
u2constantvalue_index1

 

 

该属性用于描述值为常量的字段,并且在包含该属性的字段其access_flag必须为ACC_STATIC,以表明这是一个静态常量。 
constantvalue_index指向提供常量值的常量池索引(此外,ConstantValue对应的属性的attribute_length始终为2) 
(2).Deprecated 
被@Deprecated所注释的字段、方法或类型,表示虽然该字段、方法或类型仍然存在,但是不建议使用,其在未来的版本中可能会被移除 
Deprecated对应的属性的attribute_length值始终为0 
(3).Synthetic 
用来指明为编译器所产生的字段、方法或类型 
同样,这是一个固定长度属性,其 
对应的属性的attribute_length值始终为0

class文件field域解析: 
首先由开头两个字节看出有5个field_info 

field1: 
access_flag为ACC_PRIVATE,标志其为private类型 
name_index为0x0007,指向7号常量池,即简单名称为”private_global” 
descriptor_index为0x0008,指向8号常量池,即描述符为”I” 
attributes_count为0,即没有任何属性 

field2: 
access_flag为ACC_PUBLIC,标志其为public类型 
name_index为0x0009,指向9号常量池,即简单名称为”public_global” 
descriptor_index为0x0008,指向8号常量池,即描述符为”I” 
attributes_count为0,即没有任何属性 

field3: 
access_flag为0x0010|0x0008|0x0002,即ACC_FINAL | ACC_STATIC | ACC_PRIVATE,标志其为private static final类型 
name_index为0x000A,指向10号常量池,即简单名称为”sfi” 
descriptor_index为0x0008,指向8号常量池,即描述符为”I” 
attributes_count为1,即有一个属性 

该属性的 
attribute_name_index为0x000B,指向11号常量池,即”ConstantValue”属性 
attribute_length为2,即固定2个字节 
constantvalue_index为0x000C,指向12号常量池,即sfi的值为”127”(这里还是字符串) 

field4: 
access_flag为0x0010|0x0008|0x0001,即ACC_FINAL | ACC_STATIC | ACC_PUBLIC,标志其为public static final类型 
name_index为0x000D,指向13号常量池,即简单名称为”sfs” 
descriptor_index为0x000E,指向14号常量池,即描述符为”Ljava/lang/String;” 
attributes_count为1,即有一个属性 

该属性的 
attribute_name_index为0x000B,指向11号常量池,即”ConstantValue”属性 
attribute_length为2,即固定2个字节 
constantvalue_index为0x000F,指向15号常量池,即sfs的值为”test strings” 

field5: 
access_flag为ACC_PRIVATE,标志其为private类型 
name_index为0x0011,指向17号常量池,即简单名称为”sb” 
descriptor_index为0x0012,指向18号常量池,即描述符为”Ljava/lang/StringBuilder;” 
attributes_count为0,即没有任何属性 

9.methods_count/methods(方法数/方法) 
方法域的method_info结构与字段域是一样的,即

method_info{access_flags       u2,name_index        u2,descriptor_index     u2,attributes_count     u2,attributes        attributes_info * attributes_cout }

不过其access_flag有些不同

 

如果一个方法是抽象方法,那么它就不能为private、static、final、synchronized、native和strict类型

在方法域出现的属性有Code、Deprecated、Exceptions、Synthetic 
下面介绍新出现的两种属性Code和Exceptions: 
(1).Code 
其info域的结构如下

 

其中:

  • max_stack标志该方法执行的任意时刻,其操作数栈的最大长度(以字为单位)
  • max_locals标志改方法的局部变量所需存储空间的长度(以字为单位)
  • code_length给出了该方法字节码部分的长度(以字节为单位)
  • code_length长度的字节码
  • exception_table_length是异常表的长度,紧接着是exception_table_length个exception_info所描述的异常信息
  • 最后就是该段代码的属性描述,这是一个嵌套的属性描述,会出现两个新的属性LineNumberTable和LocalVariableTable,即行号表和局部变量表

首先看exception_table_info的结构,可以预见,一个异常在代码中的描述就必须包含作用域、异常类型和异常处理三部分内容,看看exception_table_info是不是这样组织的

exception_table_info{start_pc       u2,end_pc        u2,handler_pc      u2,catch_type      u2,}

不出所料,start_pc就是异常处理器起始位置相对该段代码的偏移量, 
end_pc就是异常处理器结束位置相对该段代码的偏移量, 
handler_pc就是异常处理器第一条指令相对该段代码的偏移量 
catch_type指向描述该异常类型(java/lang/Throwable或其子类)的CONSTANT_Class常量池索引,二若catch_type为0,那么异常处理器将处理所有异常

(2).LineNumberTable 
行号表与ELF/PE文件看上去有着异曲同工之妙,它同样建立了方法的字节码偏移量和源代码行号之间的映射关系。其info域结构如下

TypeNameCount
u2line_number_table_length1
line_number_infoline_number_tableline_number_table_length

 

 

 

line_number_table_length描述了行号表的项数,注意,并不是行号表各项并不是逐行对应,而是可能按照任何顺序排列,并且可能多项对应同一行。 
line_number_info的结构如下:

line_number_info{start_pc      u2,line_number    u2,}

其中,start_pc描述了该行起始第一个字节码对应该段代码的偏移量,line_number描述了对应的行号。

(3).LocalVariableTable 
这里由LocalVariableTable保存了方法的栈帧中局部变量域源代码中局部变量的名称和描述符之间的映射关系。

 

同样,局部变量表也是以local_variable_table_length个local_variable_info结构进行组织的 

local_variable_info的结构如下:

local_variable_info{start_pc        u2,length         u2,name_index       u2,descriptor_index    u2,index          u2,}

  • start_pc为该段代码中指令开始位置的便宜
  • length为从start_pc开始的、所有局部变量有效的代码的长度(即由[start_pc, start_pc + length]描述了局部变量的作用域)
  • name_index为该局部变量简单名称的CONSTANT_Utf8常量池索引
  • descriptor_index为该局部变量描述符的CONSTANT_Utf8常量池索引
  • index为在此方法的栈帧中局部变量部分的索引 
    需要明白,local_variable_info建立了源代码中局部变量名称、类型和其在字节码的作用域、以及栈帧中的索引之间的联系,因方法区这一特殊的结构而存在。

(4).Exceptions属性 
区别于描述Code属性的exception_table部分,这里是方法可能会抛出的异常,而非包围代码的try/catch异常。Exceptions属性的info域格式如下:

TypeNameCount
u2number_of_exceptions1
u2exception_index_tablenumber_of_exceptions

 

 

 

exception_index_table是该方法抛出的异常类型的CONSTANT_Class常量池索引,number_of_exceptions指出了抛出异常类型的数量。 


methods部分的解析可以参考http://note.youdao.com/share/?id=b1c762ba1ee4874a23eb8a512cccf507&type=note 
10.attributes_count/attributes(属性数和属性) 
最后还有两种属性:InnerClass和SourceFile 
(1).SourceFile 
其info结构为:

TypeNameCount
u2sourcefile_index1

 

 

给出了指向源文件名的CONSTANT_Utf8常量池索引 
如该class文件最后的attributes_count为1,其 
attribute_name_index为0x0033,指向51号常量池,即”SourceFile”属性 
attribute_length为0x02,即2个字节 
sourcefile_index为0x0034,指向52号常量池,即源文件名为”TestClass.java”

(2).InnerClasses

TypeNameCount
u2number_of_classes1
classes_infoclassesnumber_of_classes

 

 

 

classses_info描述了内部类(成员嵌套类、局部嵌套类和匿名嵌套类)的信息,其结构如下:

classes_info{inner_class_info_index    u2,outer_class_info_index    u2,inner_name_index       u2,inner_class_access_flags   u2,}

  • inner_class_info_index指向所定义的内部类的CONSTANT_Class常量池的索引
  • outer_class_info_index指向该内部类的外围类的CONSTANT_Class常量池的索引,若该内部类不是一个成员嵌套类,其值为0
  • inner_name_index为该内部类的简单名称的CONSTANT_Utf8_info索引,当该内部类为匿名内部类时,其值为0
  • inner_class_access_flags是对该内部类的访问标志

以如下内容为例 
 
其生成的class文件如下: 

其InnerClasses属性内容为: 

其中匿名内部类Runnable的全限定名为InnerClassTest$1,由于其不是一个成员嵌套类(该类是局部嵌套类),其outer_class_info_index 为0,由于该类是一个匿名内部类,其inner_name_index为0(即简单名称为空) 
局部嵌套类NestedLocalClass的全限定名为InnerClassTest$1NestedLocalClass,由于其不是一个成员嵌套类,其outer_class_info_index为0,其简单名称为”NestedLocalClass”,access_flag为final 
成员嵌套类NestedMemberClass的全限定名为InnerClassTest$NestedMemberClass,其简单名称为”NestedMemberClass”,access_flag为public static final

此外,我们注意到内嵌类的内容会定义在各自的class文件中,而不会出现在InnerClassTest类的class文件中,在NestedMemberClass的class文件中有着如下的InnerClasses属性: 

在subClass的class文件中也有着如下的InnerClasses属性: 

可以看出,每个作为外围类的内部类的类都将保存在该外围类的CONSTANT_Class常量池中,并有一个inner_class_info结构加以描述 
如InnerClassTest的3个内部类项,NestedMemberClass的第二个内部类项
 
但是需要注意,subClass在被没有被InnerClassTest直接引用时,是不会出现在InnerClassTest的InnerClasses属性中的 
另外,InnerClasses还将表述内嵌类型的外围类,作为内部类的所有外围类都将保存在该内部类的CONSTANT_Class常量池中,并有一个inner_class_info结构加以描述 
如NestedMemberClass的第1个外部类项,subClass的2个外部类项

版权声明:本文为博主原创文章,未经博主允许不得转载。