從上一篇博客(Java Class文件解析)的分析可以看出,Class文件中的各項是按照一定的包含關系和次序關系存儲的,因此Class文件可以從頭到尾地被解析為各個項。下面請看一個解析實例:
這里我們以非常著名的HelloWorld為實例來分析Java Class文件。HelloWorld的源代碼如下:
package bytecodeResearch;


public class HelloWorld
{

//定義兩個靜態變量
private static String str_1 = "Hello";
private static String str_2 = "World";

/** *//**
* 靜態方法
* @param str
*/
private static void Hello(String str)

{
System.out.println(str);
}

/** *//**
* 靜態方法
* @param str
*/
private static void World(String str)

{
System.out.println(str);
}

/** *//**
* 程序入口方法
* @param args
*/
public static void main(String[] args)

{
Hello(str_1);
World(str_2);
}
}

編譯單元HelloWorld.java文件經過編譯器編譯之后,將得到一個HelloWorld.class文件。需要說明的是,經過不同的編譯器編譯之后得到的HelloWorld.class文件可能不一樣,本人選用的開發工具是Eclipse3.3-europa版本,Eclipse SDK自帶的JDT工具內置了增量式Java編譯器,這個編譯器與javac完全兼容。本人以下的分析都是基于Eclipse自帶的編譯器編譯得到的Class文件,如果有人用了Jikes編譯器、GNU的編譯器或者其它版本的javac編譯器的話,得到的Class文件跟我的可能不完全一樣的話,但是Class文件的格式肯定是一樣的,因此分析的原理也都是一樣的,即都是基于上一篇博客(Java Class文件解析)給出的ClassFile結構圖示。
經過Eclipse3.3編譯得到的Class文件內容如下:
注:該Class的內容是經過解析的,即將.class文件的二進制字節流轉換為16進制的數字字符流形式。由于UE查看Class文件時,用一個8位的16進制數來表示每一行起始字節對應于Class文件中的字節號,為了不造成視覺差異,本人寫了一段程序來讀寫Class文件內容,其輸出結果就是上面這段內容,每16個字節占一行,每個字節均拆解成2位16進制整數字符,每一行左端的8位的16進制整數就表示該行的起始字節對應于原Class文件中的字節號。這個程序貼在了我第一篇blog里--一個讀取Class文件的示例程序了。
好了,閑言少敘!下面開始正式解析這個Class文件。
按照上一篇博客(Java Class文件解析)介紹的ClassFile結構圖示以及對ClassFile結構的詳細分析,可以將HelloWorld.class文件的各個項順序地解析如下:
(1) 前4個字節0xCAFEBABE是magic項的內容。
(2) 接下來的2個字節0x0000是minor_version項的內容,即該Class文件的次版本號0。
(3) 接下來的2個字節0x0031是major_version項的內容,即該Class文件的主版本號為49。這個主版本號對應于J2SE5.0的編譯器的編譯結果。如果你用J2SE6.0的編譯器來編譯此HelloWorld.java程序的話,得到的主版本號應該是0x0032,即50。
(4) 接下來的2個字節0x0031是constant_pool_count項的內容,它表示接下來共有連續的49-1=48個constant_pool表項。
(5) 緊接著constant_pool_count項后面的是常量池列表項。常量池列表項的長度是可變的,這是因為不同的常量池表項的格式是不一樣的。
5.1)先來分析第一個常量池表項。上一篇博客(Java Class文件解析)中提到,每個常量池表項的具體格式是要根據其tag項(即該常量池表項的第一個字節)來決定。因此,constant_pool_count項后面的第一字節就是第一個常量池表項的tag項的內容,在這里為0x07,cp_type表可知,tag值為0x07的對應CONSTANT_Class結構的常量池表項。再查閱CONSTANT_Class_info表,該表結構如下:
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
根據《JVM Spec》(2nded)中對此表的說明可知,1個字節的tag項就是剛才讀取的值0x07,而后的2個字節的name_index項表示對一個CONSTANT_Utf8_info表的索引,該索引項包含了類或者接口的完全限定名稱。對于HelloWorld.class這個類來說,第一個常量池表項就是一個CONSTANT_Class_info表的結構,其tag項的值是0x07,其name_index項的值是0x0002,即該常量池表項指向索引為0x0002的常量池表項。
5.2)再來分析第二個常量池表項。其tag項值為0x01,查閱cp_type表可知,該常量池表項是一個CONSTANT_ Utf8_info表的結構,然后我們查閱CONSTANT_ Utf8_info表,該表結構如下:
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
根據《JVM Spec》(2nded)中對此表的說明可知,1個字節的tag項的值就是0x01,2個字節的length項給出了后續的bytes數組的長度(字節數),在這里為0x001B,即其后續的27個字節均是該bytes數組的內容,它包含了按照變體UTF-8格式存儲(不是標準的UTF-8格式啊,具體的差別請查閱那兩個參考資料,都有說明)的字符串中的字符。按照此變體格式可以將這27個字節解析為“bytecodeResearch/HelloWorld”,這27個字符實際上就是該ClassFile的完全限定名稱,這是為什么呢?因為第一個常量池表項是CONSTANT_Class_info結構的,其name_index項指向的常量池表項包含了類或者接口的完全限定名稱,第二個常量池表項正是第一個常量池表項中name_index項所指向的常量池表項,因此“bytecodeResearch/HelloWorld”這27個字符就是該ClassFile的完全限定名稱(注:是完全限定名稱的內部形式,下同)。
5.3)再來分析第三個常量池表項。第二個常量池表項后的第一個字節為0x07,由cp_type表可知,tag值為0x07的對應CONSTANT_Class結構的常量池表項。類似于對第一個常量池表項的分析,0x07后面的兩個字節為name_index項,其值為0x0004,即該常量池表項指向索引為0x0004的常量池表項,且該常量池是CONSTANT_Utf8_info表的結構
5.4)下面來分析第四個常量池表項。該常量池表項的tag項值又是0x01,類似地,同第二個常量池表項的分析,該常量池表項也是一個CONSTANT_ Utf8_info表的結構,其length項值為0x0010,即其后續的bytes數組的長度為0x0010個字節。按照《JVM Spec》(2nded)中關于變體UTF-8格式的定義,可以將這16個字節解析為“java/lang/Object”,這是Java類層次結構的根類Object類的完全限定名稱。
類似地,可以分析第5,6,7,8,9,10都是一個CONSTANT_ Utf8_info結構,分別表示“str_1”,“Ljava/lang/String”,“str_2”,“<clinit>”,“()V”,“Code”
5.5)…….其他常量池表項的分析原理是類似的,這里就不贅述了。
5.6)第四十八個常量池表項的解析。經過分析,第48個常量池起始于第000001f0h字節,終止于第00000202h字節。該常量池表項又是一個CONSTANT_Utf8_info表的結構,其15字節的bytes數組項的值可解析為“HelloWorld.java”。由后面的分析說明該常量池表項存儲的是該類文件的SourceFile屬性的信息。
好了,該Class文件的常量池部分已經解析結束了!下面開始其它部分的解析:
(6) 在常量池列表項后面的兩個字節是該Java類型的access_flags項,這里為0x0021。根據access_flags表可以查到,該值是0x0020和0x0001兩者的和,即該類的修飾符為ACC_PUBLIC+ACC_SUPER,前者表示該類是public類型,后者表示采用invokespecial指令特殊處理對超類的調用。具體可以查閱兩本參考資料中關于JVM指令集的描述J
(7)接下來的兩個字節是this_class項,它是一個對常量池表項的索引,在這里值為0x0001,即它指向1號常量池表項,而1號常量池表項是一個CONSTANT_Class_info結構,它指向2號常量池表項,2號常量池表項的值為bytecodeResearch/HelloWorld,前面提到這是該Class文件的完全路徑名稱的內部形式,因此this_class即指bytecodeResearch/HelloWorld。
(8) 接下來的兩個字節是super_class項,它是一個對常量池表項的索引,在這里值為0x0003, 即它指向3號常量池表項,查一下上面對3號常量池表項的分析,它指向4號常量池表項,而4號常量池表項包含的值為“java/lang/Object”,即super_class的實際值為“java/lang/Object”。說明我們分析的這個Class文件的超類是java.lang.Object。
(9) 下面的兩個字節是interfaces_count項,在這里的值為0x0000,這表示由該類直接實現或者由該接口所擴展的超接口的數量為0,因此該Class文件中的interfaces列表項也就不存在了。
(10)接下來的字節應該是field項的內容了。首先的兩個字節是fields_count項,這里的值為0x0002,即該類聲明了兩個字段(變量),亦即該項之后的fields列表項的元素個數為2。由于fields列表項的類型為field_info,所以在fields_count項下面的字節是兩個連續的field_info結構,下面來詳細分析這兩個具體的field_info結構;
10.1)第一個field_info,即第一個字段的相關信息。
10.1.1)首先的兩個字節是第一個field的access_flags項,在這里的值為0x000A,查閱field_access_flags表可知該access_flags項表示的是ACC_PRIVATE+ACC_STATIC,即該字段是由private和static修飾的。
10.1.2)接下來的兩個字節是name_index項,在這里的值為0x0005,即該字段的簡單名稱由第5個常量池表項描述的,根據上一篇博客(Java Class文件解析)的分析可知,該常量池包含的信息為str_1,即該字段的名稱為str_1。
10.1.3)接下來的兩個字節是descriptor_index
項,
在這里的值為0x0006,即該字段的描述符存儲在
第6個常量池表項,根據上一篇博客(Java Class文件解析)的分析可知,這個字段的類型為“Ljava/lang/String”。在Class文件中,“L<classname>”表示一個類的實例,其中<classname>是這個內部形式的完全限定類名。
10.1.4)接下來的兩個字節是attributes_count
項,
在這里的值為0x0000,即該字段沒有附加的屬性列表。因而也就不用討論attributes[]
項了。
10.2)第二個field_info,即第二個字段的相關信息。類似地,參照第一個字段信息的分析,我們很快就可以知道該字段的access_flags項為ACC_PRIVATE+ACC_STATIC,名字為str_2,類型描述符為“Ljava/lang/String”,attributes_count
項
的值為0x0000。
(11)接下來的字節應該是method項的內容了。首先的兩個字節是methods_count項,這里的值為0x0005,即該類聲明了5個方法,亦即該項之后的methods列表項的元素個數為5。由于methods列表項的類型為method_info,所以在methods_count項下面的字節是5個連續的method_info結構,下面來詳細分析這5個具體的method_info結構:
11.1)第1個method_info結構,即第一個方法的相關信息,如方法名、描述符(即方法的返回值及參數類型)以及一些其它信息。根據method_info表分析接下來的字節碼可以得到:
11.1.1)access_flags項,值為0x0008,即給方法的訪問修飾符為ACC_STATIC,它表示這是一個static方法。
11.1.2)name_index項,值為0x0008,第8號常量池表項存儲的信息為<clinit>即該方法的名稱為<clinit>。這是一個類與接口初始化方法,這個方法是由Java編譯器編譯源代碼的時候產生的,Java編譯器將該類的所有類變量初始化語句和所有類型的靜態初始化器收集到一起,放到<clinit>方法中,該方法只能被JVM隱式地調用,專門用于把類型的靜態變量設置為它們正確的初始值。
11.1.3)descriptor_index項,值為0x0009,第9號常量池表項存儲的信息為()V,這表示該方法的沒有參數,返回值為void。
11.1.4)attributes_count項,值為0x0001,即該方法有一個屬性。查閱屬性信息表的結構,如下所示:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
由這個表,我們可以知道attributes_count項后面的是這個屬性的attribute_name_index
項,該項的值為0x000A,該屬性的名字信息存儲在第10號常量池表項里。查閱第10號常量池表項可知,該屬性的名字為“Code”,然后我們查閱Code_attribute表,結構如下:
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];
}
11.1.5)Code_attribute
項,該項包含了一個Java方法,或者實例初始化方法,或者類或接口初始化方法的JVM指令和輔助信息。每個JVM的實現都必須要識別Code屬性,在每個method_info結構也必須確切地有一個Code屬性。下面來具體分析這個屬性;
11.1.5.1) attribute_name_index項,2個字節,該項的值為0x000A,查閱第10號常量池表型包含的信息后知該屬性的名字為“Code”。
11.1.5.2)attribute_length項,4個字節,值為0x00000033,這說明該屬性的長度,出去初始的6個字節,還有0x33=51個字節。如果不愿意討論接下去的51字節的話,可以直接跳過這51字節(00000226h-0000025eh字節),討論下一個方法。
11.1.5.3)max_stack項,2個字節,值為0x0001,這表示該方法執行中任何點操作數棧上字的最大個數為1。
11.1.5.4)max_locals項,2個字節,值為0x0000,這表示該方法使用的局部變量個數為0。
11.1.5.5)code_length項,4個字節,值為0x0000000B,這表示該方法的code數組中字節的總個數為11。
11.1.5.6)code[]項,由11.1.5.5)知,該方法的code數組共占11個字節。該code[]項給出了實現該方法的JVM代碼的實際字節。例如第一個指令是0x12,這是ldc指令,這個指令表示將一個常量池表項壓入棧,它需要一個操作數,而它后面的一個字節是0x0B,因此這條指令加上其操作數就表示將常量池中的第0x0B號表項壓入棧。接下來的一個指令是0xB3,這是putstatic指令,這條指令表示設置類中靜態變量的值。它需要兩個操作數indexbyte1和indexbyte2,這兩個操作數均占一個字節,JVM執行putstatic執行時,會通過計算(indexbyte1<<8)|indexbyte2生成一個對常量池表項的索引,這里的參數為0x00和0x0D,運算結果是0x0D,因此這條指令的意思就是將操作數棧的當前棧頂元素賦值給0x0D號常量池表項所存儲的字段(str_1),即完成對字段str_1的賦值。。同樣,下面的五個字節的意思,就是將索引為0x0F的常量池表項壓入操作數棧,并賦值給(0x00<<)|0x11=0x11號常量池表項中所存儲的字段(str_2),即完成對字段str_2的賦值。該Code數組的最后一個字節是0xB1,這是一條不帶操作數的指令return,它表示從方法中返回,返回值為void。
11.1.5.7)exception_table_length項,2個字節,值為0x0000,這表示該方法的異常處理器的個數為0。因此exception_table[ ]就沒有必要討論了。
11.1.5.8)attributes_count項,2個字節,值為0x0002,這表示該方法Code屬性具有兩個屬性。當前由Code屬性定義和使用的兩個屬性是LineNumberTale和LocalVariableTable屬性。
11.1.5.9)attributes[ ]項,由于LineNumberTale和LocalVariableTable兩個屬性都包含了一些調試信息,但是兩者都是可選屬性,因此這里就不多討論了。
11.2)第2個method_info結構,即第2個方法的相關信息。第2個方法是實例初始化方法<init>,這段方法在Class文件中的字節編號為:0000025fh-0000029bh字節,感興趣的朋友請繼續分析下去,原理和第一個方法的分析是一樣的。
11.3)第3個method_info結構,即第3個方法的相關信息。第3個方法是該類的靜態方法Hello,這段方法在Class文件中的字節編號為:0000029ch-000002dfh字節,感興趣的朋友請繼續分析下去,原理和第一個方法的分析是一樣的。
11.4)第4個method_info結構,即第4個方法的相關信息。第4個方法是該類的靜態方法World,這段方法在Class文件中的字節編號為:000002e0h-00000323h字節,感興趣的朋友請繼續分析下去,原理和第一個方法的分析是一樣的。
11.5)第5個method_info結構,即第5個方法的相關信息。第5個方法是該類文件的入口main方法,這段方法在Class文件中的字節編號為:00000324h-00000370h字節,感興趣的朋友請繼續分析下去,原理和第一個方法的分析是一樣的。
(12) attributes_count項,2個字節,該ClassFile的屬性計數項,它的值為0x0001,表示在后續的attributes列表中的attributes_info表的總個數為1。
(13) 和attributes[ ]項,該ClassFile的屬性列表項,這是Class文件的最后一項了!由(12)知,該列表項只有一個表項。由attribute_info表結構
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
可知,attributes_count項后面的兩個字節是attribute_name_index項,它的值為0x002F,它表示對常量池編號為0x002F的表項的一個索引。這個索引表項存儲的信息為”SourceFile”,即該ClassFile屬性的名稱為SourceFile,該屬性是一個可選的定長屬性,對于給定的ClassFile結構的attributes列表中不能有多于一個的SourceFile屬性;查閱SourceFile_attribute表可知,下面的4個字節為attribute_length項,其值為0x00000002,它表示在該項后面還有2個字節的信息。根據SourceFile_attribute表,最后的這兩個字節是sourcefile_index項,該項的值是一個對CONSTANT_Utf8_info結構的常量池表項的索引,其信息表示的是該Class文件的源文件名稱。在這里值為0x0030,根據上一篇博客(Java Class文件解析)的分析,第48號常量池表項存儲的信息可解析為“HelloWorld.java”,這是該Class文件的源文件名稱(不包括路徑)。
好了,到此為止,該Class文件的實例已經全部解析完畢,大功告成:)
posted on 2008-02-03 13:03
獨孤求敗 閱讀(4864)
評論(33) 編輯 收藏 所屬分類:
Java ByteCode