当前位置: 首页 > 科技观察

自己实现一个Java类解析器

时间:2023-03-21 01:51:23 科技观察

最近在写一个私有项目,叫ClassAnalyzer。ClassAnalyzer的目的是让我们深入了解JavaClass文件的设计和结构。主要框架和基本功能已经完成,后续会增加一些细节功能。其实JDK已经提供了反编译Class文件的命令行工具javap,不过这篇文章将阐明我实现解析器的思路。Class文件作为类或接口信息的载体,每个Class文件完整地定义了一个类。为了让Java程序“一次编写,到处运行”,Java虚拟机规范对Class文件进行了严格的规定。构成Class文件的基本数据单位是字节,而这些字节之间没有分隔符,这使得整个Class文件存储的内容几乎都是程序运行所必需的数据,而不能表示的数据由单个字节由多个连续字节表示。根据Java虚拟机规范,Class文件使用类似于C语言结构的伪结构来存储数据。这个伪结构中只有两种数据类型:无符号数和表格。Java虚拟机规范定义了u1、u2、u4、u8分别表示1字节、2字节、4字节、8字节的无符号数。无符号数可用于描述数字和索引一个引用、一个数值或一个字符串。表是由多个无符号数或其他表作为数据项组成的一致数据类型。表是用来描述符合层次关系结构的数据的,所以整个Class文件本质上就是一个表。在ClassAnalyzer中,u1、u2、u4、u8分别对应byte、short、int、long,Class文件描述为如下Java类。公共类ClassFile{公共U4魔法;//魔法公共U2次要版本;//minor_versionpublicU2majorVersion;//major_versionpublicU2constantPoolCount;//constant_pool_countpublicConstantPoolInfo[]cpInfo;//cp_infopublicU2accessFlags;//access_flagspublicU2thisClass;//this_classpublicU2superClass;//super_classpublicU2interfacesCount;//interfaces_count公共U2[]接口;//接口publicU2fieldsCount;//fields_countpublicFieldInfo[]fields;//字段publicU2methodsCount;//methods_countpublicMethodInfo[]方法;//方法publicU2attributesCount;//属性计数公共BasicAttributeInfo[]属性;//attributes}如何解析组成Class文件的各种数据项,比如幻数、Class文件的版本、访问标志、类索引、父类索引等数据项,它们在每个Class文件中都有占用固定的字节数,解析时只需要读取相应的字节数。除此之外,还有四个主要部分需要灵活处理:常量池、字段表集合、方法表集合、属性表集合。字段和方法可以有自己的属性,Class本身也有相应的属性。因此,对字段表集合和方法表集合的解析也包括对属性表的解析。常量池在Class文件中占据了很大一部分数据,用来存放所有的常量信息,包括数字和字符串常量、类名、接口名、字段名和方法名。Java虚拟机规范定义了多种常量类型,每一种都有自己的结构。常量池本身就是一个表,解析的时候需要注意几点。每个常量类型都由类型为u1的标记标识。表头给出的常量池大小(constantPoolCount)比实际值大1。比如constantPoolCount等于47,那么常量池中就有46个常量。常量池的索引范围从1开始,例如constantPoolCount=47,则常量池的索引范围为1~46。设计者将第0项留空的目的是表达“不引用任何常量池项”。CONSTANT_Utf8_info类型常量的结构包含一个u1类型的标签,一个u2类型的长度,以及长度为u1类型组成的字节。这个长度字节的连续数据是使用MUTF-8(ModifiedUTF-8)编码的字符串。MUTF-8与UTF-8不兼容。主要区别有两个:一是空字符会被编码成2个字节(0xC0和0x80);另一种是增补字符按照UTF-16拆分成surrogatepairs,单独编码是的,详情可以看这里(变体UTF-8)。属性表用于描述特定场景下的信息,Class文件、字段表、方法表都有对应的属性表集合。Java虚拟机规范定义了多种属性,ClassAnalyzer目前实现了对常用属性的分析。与常量类型的数据项不同,属性没有标识属性类型的标签,但每个属性都包含一个u2类型的attribute_name_index,attribute_name_index指向常量池中一个CONSTANT_Utf8_info类型的常量,其中包含该属性的姓名。ClassAnalyzer在解析属性时,通过attribute_name_index指向的常量对应的属性名,知道属性类型。字段表用于描述在类或接口中声明的变量,字段包括类级变量和实例级变量。字段表的结构包括u2类型access_flags、u2类型name_index、u2类型descriptor_index、u2类型attributes_count和attributes_countattributes_info类型属性。属性表的解析我们已经介绍过了,属性的解析方法和属性表一样。Class的文件方法表采用与字段表相同的存储格式,只是access_flags对应的含义不同。方法表包含一个重要的属性:代码属性。Code属性存放Java代码编译后的字节码指令。在ClassAnalyzer中,Code对应的Java类如下(只列出了类的属性)。公共类代码扩展BasicAttributeInfo{privateshortmaxStack;私有短maxLocals;私人长代码长度;私有字节[]代码;私有短异常表长度;私有ExceptionInfo[]exceptionTable;私有短属性计数;私有BasicAttributeInfo[]属性;...私有类ExceptionInfo{publicshortstartPc;公共短端PC;公共空头处理程序Pc;公共短catchType;...}}在Code属性中,codeLength和code分别用来存放字节码长度和字节码指令,每条指令是一个word段(u1类型)。虚拟机在执行时,会读取代码中的字节码,并将字节码翻译成相应的指令。另外,codeLength虽然是u4类型的值,但实际上一个方法不允许超过65535条字节码指令。实现ClassAnalyzer的源代码已经放在GitHub上。在ClassAnalyzer的README中,我以一个类的Class文件为例,分析了Class文件的每个字节,希望能帮助大家理解。