1. 程式人生 > >類分解器JavaP--分析Java位元組碼

類分解器JavaP--分析Java位元組碼

深入Java程式設計——Java的位元組程式碼

Java程式設計師很少注意程式的編譯結果。事實上,Java的位元組程式碼向我們提供了非常有價值的資訊。特別是在除錯排除Java效能問題時,

編譯結果讓我們可以更深入地理解如何提高程式執行的效率等問題。其實JDK使我們研究Java位元組程式碼變得非常容易。本文闡述怎樣利

JDK中的工具檢視解釋Java位元組程式碼,主要包含以下方面的一些內容:

a) Java類分解器——javap

b) Java位元組程式碼是怎樣使程式避免程式的記憶體錯誤

c) 怎樣通過分析位元組程式碼來提高程式的執行效率

d) 利用第三方工具反編譯Java位元組程式碼

一、

Java類分解器——javap

大多數Java程式設計師知道他們的程式不是編譯成本機程式碼的。實際上,程式被編譯成中間位元組程式碼,由Java虛擬機器來解釋執行。然而,很少程式設計師注意一下位元組程式碼,因為他們使用的工具不鼓勵他們這樣做。大多數的Java除錯工具不允許單步的位元組程式碼除錯。這些工具要麼顯示原始碼,要麼什麼都不顯示。幸好JDK提供了Java類分解器javap,一個命令列工具。javap對類名給定的檔案(.class)提供的位元組程式碼進行反編譯,打印出這些類的一個可讀版本。在預設情況下,javap打印出給定類內的公共域、方法、建構函式,以及靜態初始值。

1javap的具體用法

語法: javap <選項> <類名>

2.應用例項

讓我們來看一個例子來進一步說明如何使用javap

// Imports

import java.lang.String;

public class ExampleOfByteCode {

// Constructors

public ExampleOfByteCode() { }

// Methods

public static void main(String[] args) {

System.out.println("Hello world");

}

}

編譯好這個類以後,可以用一個十六進位制編輯器開啟.class

檔案,再通過虛擬機器說明規範來解釋位元組程式碼的含義,但這並不是好方法。

利用javap,可以將位元組程式碼轉換成人們可以閱讀的文字,只要加上-c引數:

javap -c ExampleOfByteCode

輸出結果如下:

Compiled from ExampleOfByteCode.java

public class ExampleOfByteCode extends java.lang.Object {

public ExampleOfByteCode();

public static void main(java.lang.String[]);

}

Method ExampleOfByteCode()

0 aload_0

1 invokespecial #6 <Method java.lang.Object()>

4 return

Method void main(java.lang.String[])

0 getstatic #7 <Field java.io.PrintStream out>

3 ldc #1 <String "Hello world">

5 invokevirtual #8 <Method void println(java.lang.String)>

8 return

從以上短短的幾行輸出程式碼中,可以學到關於位元組程式碼的許多知識。在main方法的第一句指令是這樣的:

0 getstatic #7 <Field java.io.PrintStream out>

開頭的初始數字是指令在方法中的偏移,所以第一個指令的偏移是0。緊跟偏移的是指令助記符。在本例中,getstatic指令將一個靜態欄位壓入一個數據結構,我們稱這個資料結構為運算元堆疊。後續指令可以通過此結構引用這個欄位。緊跟getstatic指令後面的是壓到哪個欄位中去。這裡的欄位是“#7 <Field java.io.PrintStream out>”。如果直接察看位元組程式碼,這些欄位資訊並沒有直接存放到指令中去。事實上,就象所有Java類使用的常量一樣,欄位資訊儲存在共享池中。在共享池中儲存欄位資訊可以減小位元組程式碼的大小。這是因為指令僅僅需要儲存的是整型索引號,而不是將整個常量儲存到常量池中。本例中,欄位資訊存放在常量池的第七號位置。存放的次序是由編譯器決定的,所以看到的是“#7”。通過分析第一行指令,我們可以看出猜測其它指令的含義還是比較簡單的。“ldc”(載入常量)指令將常量“Hello, World.”壓入運算元堆疊。“invokevirtual”激發println方法,此方法從運算元堆疊中彈出兩個引數。不要忘記象println這樣的方法有兩個引數:明顯的一個是字串引數,加上一個隱含的“this”引用。

二、Java位元組程式碼是怎樣使程式避免程式的記憶體錯誤

Java程式設計語言一直被稱為internet的安全語言。從表面上看,這些程式碼象典型的C++程式碼,安全從何而來?安全的重要方面是避免程式的記憶體錯誤。計算機罪犯利用程式的記憶體錯誤可以將他們的非法程式碼加到其它安全的程式中去。Java位元組程式碼是站在第一線抵禦這種攻擊的

1.型別安全檢測例項

以下的例子可以說明Java具體是怎樣做的。

public float add(float f, int n) {

return f + n;

}

如果你將這段程式碼加到第一個例子中去,重新編譯,執行javap,分析情況如下:

Method float add(float, int)

0 fload_1

1 iload_2

2 i2f

3 fadd

4 freturn

Java方法的開頭,虛擬機器將方法的引數放到一個被稱為舉辦變量表的資料結構中。從名字就可以看出,區域性變量表包含所有宣告的區域性變數。在本例中,方法從三個區域性變量表實體開始,這些是add方法的三個引數。位置0儲存該方法返回型別,位置12儲存浮點和整型引數。為了真正操縱變數,它們必須被裝載(壓)到運算元堆疊。第一條指令fload_1將浮點引數壓到運算元堆疊的位置1。第二條指令iload_2將整型引數壓到運算元堆疊的位置2。有趣的是這些指令的字首是以“i”和“f”開頭的,這表明Java位元組程式碼的指令按嚴格的型別劃分的。如果引數型別與位元組程式碼的引數型別不符合,虛擬機器將拒絕不安全的位元組程式碼。更妙的是,位元組程式碼被設計成僅執行一次

型別安全檢查——當載入類的時候。

2.Java中的型別安全檢測

型別安全是怎樣增強系統安全性的呢?如果攻擊者可以讓虛擬機器將整型變數當成浮點變數,或更嚴重更多,很容易預見計算的崩潰。如果計算是發生在銀行賬戶上的,牽連的安全問題是很明顯的。更危險的是欺騙虛擬機器將整型變數程式設計一個物件引用。在大多數情況下,虛擬機器將崩潰,但是攻擊者只要找到一個漏洞即可。不要忘記攻擊者不需要手工查詢——更好且容易的辦法是寫一個程式產生大量變換的壞的位元組程式碼,直到找到一個可以危害虛擬機器的。

另一種位元組程式碼保護記憶體安全的是陣列操作。“aastore”和“aaload”位元組程式碼操作Java陣列,而它們一直要檢查陣列的邊界。當呼叫者超越陣列邊界時,這些位元組程式碼將產生陣列溢位錯誤(ArrayIndexOutOfBoundsException)。也許所有應用中最重要的檢測是分支指令,例如,以“if.”開始的位元組程式碼。在位元組程式碼中,分支指令在同一個方法中只能跳轉到另一條指令。向方法之外傳遞控制的唯一辦法是返回,產生一個異常,或執行一個喚醒(invoke)指令。這不僅關閉了許多易受攻擊的大門,也防止由伴隨引用和堆疊的崩潰導致的可惡的程式錯誤。如果你曾經用系統偵錯程式開啟過程式碼中隨機定位的程式,你對這些程式錯誤會很熟悉。需要著重指出的是:所有的這些檢測是由虛擬機器在位元組程式碼級上完成的,不僅僅是編譯器。其它程式語言的編譯器象C++的,可以防止一些我們在上面討論過的記憶體錯誤,但這些保護是基於原始碼級的。作業系統將讀入執行任何機器程式碼,而不管這些程式碼是由小心翼翼的C++編譯器還是由邪惡的攻擊者產生的。簡單地說,C++是在源程式級上是面向物件的,而Java的面向物件特性擴充套件到已經編譯好的位元組程式碼上。

三、怎樣通過分析位元組程式碼來提高程式的執行效率

不管你注意它們與否,Java位元組程式碼的記憶體和安全保護都客觀存在,那為什麼還要那麼麻煩去看位元組程式碼呢?其實,就如在DOS下深入理解彙編就可以寫出更好的C++程式碼一樣,瞭解編譯器怎樣將你的程式碼翻譯成位元組程式碼可幫助你寫出更有效率的程式碼,有時候甚至可以防止不知不覺的程式錯誤。

1.為什麼在進行字串合併時要使用StringBuffer來代替String

我們看以下程式碼:

//Return the concatenation str1+str2

String concat(String str1, String str2) {

return str1 + str2;

}

//Append str2 to str1

void concat(StringBuffer str1, String str2) {

str1.append(str2);

}

試想一下每個方法需要執行多少函式。編譯該程式並執行javap,輸出結果如下:

Method java.lang.String concat(java.lang.String, java.lang.String)

0 new #6 <Class java.lang.StringBuffer>

3 dup

4 aload_1

5 invokestatic #14 <Method java.lang.String valueOf(java.lang.Object)>

8 invokespecial #9 <Method java.lang.StringBuffer(java.lang.String)>

11 aload_2

12 invokevirtual #10 <Method java.lang.StringBuffer append(java.lang.String)>

15 invokevirtual #13 <Method java.lang.String toString()>

18 areturn

Method void concat(java.lang.StringBuffer, java.lang.String)

0 aload_1

1 aload_2

2 invokevirtual #10 <Method java.lang.StringBuffer append(java.lang.String)>

5 pop

6 return

第一個concat方法有五個方法呼叫:newinvokestaticinvokespecial和兩個invokevirtual。這比第二個cacat方法多了好多些工作,而第二個cacat只有一個簡單的invokevirtual呼叫。String類的一個特點是其例項一旦建立,是不能改變的,除非重新給它賦值。在我們學習Java程式設計時,就被告知對於字串連線來說,使用StringBuffer比使用String更有效率。使用javap分析這點可以清楚地看到它們的區別。如果你懷疑兩種不同語言架構在效能上是否相同時,就應該使用javap分析位元組程式碼。不同的Java編譯器,其產生優化位元組程式碼的方式也不同,利用javap也可以清楚地看到它們的區別。以下是JBuilder產生位元組程式碼的分析結果:

Method java.lang.String concat(java.lang.String, java.lang.String)

0 aload_1

1 invokestatic #5 <Method java.lang.String valueOf(java.lang.Object)>

4 aload_2

5 invokestatic #5 <Method java.lang.String valueOf(java.lang.Object)>

8 invokevirtual #6 <Method java.lang.String concat(java.lang.String)>

11 areturn

可以看到經過JBuilder的優化,第一個concat方法有三個方法呼叫:兩個invokestatic invokevirtual。這還是沒有第二個concat方法簡潔。

不管怎樣,熟悉即時編譯器(JIT, Just-in-time)。因為當某個方法被第一次呼叫時,即時編譯器將對該虛擬方法表中所指向的位元組程式碼進行編譯,編譯完後表中的指標將指向編譯生成的機器碼,這樣即時編譯器將位元組程式碼重新編譯成本機程式碼,它可以使你進行更多javap分析沒有揭示的程式碼優化。除非你擁有虛擬機器的原始碼,你應當用效能基準來進行位元組程式碼分析。

2.防止應用程式中的錯誤以下的例子說明如何通過檢測位元組程式碼來幫助防止應用程式中的錯誤。首先建立兩個公共類,它們必須存放在兩個不同的檔案中。

public class ChangeALot {

// Variable

public static final boolean debug=false;

public static boolean log=false;

}

public class EternallyConstant {

// Methods

public static void main(String [] args) {

System.out.println("EternallyConstant beginning execution");

if (ChangeALot.debug)

System.out.println("Debug mode is on");

if (ChangeALot.log)

System.out.println("Logging mode is on");

}

}

如果執行EternallyConstant類,應該得到如下資訊:

EternallyConstant beginning execution.

現在我們修改ChangeALot檔案,將debuglog變數的值都設定為true。只重新編譯ChangeALot檔案,再執行EternallyConstant,輸出

結果如下:

EternallyConstant beginning execution

Logging mode is on

在除錯模式下怎麼了?即使設定debugtrue,“Debug mode is on”還是打印不出來。答案在位元組編碼中。執行javap分析EternallyConstant類,可看到如下結果:

Compiled from EternallyConstant.java

public class EternallyConstant extends java.lang.Object {

public EternallyConstant();

public static void main(java.lang.String[]);

}

Method EternallyConstant()

0 aload_0

1 invokespecial #1 <Method java.lang.Object()>

4 return

Method void main(java.lang.String[])

0 getstatic #2 <Field java.io.PrintStream out>

3 ldc #3 <String "EternallyConstant beginning execution">

5 invokevirtual #4 <Method void println(java.lang.String)>

8 getstatic #5 <Field boolean log>

11 ifeq 22

14 getstatic #2 <Field java.io.PrintStream out>

17 ldc #6 <String "Logging mode is on">

19 invokevirtual #4 <Method void println(java.lang.String)>

22 return

很奇怪吧!由於有“ifep”檢測log欄位,程式碼一點都不檢測debug欄位。因為debug欄位被標記為final,編譯器知道debug欄位在執行過程中不會改變。所以“if”語句被優化,分支部分被移去了。這是一個非常有用的優化,因為這使你可以在引用程式中嵌入除錯程式碼,而設定為false時不用付出代價,不幸的是這會導致編譯混亂。如果改變了final欄位,記住重新編譯其它引用該欄位的類。這就是引用有可能被優化的原因。Java開發工具不是每次都能檢測這個細微的改變,這些可能導致臨時的非常程式錯誤。在這裡,古老的C++格言對於Java環境來說一樣成立:“每當迷惑不解時,重新編譯所有程式。

四、利用第三方工具反編譯Java位元組程式碼

以上介紹了利用javap來分析Java位元組程式碼,實際上,利用第三方的工具,可以直接得到原始碼。這樣的工具有很多,其中NMI's Java Code Viewer (NJCV)是其中使用起來比較方便的一種。

1NMI's Java Code Viewer簡介

NJCV針對編譯好的Java位元組編碼,即.class檔案、.zip.jar檔案。.jar檔案實際上就是.zip檔案。利用NJCV這類反編譯工具,可以進一步除錯、監聽程式錯誤,進行安全分析等等。通過分析一些非常優秀的Java程式碼,我們可以從中學到許多開發Java程式的技巧。

NMI's Java Code Viewer 的最新版本是4.8.3,而且只能執行在以下Windows平臺:

lWindows 95/98

lWindows 2000

lWindows NT 3.51/4.0

2. NMI's Java Code Viewer應用例項

我們以前面例舉到的ExampleOfByteCode.class作為例子。開啟File選單中的open選單,開啟Java位元組程式碼檔案,Java class files中列出了所有與該檔案在同一個目錄的檔案。選擇要反編譯的檔案,然後在Process選單中選擇DecompileDissasemble,反編譯好的檔案列在Souce-code files一欄。用NMI's Java Code Viewer提供的Programmers File Editor開啟該檔案,瞧,原始碼都列出來了。

// Processed by NMI's Java Code Viewer 4.8.3 © 1997-2000 B. Lemaire

// Website: http://njcv.htmlplanet.comE-mail: [email protected]

// Copy registered to Evaluation Copy

// Source File Name:ExampleOfByteCode.java

import java.io.PrintStream;

public class ExampleOfByteCode {

public ExampleOfByteCode() {

}

public static void main(String args[]) {

System.out.println("Hello world");

}

public float add(float f, int n) {

return f + (float)n;

}

String concat(String str1, String str2) {

return str1 + str2;

}

void concat(StringBuffer str1, String str2) {

str1.append(str2);

}

}

NMI's Java Code Viewer也支援直接從jar/zip檔案中提取類檔案。反編譯好的檔案預設用.nmi副檔名存放,使用者可以設定.java副檔名。編輯原始檔時可以使用NJCV提供的編輯器,使用者可以選擇自己喜歡的編輯器。其結果與原檔案相差不大,相信大家會喜歡它。

五、結束語

瞭解一些位元組程式碼可以幫助從事Java程式程式語言的程式設計師們程式設計。javap工具使察看位元組程式碼變得非常容易,第三方的一些工具使程式碼的反編譯易如反掌。經常使用javap檢測程式碼,利用第三方工具反編程式碼,對於找到特別容易忘記的程式錯誤、提高程式執行效率、提高系統的安全性和效能來說,其價值是無法估量的。隨著Java程式設計技術的發展,Java類庫不斷完善,利用Java優越的跨平臺效能開發的應用軟體也越來越多。OracleJava編寫了Oracle

8iEnterprise Manager,以及其資料庫的安裝程式;Inprise公司的Borland JBuilder 3.5也用Java寫成;一些Internet電話也使用了Java技術,如MediaRingDialPad的網路電話採用了Java的解決方案;甚至以上提到的NMI's Java Code Viewer也是用Java寫成的。Java2已使Java得執行效能基本接近C++程式的執行速度,結合Enterprise JavaBeanServlet以及COBRARMI技術,Java的功能會越來越強大,其應用也將日益廣泛。

參考文獻:

1.Think in Java (Prentice Hall) Bruce Eckel

2.Sun Java Web Site – JDC Tech Tips

3.Java in a Nutshell (O`eilly and Assoc.) Mike Loukides, ed.

4.Just Java 2 (Prentice Hall) Peter van der Linden

5.The Java Virtual Machine Specifications (Addison Wesley) Tim Lindholm and Frank Yellin

轉自: http://www.comprg.com.cn/detail.asp?hw_id=2632