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


public class HelloWorld
{

//定義兩個靜態(tài)變量
private static String str_1 = "Hello";
private static String str_2 = "World";

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

{
System.out.println(str);
}

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