JVM: 位元組碼解析
參考書目:《Java虛擬機器規範》/第四章:class檔案解析
不同語言編寫的原始檔(e.g. java, groovy, kotlin, scala)編譯後生成的.class位元組碼檔案都能在JVM上執行。
小端&大端–從記憶體中讀取資料的兩種方式
大端模式:低地址存放資料的高位,高地址存放資料的低位。
小端模式:低地址存放資料的低位,高地址存放資料的高位。
位元組碼在JVM(計算機)記憶體中是小端儲存的。在寫JVM時需要用大端模式讀取。證明:
void JavaVirtualMachine::parse_single_class(JavaClassContent *class_content) { printf(“%X \n”, *(int*)class_content->get_content_ptr()); printf(“%X\n”, htonl(*(int*)class_content->get_content_ptr())); //htonl: 轉換為大端模式 … }
輸出:BEBAFECACAFEBABE
原因:小端;大端
描述符
根據Class檔案結構中常量池中11種資料型別的結構總表,CONSTANT_Methodref_Info、CONSTANT_InterfaceMethodref_Info和CONSTANT_NameAndType_Info中都含有CONSTANT_NameAndType_info。而CONSTANT_NameAndType_Info中又包含名稱和描述符。
資料型別的描述符
void型別Ve.g. ()v型別為v的方法
引用型別e.g. String的描述符: Ljava/lang/String; //引用型別後面記得有分號
陣列型別e.g. byte陣列的描述符: [B
e.g. String陣列的描述符: [Ljava/lang/String;
方法的描述符
(引數資料型別的描述符)返回值的描述符
如果有多個引數,描述符用逗號分隔
e.g.javap –verbose輸出中檢視到main方法的描述符
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;]V //main方法的描述符
e.g.某個方法的描述符
([[Ljava/lang/String;. I, Ljava/AA/BB;]Ljava/lang/String;
原形:String XXX(String[][], int, BB)
實驗:手動解析位元組碼
Example java code:
public class Simple { static int a = 10; public static void main(String[] args) {} }
使用hexdump -C檢視位元組碼檔案,再開啟jclasslib對比確認手動解析結果是否正確。
class檔案的組成部分
uX:佔X個位元組。
!:位元組長度不確定,需要動態計算。
魔數(u4):值為ca fe ba be。每個Java class檔案都以0xCAFEBABE開頭。用於判斷一個檔案是否為合格class檔案。如果不是則不執行。
次版本號(u2)&主版本號(u2):JDK的版本對應的major、minor號。
e.g.00000000ca fe ba be00 00 00 3400 19 0a 00 04 00 15 09
次版本號為0,主版本號為16進位制34轉為10進位制52,所以表示jdk版本為1.8。jclasslib檢視genral information可證。
常量池大小(u2):常量池個數。
*注意:常量池最小index不是0而是1。真實常量池個數=位元組碼檔案中常量池個數-1。
e.g.00000000ca fe ba be 00 00 00 3400 190a 00 04 00 15 09
00 19->25,真實常量池個數為25-1=24。Jclasslib中檢視Constant Pool可證。
常量池:每個資料結構tag都佔前1個位元組。根據tag值對應資料型別的結構解析其中儲存的資訊。
CONSTANT Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
…
常量池中的11種資料結構型別:
e.g.解析常量池
常量池中第1個元素:
00000000ca fe ba be 00 00 00 3400 190a00 0400 1509
tag:0a==10 –> CONSTANT_Methodref
class_index:00 04 == 4 –> 04位置還沒解析出來,這裡只能放符號引用,後面才能轉為直接引用
name_and_type:00 15 ==21
Jclasslib中檢視Constant Pool中第1個元素可證。
常量池中第2個元素:
00000000ca fe ba be 00 00 00 3400 19 0a 00 04 00 1509
0000001000 03 00 1607 00 17 07 00 18 01 00 01 61 01 00
tag:09 ==9 -> CONSTANT_Fieldref_info
class_index:00 03 ==3 ->符號引用第3個元素,所以該field的型別為第3個元素的類。
name_and_type:00 16 ==22
Jclasslib中檢視Constant Pool中第2個元素可證。檢視靜態常量池也可證明符號引用了第3個元素,因為#2 field的值為#3。
常量池中第3個元素:
0000001000 03 00 1607 00 1707 00 18 01 00 01 6101 00
tag:07 ==7 -> CONSTANT_Class_Info
name_index:17
常量池中第4個元素:
0000001000 03 00 16 07 00 1707 00 1801 00 01 61 01 00
tag:07 ==7 -> CONSTANT_Class_Info
…
常量池中第5個元素:
0000001000 03 00 16 07 00 17 07 00 180100 016101 00
tag:01 ==1 -> CONSTANT_Utf8_info,說明是String型別
length:00 01==1 –>長度為1,所以byte只往後取1位
bytes:61 == 97 -> ASCII碼對應‘a’
…
access flags(u2):
通過或運算解析access flag值的含義.e.g. 0x11表示public final
e.g.解析access flag
0000012061 6e 67 2f 4f 62 6a 6563 7400 2100 03 00 04
0x0021 -> public
this_class(u2):當前類
e.g.解析this class
0000012061 6e 67 2f 4f 62 6a 6563 74 00 2100 0300 04
檢視靜態常量池,當前類是常量池中第3個元素
super_class(u2):
e.g.解析super class
0000012061 6e 67 2f 4f 62 6a 6563 74 00 21 00 0300 04
檢視靜態常量池,super class是常量池中第4個元素
interfaces_count(u2):介面數量
e.g.解析Interfaces_count
0000013000 0000 01 00 08 00 0500 06 00 00 00 03 00 01
介面數量是0。
*一個類最多可以實現多少個介面?
-65535。//因為介面數量佔2個位元組=2*8=16位,二進位制16位數最大為1111 1111 1111 1111=十進位制2^16-1。
interfaces[](!):如果介面數量是0,則該區域在位元組碼檔案中不會出現。
fields_count[](u2):fields(類的屬性)的數量
e.g.解析fields數量
0000013000 0000 0100 08 00 0500 06 00 00 00 03 00 01
fields_info(!):field在常量池中的儲存方式。
field_info { u2 access_flags; u2 name_index; //指向常量池 u2 descriptor_index; u2 attributes_count; //屬性數量, attribute_info attributes[attributes_count]; //屬性內容 }
attributes指的是field的屬性(e.g.加了final就會有ConstantValue屬性)。如果attributes_count為0,attributes區域在位元組碼檔案中就不會出現。
e.g.解析一個field
先前fields_count的解析結果說明該類中只有一個field。
0000013000 00 00 0100 0800 0500 0600 0000 03 00 01
第1個屬性:
access_flags:00 08 -> static (參照類訪問和屬性修飾符標誌 表)
name_index:00 05 ->指向常量池中第5個元素
descriptor_index:00 06
attribute_count:00 00
attributes:因為count為0,attributes沒有出現
methods_count(u2):方法數量
e.g.解析方法數量
0000013000 00 00 01 00 08 00 0500 06 00 0000 0300 01
->有3個方法:靜態方法clint、預設構造方法init、main。
methods_info(!):方法
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; //屬性、Code、Exception attribute_info attributes[attributes_count]; }
如果attributes_count為0,attributes區域在位元組碼檔案中就不會出現。
Code_attribute { u2 attribute_name_index; //即為method_info中解析出的attributes值 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屬性的屬性 }
exceptions分別在method_info和Code_attribute中都有記錄,可驗證得知throws異常和try catch異常在位元組碼中有不同的表示。
Code_attribute中有max_stack和max_locals,說明運算元棧和區域性變量表的大小在編譯時就已經確定。
Jclasslib開啟/Methods/方法名/Code,在specific info檢視指令內容。
Code的屬性包括LineNumberTable(程式碼行號,儲存Java程式碼所在行數)和LocalVariableTable(區域性變量表,儲存引數和區域性變數),Jclasslib開啟/Methods/方法名/Code可查。
e.g.解析第1個方法
0000013000 00 00 01 00 08 00 0500 06 00 00 00 0300 01
0000014000 0700 0800 0100 0900 00 00 2f 00 01 00 01
access_flag:00 01
name_index:00 07
descriptor_index:00 08
attr_count:00 01
attributes:00 09
name_index==7 ->常量池中第7個元素為CONSTANT_Utf8_info, String為<init>,說明是構造方法。
Attributes==9 ->第1個屬性為Code,按照Code_attribute結構繼續解析:
e.g.解析init方法的code屬性
0000014000 07 00 08 00 0100 0900 00 00 2f00 01 00 01
0000015000 00 00 052a b7 00 01b100 0000 0200 0a 00
attribute_name_index:00 09 //00 09為Code_attribute的attribute_name_index值
attribute_length:00 00 00 2f
max_stack:00 01
max_locals:00 01
code_length:00 00 00 05
code:2a b7 00 01 b1
exception_table_length:00 00
exception_table:
attributes_count:00 02
attribute_length==47,說明從attribute_length往後所有東西加起來共47個位元組。
方法體:
0x2a == 42 -> aload_0
…
0xb1 == 177 -> return
Jclasslib->Methods/<init>/Code檢視Bytecode可證:
aload_0 invokespecial #1 <java/lang/Object.<init>> return
根據attributes_count確定code有2個attributes後,繼續解析code的attributes:
0000015000 00 00 05 2a b7 00 01b1 00 00 00 0200 0a00
0000016000 00 0600 0100 00000700 0b 00 00 00 0c 00
Code的第1個屬性:
attr_name_index:00 0a
Code第1個屬性的attr_name_index==10, jclasslib檢視常量池第10個元素為CONSTANT_Utf8_info,其String值為LineNumberTable。所以第1個屬性是程式碼行號表。
確定當前code屬性為LineNumberTable後,根據LineNumberTable的結構繼續解析:
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}
attr_name_index:00 0a
attribute_length:00 00 00 06
line_number_table_length:00 01
[ start_pc:00 00
line_number:00 07]
Code的第2個屬性:
0000016000 00 06 00 01 00 00 000700 0b00 00 00 0c00
000001700100 0000 0500 0c000d00 0000 09 00 0e 00
attr_name_index:00 0b
Code第2個屬性的attr_name_index==11, jclasslib檢視常量池第11個元素為CONSTANT_Utf8_info,其String值為LocalVariableTable。所以第2個屬性是區域性變量表。
確定當前code屬性為LocalVariableTable後,根據LocalVariableTable的結構繼續解析:
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{ u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable _table[local_variable _table_length];
}
attr_name_index:00 0b
attribute_length:00 00 00 0c
local_variable_table_length:00 01
[ start_pc:00 00
length:00 05
name_index:00 0c
descriptor_index:00 0d
index:00 00]
attributes_count(u2)
attributes[](!):
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}