最近在寫一個私人項目,名字叫做ClassAnalyzer
,ClassAnalyzer
的目的是能讓我們對<span class="wp_keywordlink">Java Class</span>
文件的設計與結構能夠有一個深入的理解。主體框架與基本功能已經完成,還有一些細節功能日後再增加。實際上JDK
已經提供了命令列工具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
類別。
public class ClassFile { public U4 magic; // magic public U2 minorVersion; // minor_version public U2 majorVersion; // major_version public U2 constantPoolCount; // constant_pool_count public ConstantPoolInfo[] cpInfo; // cp_info public U2 accessFlags; // access_flags public U2 thisClass; // this_class public U2 superClass; // super_class public U2 interfacesCount; // interfaces_count public U2[] interfaces; // interfaces public U2 fieldsCount; // fields_count public FieldInfo[] fields; // fields public U2 methodsCount; // methods_count public MethodInfo[] methods; // methods public U2 attributesCount; // attributes_count public BasicAttributeInfo[] attributes; // attributes }
組成Class
檔案的各個資料項目中,例如魔數、Class
檔案的版本等資料項目、存取標誌、類別索引、父類別索引,它們在每個Class
檔案中都佔用固定數量的字節,在解析時只需要讀取對應數量的位元組。除此之外,需要靈活處理的主要包括4
部分:常數池、欄位表集合、方法表集合和屬性表集合。欄位和方法都可以具備自己的屬性,Class
本身也有對應的屬性,因此,在解析欄位表集合和方法表集合的同時也包含了屬性表的解析。
常數池佔據了Class
檔案很大一部分的數據,用於儲存所有的常數信息,包括數字和字串常數、類別名稱、介面名稱、欄位名稱和方法名稱等。 Java
虛擬機器規格定義了多種常數類型,每種常數類型都有自己的結構。常量池本身就是一個表,在解析時有幾點要注意。
每個常數類型都透過一個u1
類型的tag來識別。
表頭給出的常數池大小(constantPoolCount
)比實際大1
,例如,如果constantPoolCount
等於47
,那麼常數池中有46
項常數。
常數池的索引範圍從1
開始,例如,如果constantPoolCount
等於47
,那麼常數池的索引範圍為1~46
。設計者將第0
項空出來的目的是用來表示「不引用任何一個常數池項目」。
CONSTANT_Utf8_info
型常數的結構中包含u1
類型的tag
、u2
類型的length
和由length
個u1
類型組成的bytes
,這length
位元組的連續資料是一個使用MUTF-8
(Modified UTF-8)
編碼的字串。 MUTF-8
與UTF-8
並不相容,主要差異有兩點:一是null
字元會被編碼成2
位元組(0xC0
和0x80
);二是補充字元是依照UTF-16
拆分為代理對分別編碼的,相關細節可以看這裡(變種UTF-8)。
属性表用于描述某些场景专有的信息,Class
文件、字段表和方法表都有相应的属性表集合。Java
虚拟机规范定义了多种属性,ClassAnalyzer
目前实现了对常用属性的解析。和常量类型的数据项不同,属性并没有一个tag
来标识属性的类型,但是每个属性都包含有一个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_count
个attribute_info
类型的attributes
。我们已经介绍了属性表的解析,attributes
的解析方式与属性表的解析方式一致。
Class
的文件方法表采用了和字段表相同的存储格式,只是access_flags
对应的含义有所不同。方法表包含着一个重要的属性:Code
属性。Code
属性存储了Java
代码编译成的字节码指令,在ClassAnalyzer
中,Code
对应的Java
类如下所示(仅列出了类属性)。
public class Code extends BasicAttributeInfo { private short maxStack; private short maxLocals; private long codeLength; private byte[] code; private short exceptionTableLength; private ExceptionInfo[] exceptionTable; private short attributesCount; private BasicAttributeInfo[] attributes; ... private class ExceptionInfo { public short startPc; public short endPc; public short handlerPc; public short catchType; ... } }
在Code
属性中,codeLength
和code
分别用于存储字节码长度和字节码指令,每条指令即一个字节(u1
类型)。在虚拟机执行时,通过读取code
中的一个个字节码,并将字节码翻译成相应的指令。另外,虽然codeLength
是一个u4
类型的值,但是实际上一个方法不允许超过65535
条字节码指令。
ClassAnalyzer
的源码已放在了GitHub上。在ClassAnalyzer
的README中,我以一个类的Class
文件为例,对该Class
文件的每个字节进行了分析,希望对大家的理解有所帮助。
以上是實作一個Java Class解析器的實力程式碼分享的詳細內容。更多資訊請關注PHP中文網其他相關文章!