实例探索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文件进行分析:

//TestInterface.java
public interface TestInterface {
    public void interface_method();
}

//TestClass.java
public class TestClass implements TestInterface{
    private int private_global = 3;
    public int public_global;
    private static final int sfi = 127;
    public static final String sfs = "test strings";
    private StringBuilder sb;
    
    public void method_1(){
        private_global = public_global * 2;
        sb.append(private_global);
    }
    
    public void method_2(int pub){
        public_global = pub;
    }

    public void method_2(int pub, boolean flag){
        int tmp = 5;
        public_global = pub * 2 + tmp;
    }
    
    @Override
    public void interface_method() {
        method_1();
    }
    
}

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

 

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

次版本号和主版本号分别为0×0000和0×0032(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:
            FieldType
ComponentType:
            FieldType
FieldType:
            BaseType
            ObjectType
            ArrayType
BaseType:
            B
            C
            D
            F
            I
            J
            S
            Z
ObjectType:
            L<classname>;
ArrayType:
            [ComponentType
MethodDescriptor:
            (ParameterDescriptor*) ReturnDescriptor
ParameterDescriptor:
            FieldType
ReturnDescriptor:
            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结构:

Type Name Count
u4 bytes 1

 

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

(3).CONSTANT_Float结构:

Type Name Count
u4 bytes 1

 

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

Type Name Count
u8 bytes 1

 

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

Type Name Count
u8 bytes 1

 

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

Type Name Count
u2 name_index 1

 

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

Type Name Count
u2 sring_index 1

 

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

Type Name Count
u2 class_index 1
u2 name_and_type_index 1

 

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

Type Name Count
u2 class_index 1
u2 name_and_type_index 1

 

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

Type Name Count
u2 class_index 1
u2 name_and_type_index 1

 

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

Type Name Count
u2 class_index 1
u2 name_and_type_index 1

 

注意区分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

Type Name Count
u2 constantvalue_index 1

 

该属性用于描述值为常量的字段,并且在包含该属性的字段其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域结构如下

Type Name Count
u2 line_number_table_length 1
line_number_info line_number_table line_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域格式如下:

Type Name Count
u2 number_of_exceptions 1
u2 exception_index_table number_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结构为:

Type Name Count
u2 sourcefile_index 1

 

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

(2).InnerClasses

Type Name Count
u2 number_of_classes 1
classes_info classes number_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个外部类项



相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部