解读 Java Class 文件格式

http://blog.csdn.net/tyrone1979/article/details/964560

1.目的

大型软件系统开发时,某些Java组件可能涉及到多种数据库或中间件系统的连接和应用,例如一个数据传递组件需要从DB2中读取数据,并将数据通过中间件WebSphere MQ发送到其他系统,这类组件功能单一,但却需要连接多种第三方产品,使得程序员的单元测试变的非常不便,程序员不得不注视或修改部分源代码,或者在本地安装所需第三方产品。无疑这两种选择都是痛苦的。

基于以上的不便,本文开发了解析Java Class文件程序,目的是将第三方产品API的Class文件转换为Java源文件(不包括Java类的方法实现),在源文件的各种程序所需的方法里实现一些简单的语句,例如数据库连接方法永远返回true,获得数据方法永远返回”Hello world”等,用JDK重新编译转换后的Java源文件,来替换真正的API文件,这样程序员在UT测试时,无需修改源代码,也无需安装任何产品,并且能通过修改替换的API Java源文件实施各种UT测试。

为了实现以上需求,必须先要了解Java Class文件格式。Java虚拟机识别的class文件格式包含Java虚拟机指令(或者bytecodes)和一个符号表以及其他的辅助信息。本文将使用VC++语言解析Java Class文件符号表,逆向生成Java源代码结构。如图1:

图1

之所以使用VC++而不使用Java的主要是因为VC++界面开发简单;运行速度快,不需要虚拟机;需要用指针建立复杂的数据结构。

2.实现

实现该工具的过程如下:

1.解析Class文件,从Class文件中读取数据并保存到称为ClassFile结构体中;

2.解析ClassFile结构体,生成源代码字符串;

3.将字符串显示到视图中。

2.1解析Class文件

为实现第1步,首先需要了解Class文件格式规范,参考《Java虚拟机规范》第四章class文件格式,总结class文件的数据结构如图2。

2.1.1Class文件格式

Class文件格式ClassFile结构体的C语言描述如下:

structClassFile

{

u4 magic;//识别Class文件格式,具体值为0xCAFEBABE,

u2 minor_version;// Class文件格式副版本号,

u2 major_version;// Class文件格式主版本号,

u2 constant_pool_count;//常数表项个数,

cp_info **constant_pool;//常数表,又称变长符号表,

u2 access_flags;//Class的声明中使用的修饰符掩码,

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为unsigned short,u4为unsigned long:

typedef unsigned charu1;

typedef unsigned shortu2;

typedef unsigned longu4;

cp_info **constant_pool是常量表的指针数组,指针数组个数为constant_pool_count,结构体cp_info为

structcp_info

{

u1 tag;//常数表数据类型

u1 *info;//常数表数据

};

常数表数据类型Tag定义如下:

#defineCONSTANT_Class7

#defineCONSTANT_Fieldref9

#defineCONSTANT_Methodref10

#defineCONSTANT_InterfaceMethodref11

#defineCONSTANT_String8

#defineCONSTANT_Integer3

#defineCONSTANT_Float4

#defineCONSTANT_Long5

#defineCONSTANT_Double6

#defineCONSTANT_NameAndType12

#defineCONSTANT_Utf81

每种类型对应一个结构体保存该类型数据,例如CONSTANT_Class的info指针指向的数据类型应为CONSTANT_Class_info

structCONSTANT_Class_info

{

u1 tag;

u2 name_index;

};

图2

CONSTANT_Utf8的info指针指向的数据类型应为CONSTANT_Utf8_info

structCONSTANT_Utf8_info

{

u1 tag;

u2 length;

u1 *bytes;

};

Tag和info的详细说明参考《Java虚拟机规范》第四章4.4节。

access_flags为类修饰符掩码,域与方法都有各自的修饰符掩码。

#defineACC_PUBLIC0x0001

#defineACC_PRIVATE0x0002

#defineACC_PROTECTED0x0004

#defineACC_STATIC0x0008

#defineACC_FINAL0x0010

#defineACC_SYNCHRONIZED0x0020

#defineACC_SUPER0x0020

#defineACC_VOLATILE0x0040

#defineACC_TRANSIENT0x0080

#defineACC_NATIVE0x0100

#defineACC_INTERFACE0x0200

#defineACC_ABSTRACT0x0400

#defineACC_STRICT0x0800

例如类的修饰符为public abstract则access_flags的值为ACC_PUBLIC | ACC_ABSTRACT=0x0401。

this_class的值是常数表的索引,索引的info内保存类或接口名。例如类名为com.sum.java.swing.SwingUtitlities2在info保存为com/sum/java/swing/SwingUtitlities2

super_class的值是常数表的索引,索引的info内保存超类名,在info内保存形式和类名相同。

interfaces是数组,数组个数为interfaces_count,数组内的元素为常数表的索引,索引的info内保存超接口名,在info内保存形式和类名相同。

field_info **fields是类域数据的指针数组,指针数组个数为fields_count,结构体field_info定义如下:

structfield_info

{

u2 access_flags;//域修饰符掩码

u2 name_index;//域名在常数表内的索引

u2 descriptor_index;//域的描述符,其值是常数表内的索引

u2 attributes_count;//域的属性个数

attribute_info **attributes;//域的属性数据,即域的值

};

例如一个域定义如下:

private final static byte UNSET=127;

则该域的修饰符掩码值为:ACC_PRIVATE | ACC_STATIC | ACC_FINAL=0x001A

常数表内name_index索引内保存数据为UNSET,常数表内descriptor_index索引内保存的数据为B(B表示byte,其他类型参考《Java虚拟机规范》第四章4.3.2节)。attributes_count的值为1,其中attributes是指针数组。指针数组个数为attributes_count,在此为1,attribute_info结构体如下:

structattribute_info

{

u2 attribute_name_index;//常数表内索引

u4 attribute_length;//属性长度

u1 *info;//根据属性类型不同而值不同

};

attribute_info可以转换(cast)为多种类型ConstantValue_attribute,Exceptions_attribute,LineNumberTable_attribute,LocalVariableTable_attribute,Code_attribute等。

因为域的属性只有一种:ConstantValue_attribute,因此此结构体转换为

structConstantValue_attribute

{

u2 attribute_name_index;//常数表内索引

u4 attribute_length;//属性长度值,永远为2

u2 constantvalue_index;//常数表内索引,保存域的值

//在此例中,常数表内保存的值为127

};

method_info **methods是方法数据的指针数组,指针数组个数为methods_count,结构体method_info定义如下:

structmethod_info

{

u2 access_flags;//方法修饰符掩码

u2 name_index;//方法名在常数表内的索引

u2 descriptor_index;//方法描述符,其值是常数表内的索引

u2 attributes_count;//方法的属性个数

attribute_info **attributes;//方法的属性数据,

//保存方法实现的Bytecode和异常处理

};

例如一个方法定义如下:

public static booleancanAccessSystemClipboard(){

}

则access_flags的值为ACC_PUBLIC | ACC_STATIC =0x0009,常数表内name_index索引内保存数据为canAccessSystemClipboard,常数表内descriptor_index索引内保存数据为()Z;(括号表示方法参数,Z表示返回值为布尔型,详细说明参照《Java虚拟机规范》第四章4.3.2节)。attribute_info **attributes是方法的属性指针数组,个数为attributes_count,数组内保存的是常数表索引,info为Code_attribute或Exceptions_attribute。

本文不解析方法内容,因此忽略Code_attribute和Exceptions_attribute的内容。

ClassFile结构体中的attribute_info **attributes是附加属性数组指针,个数为attributes_count,本文只识别SourceFile属性。

structSourceFile_attribute

{

u2 attribute_name_index;//常数表内索引

u4 attribute_length;//属性长度值,永远为2

u2 sourcefile_index;//常数表内索引,info保存源文件名

};

例如com.sum.java.swing.SwingUtitlities2类的源文件名为SwingUtitlities2.java。

以上是本文需要解析的Class文件格式。

2.1.2读取数据

定义CJavaClass类完成解析Class文件,生成Java源程序字符串。使用VC++的MFC类CFile从Class文件读取数据。例如:用16进制编辑器打开Class文件,如图3,前4个byte分别是CA FE BA BE,使用CFile::Read(tmp,sizeof(u4))读取后,tmp的值为0xBEBAFECA,所以需要位转换。定义以下方法从文件读取定长数据:

voidreadu1(u1 *buff);

voidreadu2(u2 *buff);

voidreadu4(u4 *buff);

定义如下方法读取变长数据。

voidreadun(void *buff,u4 len);

读取的u2和u4的数据需要位转换:

U1[0]

U1 [1]

U1[1]

U1 [0]

U2:

U1[0]

U1 [1]

U1[3]

U4:

U1 [2]

U1[3]

U1 [2]

U1[0]

U1 [1]

调用voidreadu4(u4 *buff);后buff的值为0xCAFEBABE,该值为ClassFile的magic,识别该文件是Java Class文件。

图3

magic的后面是Class格式的版本号,图3的版本为0x00000030=0.48。版本后面是常数表的元素个数,图3的常数表的元素个数为0xD2=210个。常数表的元素个数之后如ClassFile结构体定义的常数表,类信息,接口信息,域信息,方法信息和附加属性等。

2.2生成Java源文件

解析Class文件后,生产ClassFile结构体。遍历该结构体数据,则可根据Java语言规范生成Java源文件。例如根据ClassFile的access_flags值获得Java类的修饰符,其中access是CArray<CString,CString&>,保存类所有的修饰符:

if((flag & ACC_PUBLIC )==ACC_PUBLIC)

{

access.Add(CString("public"));

}

if((flag & ACC_PRIVATE)==ACC_PRIVATE)

{

access.Add(CString("private"));

}

if((flag & ACC_PROTECTED)==ACC_PROTECTED)

{

access.Add(CString("protected"));

}

2.3显示视图

将获得的Java源代码显示在MFC的CScrollView视图非常简单,可以添加一些关键字颜色,例如注释显示为草绿色等,如图4。

图4

3.总结

本文根据《Java虚拟机规范》开发了解析Java Class文件格式,并生成Java源代码结构的工具。其优点是:

1.脱离Java虚拟机或Java开发环境;

2.可查阅没有Java源代码的Class文件的内容;

3.为一些复杂的Java Jar包生成相同类名的替代类,方便开发调试。例如,用返回固定字符串的java源文件更换需要网络链接的相同java类,有助于本地运行与调试。

缺点是:

1.由于没有反编译Bytecode,工具生成的部分Java源文件需要手动添加一些Java属性值;

2.Java源文件内的所需要使用的Java方法内容需要程序员手动实现。

我想,这就是旅行的真义吧。

解读 Java Class 文件格式

相关文章:

你感兴趣的文章:

标签云: