1. 程式人生 > 實用技巧 >JVM: 位元組碼解析

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];
}