JVM 深入學習:Java 解析 Class 檔案過程解析
前言:
身為一個java程式設計師,怎麼能不瞭解JVM呢,倘若想學習JVM,那就又必須要了解Class檔案,Class之於虛擬機器,就如魚之於水,虛擬機器因為Class而有了生命。《深入理解java虛擬機器》中花了一整個章節來講解Class檔案,可是看完後,一直都還是迷迷糊糊,似懂非懂。正好前段時間看見一本書很不錯:《自己動手寫Java虛擬機器》,作者利用go語言實現了一個簡單的JVM,雖然沒有完整實現JVM的所有功能,但是對於一些對JVM稍感興趣的人來說,可讀性還是很高的。作者講解的很詳細,每個過程都分為了一章,其中一部分就是講解如何解析Class檔案。
這本書不太厚,很快就讀完了,讀完後,收穫頗豐。但是紙上得來終覺淺,絕知此事要躬行,我便嘗試著自己解析Class檔案。go語言雖然很優秀,但是終究不熟練,尤其是不太習慣其把型別放在變數之後的語法,還是老老實實用java吧。
話不多說,先貼出專案地址:https://github.com/HalfStackDeveloper/ClassReader
Class檔案
什麼是Class檔案?
java之所以能夠實現跨平臺,便在於其編譯階段不是將程式碼直接編譯為平臺相關的機器語言,而是先編譯成二進位制形式的java位元組碼,放在Class檔案之中,虛擬機器再載入Class檔案,解析出程式執行所需的內容。每個類都會被編譯成一個單獨的class檔案,內部類也會作為一個獨立的類,生成自己的class。
基本結構
隨便找到一個class檔案,用Sublime Text開啟是這樣的:
是不是一臉懵逼,不過java虛擬機器規範中給出了class檔案的基本格式,只要按照這個格式去解析就可以了:
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
ClassFile中的欄位型別有u1、u2、u4,這是什麼型別呢?其實很簡單,就是分別表示1個位元組,2個位元組和4個位元組。
開頭四個位元組為:Magic,是用來唯一標識檔案格式的,一般被稱作magic number(魔數),這樣虛擬機器才能識別出所載入的檔案是否是class格式,class檔案的魔數為cafebabe。不只是class檔案,基本上大部分檔案都有魔數,用來標識自己的格式。
接下來的部分主要是class檔案的一些資訊,如常量池、類訪問標誌、父類、介面資訊、欄位、方法等,具體的資訊可參考《Java虛擬機器規範》。
解析
欄位型別
上面說到ClassFile中的欄位型別有u1、u2、u4,分別表示1個位元組,2個位元組和4個位元組的無符號整數。java中short、int、long分別為2、4、8個位元組的有符號整數,去掉符號位,剛好可以用來表示u1、u2、u4。
public class U1 { public static short read(InputStream inputStream) { byte[] bytes = new byte[1]; try { inputStream.read(bytes); } catch (IOException e) { e.printStackTrace(); } short value = (short) (bytes[0] & 0xFF); return value; } } public class U2 { public static int read(InputStream inputStream) { byte[] bytes = new byte[2]; try { inputStream.read(bytes); } catch (IOException e) { e.printStackTrace(); } int num = 0; for (int i= 0; i < bytes.length; i++) { num <<= 8; num |= (bytes[i] & 0xff); } return num; } } public class U4 { public static long read(InputStream inputStream) { byte[] bytes = new byte[4]; try { inputStream.read(bytes); } catch (IOException e) { e.printStackTrace(); } long num = 0; for (int i= 0; i < bytes.length; i++) { num <<= 8; num |= (bytes[i] & 0xff); } return num; } }
常量池
定義好欄位型別後,我們就可以讀取class檔案了,首先是讀取魔數之類的基本資訊,這部分很簡單:
FileInputStream inputStream = new FileInputStream(file); ClassFile classFile = new ClassFile(); classFile.magic = U4.read(inputStream); classFile.minorVersion = U2.read(inputStream); classFile.majorVersion = U2.read(inputStream);
這部分只是熱熱身,接下來的大頭在於常量池。解析常量池之前,我們先來解釋一下常量池是什麼。
常量池,顧名思義,存放常量的資源池,這裡的常量指的是字面量和符號引用。字面量指的是一些字串資源,而符號引用分為三類:類符號引用、方法符號引用和欄位符號引用。通過將資源放在常量池中,其他項就可以直接定義成常量池中的索引了,避免了空間的浪費,不只是class檔案,Android可執行檔案dex也是同樣如此,將字串資源等放在DexData中,其他項通過索引定位資源。java虛擬機器規範給出了常量池中每一項的格式:
cp_info { u1 tag; u1 info[]; }
上面的這個格式只是一個通用格式,常量池中真正包含的資料有14種格式,每種格式的tag值不同,具體如下所示:
由於格式太多,文章中只挑選一部分講解:
這裡首先讀取常量池的大小,初始化常量池:
//解析常量池 int constant_pool_count = U2.read(inputStream); ConstantPool constantPool = new ConstantPool(constant_pool_count); constantPool.read(inputStream);
接下來再逐個讀取每項內容,並儲存到陣列cpInfo中,這裡需要注意的是,cpInfo[]下標從1開始,0無效,且真正的常量池大小為constant_pool_count-1。
public class ConstantPool { public int constant_pool_count; public ConstantInfo[] cpInfo; public ConstantPool(int count) { constant_pool_count = count; cpInfo = new ConstantInfo[constant_pool_count]; } public void read(InputStream inputStream) { for (int i = 1; i < constant_pool_count; i++) { short tag = U1.read(inputStream); ConstantInfo constantInfo = ConstantInfo.getConstantInfo(tag); constantInfo.read(inputStream); cpInfo[i] = constantInfo; if (tag == ConstantInfo.CONSTANT_Double || tag == ConstantInfo.CONSTANT_Long) { i++; } } } }
我們先來看看CONSTANT_Utf8格式,這一項裡面存放的是MUTF-8編碼的字串:
CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; }
那麼如何讀取這一項呢?
public class ConstantUtf8 extends ConstantInfo { public String value; @Override public void read(InputStream inputStream) { int length = U2.read(inputStream); byte[] bytes = new byte[length]; try { inputStream.read(bytes); } catch (IOException e) { e.printStackTrace(); } try { value = readUtf8(bytes); } catch (UTFDataFormatException e) { e.printStackTrace(); } } private String readUtf8(byte[] bytearr) throws UTFDataFormatException { //copy from java.io.DataInputStream.readUTF() } }
很簡單,首先讀取這一項的位元組陣列長度,接著呼叫readUtf8(),將位元組陣列轉化為String字串。
再來看看CONSTANT_Class這一項,這一項儲存的是類或者介面的符號引用:
CONSTANT_Class_info { u1 tag; u2 name_index; }
注意這裡的name_index並不是直接的字串,而是指向常量池中cpInfo陣列的name_index項,且cpInfo[name_index]一定是CONSTANT_Utf8格式。
public class ConstantClass extends ConstantInfo { public int nameIndex; @Override public void read(InputStream inputStream) { nameIndex = U2.read(inputStream); } }
常量池解析完畢後,就可以供後面的資料使用了,比方說ClassFile中的this_class指向的就是常量池中格式為CONSTANT_Class的某一項,那麼我們就可以讀取出類名:
int classIndex = U2.read(inputStream); ConstantClass clazz = (ConstantClass) constantPool.cpInfo[classIndex]; ConstantUtf8 className = (ConstantUtf8) constantPool.cpInfo[clazz.nameIndex]; classFile.className = className.value; System.out.print("classname:" + classFile.className + "\n");
位元組碼指令
解析常量池之後還需要接著解析一些類資訊,如父類、介面類、欄位等,但是相信大家最好奇的還是java指令的儲存,大家都知道,我們平時寫的java程式碼會被編譯成java位元組碼,那麼這些位元組碼到底儲存在哪呢?別急,講解指令之前,我們先來了解下ClassFile中的method_info,其格式如下:
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
method_info裡主要是一些方法資訊:如訪問標誌、方法名索引、方法描述符索引及屬性陣列。這裡要強調的是屬性陣列,因為位元組碼指令就儲存在這個屬性陣列裡。屬性有很多種,比如說異常表就是一個屬性,而儲存位元組碼指令的屬性為CODE屬性,看這名字也知道是用來儲存程式碼的了。屬性的通用格式為:
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
根據attribute_name_index可以從常量池中拿到屬性名,再根據屬性名就可以判斷屬性種類了。
Code屬性的具體格式為:
Code_attribute { u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_length]; u2 attributes_count; attribute_info attributes[attributes_count]; }
其中code陣列裡儲存就是位元組碼指令,那麼如何解析呢?每條指令在code[]中都是一個位元組,我們平時javap命令反編譯看到的指令其實是助記符,只是方便閱讀位元組碼使用的,jvm有一張位元組碼與助記符的對照表,根據對照表,就可以將指令翻譯為可讀的助記符了。這裡我也是在網上隨便找了一個對照表,儲存到本地txt檔案中,並在使用時解析成HashMap。程式碼很簡單,就不貼了,可以參考我程式碼中InstructionTable.java。
接下來我們就可以解析位元組碼了:
for (int j = 0; j < methodInfo.attributesCount; j++) { if (methodInfo.attributes[j] instanceof CodeAttribute) { CodeAttribute codeAttribute = (CodeAttribute) methodInfo.attributes[j]; for (int m = 0; m < codeAttribute.codeLength; m++) { short code = codeAttribute.code[m]; System.out.print(InstructionTable.getInstruction(code) + "\n"); } } }
執行
整個專案終於寫完了,接下來就來看看效果如何,隨便找一個class檔案解析執行:
哈哈,是不是很贊!
相關文章
- 【JVM】深入解析class類檔案JVM
- Jvm之用java解析class檔案JVMJava
- java class檔案解析Java
- java class 檔案格式解析Java
- 深入解析Class類檔案的結構
- Class檔案解析
- JVM系列(三):JVM建立過程解析JVM
- JVM學習--Class類檔案結構JVM
- Java二進位制Class檔案格式解析Java
- JVM學習筆記——Class類檔案解讀JVM筆記
- 【深入學習JVM 04】回收“已死”物件的過程JVM物件
- Android啟動過程深入解析Android
- 【sharpedge 】.NET配置檔案解析過程詳解
- 深入理解JVM(五)Class類的檔案結構JVM
- 【JVM】JVM系列之Class檔案(三)JVM
- Java 原始碼編譯成 Class 檔案的過程分析Java原始碼編譯
- 解析Class檔案魔數和版本號[轉]
- 破解class檔案的第一步:深入理解JAVA Class檔案Java
- 《深入理解java虛擬機器》學習筆記5——Java Class類檔案結構Java虛擬機筆記
- java解析yaml配置檔案JavaYAML
- 使用 Java 解析XML檔案JavaXML
- 用Java解析CSV檔案Java
- 深入解析 Java OutOfMemoryErrorJavaError
- Java學習過程Java
- sql學習過程1:sql server資料型別解析SQLServer資料型別
- 《深入理解Java虛擬機器》-(實戰)練習修改class檔案Java虛擬機
- Windows通過hosts檔案解析域名Windows
- Linux Desktop Entry 檔案深入解析Linux
- DNS解析過程原理DNS
- SQL 解析的過程SQL
- 域名解析過程
- java的學習過程Java
- JXL包大解析;Java程式生成excel檔案和解析excel檔案內容JavaExcel
- 阿里P8大佬帶你深入解析JVM與java阿里JVMJava
- Java解析ELF檔案:ELF檔案格式規範Java
- Java虛擬機器啟動過程解析Java虛擬機
- Java影像灰度化的實現過程解析Java
- Java XML檔案解析書目錄JavaXML