天天看點

執行個體探索Class檔案

 class檔案是指以.class為檔案字尾的Java虛拟機可裝載檔案。無論該class檔案是在linux上進行編譯的,還是在windows環境下編譯的,無論虛拟機是在何種平台下實作和運作的,class檔案使得Java虛拟機可以正确的讀取、解釋所有的class檔案。 在分析和研究class檔案之前,先提出有一些問題:

1.類/接口(class檔案也可能定義的是接口,是以還是不要了解為類檔案為好)内有哪些内容?

2.以上内容分别儲存在class檔案的什麼地方?

3.這些内容在加載過程中又如何被讀取和解析?

4.這些内容加載後又會被解析成為什麼樣的資料結構儲存在虛拟機中?

5.這些資料結構在虛拟機的運作過程中又是如何被使用的?

擴充問題:

6.如何防止class檔案被劫持?

7.如何防止class檔案被反編譯?

class檔案的組織結構定義如下:

ClassFile{
magic                        u4,
minor_version                u2,
major_version                u2,
constant_pool_count            u2,
constant_pool                cp_info*constant_pool_count,
access_flags                 u2,
this_class                   u2,
super_class                  u2,
interface_count              u2,
interfaces                   u2 * interface_count,
fields_count                 u2,
fields                      field_info * fields_count,
methods_count                u2,
methods                      method_info * methods_count,
attributes_count             u2,
attributes                   attributes_info * attributes_count
}      

以如下程式為例,對生成的class檔案進行分析:

1 //TestInterface.java
 2 public interface TestInterface {
 3     public void interface_method();
 4 }
 5 
 6 //TestClass.java
 7 public class TestClass implements TestInterface{
 8     private int private_global = 3;
 9     public int public_global;
10     private static final int sfi = 127;
11     public static final String sfs = "test strings";
12     private StringBuilder sb;
13     
14     public void method_1(){
15         private_global = public_global * 2;
16         sb.append(private_global);
17     }
18     
19     public void method_2(int pub){
20         public_global = pub;
21     }
22 
23     public void method_2(int pub, boolean flag){
24         int tmp = 5;
25         public_global = pub * 2 + tmp;
26     }
27     
28     @Override
29     public void interface_method() {
30         method_1();
31     }
32     
33 }      

1.magic(魔數) 值為0xcafebabe,沒有特别的意義,放在檔案頭并選取用來标記改檔案是一個class檔案。

執行個體探索Class檔案

2.minor_version/major_version(次版本号和主版本号)

執行個體探索Class檔案

次版本号和主版本号分别為0x0000和0x0032(50),即主版本号位50,次版本号為0

3.constant_pool_count/constant_pool(常量池數量和常量池)

常量池儲存了檔案中類或接口相關的一切常量,字面常量(直接量),如文字字元串、final變量值,以及符号引用,如類或接口的全限定名、方法或字段的簡單名稱和描述符。

其中,全限定名用以在目前命名空間内唯一标志類或接口,在java語言中如java.lang.Object,在class檔案中,會将'.'用'/'取代,即表示為java/lang/Object 簡單名稱就是簡單的方法名或變量名的字元串,如java.lang.Object的成員方法wait()的簡單名稱為"wait"。

而隻有簡單名稱是無法唯一确定調用的方法是哪一個,由于Java語言的特性,方法可能被重寫或重載, 是以還需要根據方法的傳回值、參數數量、類型、順序來确定一個方法描述符來唯一标志該方法,字段的描述符則簡單得多,隻需要給出字段的類型 描述符讓我們聯想起PE/ELF檔案的函數簽名,它由上下文無關文法定義:

FieldDescriptor:
            FieldType
ComponentType:
            FieldType
FieldType:
            BaseType
            ObjectType
            ArrayType
BaseType:
            B
            C
            D
            F
            I
            J
            S
            Z
ObjectType:
            L<classname>;
ArrayType:
            [ComponentType
MethodDescriptor:
            (ParameterDescriptor*) ReturnDescriptor
ParameterDescriptor:
            FieldType
ReturnDescriptor:
            FieldType
            V      

其終結符号如下:

執行個體探索Class檔案

以深入java虛拟機上的示例作為參考:

執行個體探索Class檔案
執行個體探索Class檔案

下面看class檔案内常量池部分: 首先是常量池數:即(0x35)53個常量池

執行個體探索Class檔案

Java虛拟機将常量池組織成為清單(可以看做是一個常量池的數組)的形式,常量池内容可能指向其他常量池,并且class檔案中其他部分内容也可能指向常量池入口,這些常量池通過該常量池在常量池清單中的索引來定位,常量池清單的0号常量池其實是空的,作為常量池的NULL引用,即常量池清單的第一項實際上是1号常量池,常量池清單實際上隻有constant_pool_count - 1個常量池項。 随後是常量池清單,常量池的結構如下:

cp_info{
tag,
info
}      

常量池的固定第一個位元組是常量值标簽,用來描述該常量池儲存内容的類型,常量池标志和含義如下:

執行個體探索Class檔案

根據常量池标志tag的不同,info有不同的組織方式:

(1).CONSTANT_Utf8結構:

執行個體探索Class檔案

(可以看出length由2個位元組表示,最大長度就應該是65536位元組)

該類型是一個長度可變(長度為length)的常量字元串表,用來存儲以下類型的字元串:

  • 文字字元串,如String對象的内容
  • 目前類或接口的全限定名
  • 目前類的超類的全限定名
  • 目前類或接口的父接口的全限定名
  • 字段的簡單名稱或描述符
  • 方法的簡單名稱或描述符
  • 引用類或接口的全限定名
  • 引用字段的簡單名稱和描述符
  • 引用方法的簡單名稱和描述符

字元的存放: 

對于0x0001-0x007f的字元将使用一個位元組(該位元組的0-6位,第7位為0)存放 

對于0x080-0x07ff的字元将使用兩個位元組(依次高位元組的0-5位和低位元組的0-4位,剩餘位分别為10、110)存放 

執行個體探索Class檔案

對于0x0800-0xffff的字元将使用3個位元組(依次為高位元組的0-5中間位元組的0-5,和低位元組的0-3位,剩餘位分别為10、10、1110)存放。

執行個體探索Class檔案

(2).CONSTANT_Integer結構:

Type Name Count
u4 bytes 1

按高位在前的格式存儲int型資料

(3).CONSTANT_Float結構:

按高位在前的格式存儲float型資料 

(4).CONSTANT_Doube結構:

u8

按高位在前的格式存儲double型資料 

(5).CONSTANT_Long結構:

按高位在前的格式存儲long型資料 

(6).CONSTANT_Class結構:

u2 name_index

name_index為類或者接口符号引用的CONSTANT_Utf8常量池的索引(全限定名) 

(7).CONSTANT_String結構:

sring_index

string_index為字元串的CONSTANT_Utf8常量池的索引 

(8).CONSTANT_Fieldref結構: 

描述了指向字段的符号引用,其内容分兩項表示,一項為被引用字段所在類或接口的CONSTANT_Class常量池索引,一項為字段的簡單名稱和描述符,指向一個CONSTANT_NameAndType常量池

class_index
name_and_type_index

(9).CONSTANT_Methodref結構: 

與CONSTANT_Fieldref類似,描述了指向類中聲明的方法的符号引用,其内容分兩項表示,一項為被引用方法所在類的CONSTANT_Class常量池索引,一項為方法的簡單名稱和描述符,指向一個CONSTANT_NameAndType常量池

(10).CONSTANT_InterfaceMethodref結構: 

與CONSTANT_Methodref類似,描述了指向接口中聲明的方法的符号引用,其内容分兩項表示,一項為被引用方法所在接口的CONSTANT_Class常量池索引,一項為方法的簡單名稱和描述符,指向一個CONSTANT_NameAndType常量池

(11).CONSTANT_NameAndType結構: 

可以預見,該常量池提供了所引用字段或方法的簡單名稱和常量池入口

注意區分class_index指向的是對應類的常量池,該CONSTANT_Class常量池指向一個全限定名的CONSTANT_Utf8字元串常量池 

常量池部分的解析可以參考http://note.youdao.com/share/?id=3c1f3fac45837f95cc87fa6694a25b84&type=note 

4.access_flags 

該項2位元組标志了所定義類或接口的類型資訊

執行個體探索Class檔案

該檔案中access_flags為0x0021 ,可見該類是public super類型。

執行個體探索Class檔案

5.this_class(目前類) 

該項2位元組标志了所定義類或接口的CONSTANT_Class常量池索引,最終指向全限定名”TestClass” 

執行個體探索Class檔案

6.super_class(超類) 

該項2位元組标志了所定義類的超類的CONSTANT_Class常量池索引,最終指向全限定名”java/lang/Object”

執行個體探索Class檔案

7.interfaces_count/interfaces(接口數和接口) 

首先2位元組是在該類中直接實作或擴充的接口數,後面緊随若幹個(接口數)2位元組,代表所直接實作或擴充的接口的CONSTANT_Class常量池的索引 

執行個體探索Class檔案

這裡隻實作了一個接口,就是5号常量池,即全限定名”TestInterface”所定義的接口

8.fields_count/fields(字段數和字段) 

fields_count是類變量(靜态變量)和執行個體變量(非靜态變量)的字段數總和,與constant_pool組織形式類似,後面是fields_count個field_info,需要注意的是,目前類的字段不會包含其超類或父接口中繼承的字段,也會包含在Java源檔案中沒有但是在編譯時添加的一些字段。field_info結構如下:

field_info{
access_flags             u2,
name_index               u2,
descriptor_index         u2,
attributes_count         u2,
attributes               attributes_info * attributes_cout 
}      

(1).字段的accesss_flags與描述目前類的access_flags不同:

  

執行個體探索Class檔案

類中聲明的字段,隻能擁有ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三個标志中的一個。ACC_FINAL 

和ACC_VOLATILE 不能同時設定。所有接口中聲明的字段必須有ACC_PUBLIC、ACC_STATIC、ACC_FINAL 這三種标志。 

(2).name_index為該字段的簡單名稱的CONSTANT_Utf8常量池索引 

(3).descriptor_index為該字段的描述符的CONSTANT_Utf8常量池索引 

(4).attributes_count和attributes是attributes_count個attribute_info結構所表述的屬性集合。在字段域出現的屬性有ConstantValue(final常量)、Deprecated(被禁用的訓示符)、Synthetic(編譯器産生的訓示符)

屬性出現在ClassFile、field_info、method_info、Code_attribute中。所有Java虛拟機必須能夠識别Code、ConstantValue、Exception。對于能夠正常實作Java/Java2平台類庫的虛拟機必須能夠識别InnerClass和Synthetic屬性。

執行個體探索Class檔案

attribute_info的結構如下:

attribute_info{
attribute_name_index            u2,
attribute_length                u4,
info                            u1,
}      

attribute_name_index為描述屬性的字元串名稱(即上述列出屬性名)的CONSTANT_Utf8常量池索引, 

attribute_length為後面屬性内容的長度 

這裡先介紹将字段可能用到的ConstantValue、Deprecated和Synthetic屬性 

(1).ConstantValue

constantvalue_index

該屬性用于描述值為常量的字段,并且在包含該屬性的字段其access_flag必須為ACC_STATIC,以表明這是一個靜态常量。 

constantvalue_index指向提供常量值的常量池索引(此外,ConstantValue對應的屬性的attribute_length始終為2) 

(2).Deprecated 

被@Deprecated所注釋的字段、方法或類型,表示雖然該字段、方法或類型仍然存在,但是不建議使用,其在未來的版本中可能會被移除 

Deprecated對應的屬性的attribute_length值始終為0 

(3).Synthetic 

用來指明為編譯器所産生的字段、方法或類型 

同樣,這是一個固定長度屬性,其 

對應的屬性的attribute_length值始終為0

class檔案field域解析: 

首先由開頭兩個位元組看出有5個field_info 

執行個體探索Class檔案

field1: 

access_flag為ACC_PRIVATE,标志其為private類型 

name_index為0x0007,指向7号常量池,即簡單名稱為”private_global” 

descriptor_index為0x0008,指向8号常量池,即描述符為”I” 

attributes_count為0,即沒有任何屬性 

執行個體探索Class檔案

field2: 

access_flag為ACC_PUBLIC,标志其為public類型 

name_index為0x0009,指向9号常量池,即簡單名稱為”public_global” 

執行個體探索Class檔案

field3: 

access_flag為0x0010|0x0008|0x0002,即ACC_FINAL | ACC_STATIC | ACC_PRIVATE,标志其為private static final類型 

name_index為0x000A,指向10号常量池,即簡單名稱為”sfi” 

attributes_count為1,即有一個屬性 

執行個體探索Class檔案

該屬性的 

attribute_name_index為0x000B,指向11号常量池,即”ConstantValue”屬性 

attribute_length為2,即固定2個位元組 

constantvalue_index為0x000C,指向12号常量池,即sfi的值為”127”(這裡還是字元串) 

執行個體探索Class檔案

field4: 

access_flag為0x0010|0x0008|0x0001,即ACC_FINAL | ACC_STATIC | ACC_PUBLIC,标志其為public static final類型 

name_index為0x000D,指向13号常量池,即簡單名稱為”sfs” 

descriptor_index為0x000E,指向14号常量池,即描述符為”Ljava/lang/String;” 

執行個體探索Class檔案

constantvalue_index為0x000F,指向15号常量池,即sfs的值為”test strings” 

執行個體探索Class檔案

field5: 

name_index為0x0011,指向17号常量池,即簡單名稱為”sb” 

descriptor_index為0x0012,指向18号常量池,即描述符為”Ljava/lang/StringBuilder;” 

執行個體探索Class檔案

9.methods_count/methods(方法數/方法) 

方法域的method_info結構與字段域是一樣的,即

method_info{
access_flags             u2,
name_index               u2,
descriptor_index         u2,
attributes_count         u2,
attributes               attributes_info * attributes_cout 
}      

不過其access_flag有些不同

執行個體探索Class檔案

如果一個方法是抽象方法,那麼它就不能為private、static、final、synchronized、native和strict類型

在方法域出現的屬性有Code、Deprecated、Exceptions、Synthetic 

下面介紹新出現的兩種屬性Code和Exceptions: 

(1).Code 

其info域的結構如下

執行個體探索Class檔案

其中:

  • max_stack标志該方法執行的任意時刻,其操作數棧的最大長度(以字為機關)
  • max_locals标志改方法的局部變量所需存儲空間的長度(以字為機關)
  • code_length給出了該方法位元組碼部分的長度(以位元組為機關)
  • code_length長度的位元組碼
  • exception_table_length是異常表的長度,緊接着是exception_table_length個exception_info所描述的異常資訊
  • 最後就是該段代碼的屬性描述,這是一個嵌套的屬性描述,會出現兩個新的屬性LineNumberTable和LocalVariableTable,即行号表和局部變量表

首先看exception_table_info的結構,可以預見,一個異常在代碼中的描述就必須包含作用域、異常類型和異常處理三部分内容,看看exception_table_info是不是這樣組織的

exception_table_info{
start_pc             u2,
end_pc               u2,
handler_pc           u2,
catch_type           u2,
}      

不出所料,start_pc就是異常處理器起始位置相對該段代碼的偏移量, 

end_pc就是異常處理器結束位置相對該段代碼的偏移量, 

handler_pc就是異常處理器第一條指令相對該段代碼的偏移量 

catch_type指向描述該異常類型(java/lang/Throwable或其子類)的CONSTANT_Class常量池索引,二若catch_type為0,那麼異常處理器将處理所有異常

(2).LineNumberTable 

行号表與ELF/PE檔案看上去有着異曲同工之妙,它同樣建立了方法的位元組碼偏移量和源代碼行号之間的映射關系。其info域結構如下

line_number_table_length
line_number_info line_number_table

line_number_table_length描述了行号表的項數,注意,并不是行号表各項并不是逐行對應,而是可能按照任何順序排列,并且可能多項對應同一行。 

line_number_info的結構如下:

line_number_info{
start_pc           u2,
line_number        u2,
}      

其中,start_pc描述了該行起始第一個位元組碼對應該段代碼的偏移量,line_number描述了對應的行号。

(3).LocalVariableTable 

這裡由LocalVariableTable儲存了方法的棧幀中局部變量域源代碼中局部變量的名稱和描述符之間的映射關系。

執行個體探索Class檔案

同樣,局部變量表也是以local_variable_table_length個local_variable_info結構進行組織的 

local_variable_info的結構如下:

local_variable_info{
start_pc                u2,
length                  u2,
name_index              u2,
descriptor_index        u2,
index                   u2,
}      
  • start_pc為該段代碼中指令開始位置的便宜
  • length為從start_pc開始的、所有局部變量有效的代碼的長度(即由[start_pc, start_pc + length]描述了局部變量的作用域)
  • name_index為該局部變量簡單名稱的CONSTANT_Utf8常量池索引
  • descriptor_index為該局部變量描述符的CONSTANT_Utf8常量池索引
  • index為在此方法的棧幀中局部變量部分的索引 

    需要明白,local_variable_info建立了源代碼中局部變量名稱、類型和其在位元組碼的作用域、以及棧幀中的索引之間的聯系,因方法區這一特殊的結構而存在。

(4).Exceptions屬性 

差別于描述Code屬性的exception_table部分,這裡是方法可能會抛出的異常,而非包圍代碼的try/catch異常。Exceptions屬性的info域格式如下:

number_of_exceptions
exception_index_table

exception_index_table是該方法抛出的異常類型的CONSTANT_Class常量池索引,number_of_exceptions指出了抛出異常類型的數量。 

methods部分的解析可以參考http://note.youdao.com/share/?id=b1c762ba1ee4874a23eb8a512cccf507&type=note 

10.attributes_count/attributes(屬性數和屬性) 

最後還有兩種屬性:InnerClass和SourceFile 

(1).SourceFile 

其info結構為:

sourcefile_index

給出了指向源檔案名的CONSTANT_Utf8常量池索引 

如該class檔案最後的attributes_count為1,其 

attribute_name_index為0x0033,指向51号常量池,即”SourceFile”屬性 

attribute_length為0x02,即2個位元組 

sourcefile_index為0x0034,指向52号常量池,即源檔案名為”TestClass.java”

(2).InnerClasses

number_of_classes
classes_info classes

classses_info描述了内部類(成員嵌套類、局部嵌套類和匿名嵌套類)的資訊,其結構如下:

classes_info{
inner_class_info_index        u2,
outer_class_info_index        u2,
inner_name_index              u2,
inner_class_access_flags      u2,
}      
  • inner_class_info_index指向所定義的内部類的CONSTANT_Class常量池的索引
  • outer_class_info_index指向該内部類的外圍類的CONSTANT_Class常量池的索引,若該内部類不是一個成員嵌套類,其值為0
  • inner_name_index為該内部類的簡單名稱的CONSTANT_Utf8_info索引,當該内部類為匿名内部類時,其值為0
  • inner_class_access_flags是對該内部類的通路标志

以如下内容為例 

執行個體探索Class檔案

其生成的class檔案如下: 

執行個體探索Class檔案

其InnerClasses屬性内容為: 

執行個體探索Class檔案

其中匿名内部類Runnable的全限定名為InnerClassTest$1,由于其不是一個成員嵌套類(該類是局部嵌套類),其outer_class_info_index 為0,由于該類是一個匿名内部類,其inner_name_index為0(即簡單名稱為空) 

局部嵌套類NestedLocalClass的全限定名為InnerClassTest$1NestedLocalClass,由于其不是一個成員嵌套類,其outer_class_info_index為0,其簡單名稱為”NestedLocalClass”,access_flag為final 

成員嵌套類NestedMemberClass的全限定名為InnerClassTest$NestedMemberClass,其簡單名稱為”NestedMemberClass”,access_flag為public static final

此外,我們注意到内嵌類的内容會定義在各自的class檔案中,而不會出現在InnerClassTest類的class檔案中,在NestedMemberClass的class檔案中有着如下的InnerClasses屬性: 

執行個體探索Class檔案

在subClass的class檔案中也有着如下的InnerClasses屬性: 

執行個體探索Class檔案

可以看出,每個作為外圍類的内部類的類都将儲存在該外圍類的CONSTANT_Class常量池中,并有一個inner_class_info結構加以描述 

如InnerClassTest的3個内部類項,NestedMemberClass的第二個内部類項 

但是需要注意,subClass在被沒有被InnerClassTest直接引用時,是不會出現在InnerClassTest的InnerClasses屬性中的 

另外,InnerClasses還将表述内嵌類型的外圍類,作為内部類的所有外圍類都将儲存在該内部類的CONSTANT_Class常量池中,并有一個inner_class_info結構加以描述 

如NestedMemberClass的第1個外部類項,subClass的2個外部類項

版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。