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(次版本号和主版本号)

次版本号和主版本号分别为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:
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个外部类项

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

实例探索Class文件的更多相关文章

  1. 内核编程实例,多文件的Makefile

    内核编程实例,多文件的Makefile 经典的hello word测试 ////# cat hello.c #include <linux/module.h> #include <l ...

  2. 数据库实例: STOREBOOK > 数据文件/退回字段/重做日志组|管理员

    ylbtech-Oracle:数据库实例: STOREBOOK  > 数据文件/退回字段/重做日志组|管理员 数据文件/退回字段/重做日志组|管理员 1. 数据库实例: STOREBOOK  & ...

  3. C 语言实例 - 输出当前文件执行代码

    C 语言实例 - 输出当前文件执行代码 输出当前文件执行代码,__FILE__ 为当前执行的文件常量. 实例 #include <stdio.h> int main() { FILE *f ...

  4. MATLAB实例:新建文件夹,保存.mat文件并保存数据到.txt文件中

    MATLAB实例:新建文件夹,保存.mat文件并保存数据到.txt文件中 作者:凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ 用MATLAB实现:指定路径下 ...

  5. 实例分析ELF文件动态链接

    参考文献: <ELF V1.2> <程序员的自我修养---链接.装载与库>第6章 可执行文件的装载与进程 第7章 动态链接 <Linux GOT与PLT> 开发平台 ...

  6. 实例分析ELF文件静态链接

    参考文献: <ELF V1.2> <程序员的自我修养---链接.装载与库>第4章 静态链接 开发平台: [thm@tanghuimin static_link]$ uname ...

  7. Android实例-如何将文件打包到安装文件(XE8+小米2)

    结果: 1.文件名支持英文字母.数字,不支持中文,大小写敏感. 2.文件最好放在根目录下,或是子目录下,如果放在根目外,根目录改名后,找不到文件. 3.打开Project->Deployment ...

  8. Python实例获取mp3文件的tag信息

    下面利用一个python的实例程序,来学习python.这个程序的目的就是分析出所有MP3文件的Tag信息并输出. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ...

  9. Go语言学习之7 接口实例、终端文件读写、异常处理

    本节主要内容: 1. 终端读写2. 文件读写3. 命令行参数4. Json5. 自定义错误 1. 终端读写 操作终端相关文件句柄常量    os.Stdin:标准输入    os.Stdout:标准输 ...

随机推荐

  1. 【spring boot】使用定时任务@Scheduled 报错:Encountered invalid @Scheduled method 'dealShelf': Cron expression must consist of 6 fields (found 7 in "0 30 14 * * ? *")

    在spring boot中使用使用定时任务@Scheduled 报错: org.springframework.beans.factory.BeanCreationException: Error c ...

  2. webService 三要素

    WebService(jax-ws)三要素 SOAP: 基于HTTP协议,采用XML格式,用来传递信息的格式. WSDL: 用来描述如何访问具体的服务.(相当于说明书) UDDI: 用户自己可以按UD ...

  3. 如何获取gcr等镜像

    在cloud.docker.com上注册一个用户,然后登录 然后在github.com上注册一个用户 通过github Desktop建立一个repository,同时加入一个Dockerfile,然 ...

  4. 用LaTeX写线性规划

    线性规划由目标函数和若干约束构成,Latex中并没有直接的命令来写线性规划.简单的做法是使用\begin{eqnarray} … \end{eqnarray}命令,但eqnarray命令是使若干方程按 ...

  5. [转]Mapping Stored Procedure Parameters in SSIS OLE DB Source Editor

    本文转自:http://geekswithblogs.net/stun/archive/2009/03/05/mapping-stored-procedure-parameters-in-ssis-o ...

  6. Lateral View使用指南

    https://blog.csdn.net/sunnyyoona/article/details/62894761 select sum(pitem) from (select map_values( ...

  7. Struts基本概念

    内容源自: Struts2基本概念 一.struts2体系结构: 1.Web浏览器请求一个资源.2.过滤器Dispatcher查找方法,确定适当的Action.3.拦截器自动对请求应用通用功能,如验证 ...

  8. STL 源代码剖析 算法 stl_algo.h -- next_permutation

    本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie next_permutation ----------------------------- ...

  9. Netty利用ChannelGroup广播消息

    在Netty中提供了ChannelGroup接口,该接口继承Set接口,因此可以通过ChannelGroup可管理服务器端所有的连接的Channel,然后对所有的连接Channel广播消息. Serv ...

  10. 06-spring学习-自动装配

    自动装配前面也有写过.这里只做补充 在之前,对于要引用的属性,都必须写上名称, 原始配置: 当要在emp对象里面引用dept对象的时候,需要明确的使用“ref“属性去找到指定的名称,但是这种操作中也可 ...