類分解器JavaP--分析Java字節…

原文:http://blog.csdn.net/wencool/article/details/4007922           作者:wencool


難以理解,留作交流學習,以備後用。


深入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打印出給定類內的公共域、方法、構造函數,以及靜態初始值。

 

1.javap的具體用法

語法: 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 {

    publicExampleOfByteCode();

    publicstatic void main(java.lang.String[]);

}

 

Method ExampleOfByteCode()

   0 aload_0

   1 invokespecial #6

   4 return

 

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

   0 getstatic #7

   3 ldc #1

   5 invokevirtual #8

   8 return

 

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

0 getstatic #7

開頭的初始數字是指令在方法中的偏移,所以第一個指令的偏移是0。緊跟偏移的是指令助記符。在本例中,getstatic指令將一個靜態字段壓入一個數據結構,我們稱這個數據結構爲操作數堆棧。後續指令可以通過此結構引用這個字段。緊跟getstatic指令後面的是壓到哪個字段中去。這裏的字段是“#7”。如果直接察看字節代碼,這些字段信息並沒有直接存放到指令中去。事實上,就象所有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保存該方法返回類型,位置1和2保存浮點和整型參數。爲了真正操縱變量,它們必須被裝載(壓)到操作數堆棧。第一條指令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

    Stringconcat(String str1, String str2) {

       return str1 + str2;

    }

 

    //Appendstr2 to str1

    voidconcat(StringBuffer str1, String str2) {

       str1.append(str2);

    }

 

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

 

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

   0 new #6

   3 dup

   4 aload_1

   5 invokestatic #14

   8 invokespecial #9

  11 aload_2

  12 invokevirtual #10

  15 invokevirtual #13

  18 areturn

 

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

   0 aload_1

   1 aload_2

   2 invokevirtual #10

   5 pop

   6 return

 

第一個concat方法有五個方法調用:new,invokestatic,invokespecial和兩個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

   4 aload_2

   5 invokestatic #5

   8 invokevirtual #6

  11 areturn

 

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

不管怎樣,熟悉即時編譯器(JIT,Just-in-time)。因爲當某個方法被第一次調用時,即時編譯器將對該虛擬方法表中所指向的字節代碼進行編譯,編譯完後表中的指針將指向編譯生成的機器碼,這樣即時編譯器將字節代碼重新編譯成本機代碼,它可以使你進行更多javap分析沒有揭示的代碼優化。除非你擁有虛擬機的源代碼,你應當用性能基準來進行字節代碼分析。

 

2.防止應用程序中的錯誤

以下的例子說明如何通過檢測字節代碼來幫助防止應用程序中的錯誤。首先創建兩個公共類,它們必須存放在兩個不同的文件中。

public class ChangeALot {
    //Variable
    publicstatic final boolean debug=false;
    publicstatic boolean log=false;
}
 
public class EternallyConstant {
    //Methods
    publicstatic 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文件,將debug和log變量的值都設置爲true。只重新編譯ChangeALot文件,再運行EternallyConstant,輸出


結果如下:

 

EternallyConstant beginning execution

Logging mode is on

在調試模式下怎麼了?即使設置debug爲true,“Debug mode ison”還是打印不出來。答案在字節編碼中。運行javap分析EternallyConstant類,可看到如下結果:

Compiled from EternallyConstant.java

public class EternallyConstant extends java.lang.Object {

    publicEternallyConstant();

    publicstatic void main(java.lang.String[]);

}

 

Method EternallyConstant()

   0 aload_0

   1 invokespecial #1

   4 return

 

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

   0 getstatic #2

   3 ldc #3

   5 invokevirtual #4

   8 getstatic #5

  11 ifeq 22

  14 getstatic #2

  17 ldc #6

  19 invokevirtual #4

  22 return

 

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

 

四、利用第三方工具反編譯Java字節代碼

以上介紹了利用javap來分析Java字節代碼,實際上,利用第三方的工具,可以直接得到源代碼。這樣的工具有很多,其中NMI'sJava Code Viewer (NJCV)是其中使用起來比較方便的一種。

 

1.NMI's Java Code Viewer簡介

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

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

l        Windows 95/98

l        Windows 2000

l        Windows NT 3.51/4.0

 

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

我們以前面例舉到的ExampleOfByteCode.class作爲例子。打開File菜單中的open菜單,打開Java字節代碼文件,Javaclassfiles中列出了所有與該文件在同一個目錄的文件。選擇要反編譯的文件,然後在Process菜單中選擇Decompile或Dissasemble,反編譯好的文件列在Souce-codefiles一欄。用NMI's Java Code Viewer提供的Programmer’s FileEditor打開該文件,瞧,源代碼都列出來了。

 

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

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

// Copy registered to Evaluation Copy

// Source File Name:  ExampleOfByteCode.java

 

import java.io.PrintStream;

 

public class ExampleOfByteCode {

 

    publicExampleOfByteCode() {

    }

 

    publicstatic void main(String args[]) {

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

    }

 

    publicfloat add(float f, int n) {

       return f + (float)n;

    }

 

    Stringconcat(String str1, String str2) {

       return str1 + str2;

    }

 

    voidconcat(StringBuffer str1, String str2) {

       str1.append(str2);

    }

}

 

NMI's Java CodeViewer也支持直接從jar/zip文件中提取類文件。反編譯好的文件缺省用.nmi擴展名存放,用戶可以設置.java擴展名。編輯源文件時可以使用NJCV提供的編輯器,用戶可以選擇自己喜歡的編輯器。其結果與原文件相差不大,相信大家會喜歡它。

 

五、結束語

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

8i的Enterprise Manager,以及其數據庫的安裝程序;Inprise公司的Borland JBuilder3.5也用Java寫成;一些Internet電話也使用了Java技術,如MediaRing、DialPad的網絡電話採用了Java的解決方案;甚至以上提到的NMI'sJava CodeViewer也是用Java寫成的。Java2已使Java得運行性能基本接近C++程序的執行速度,結合EnterpriseJavaBean、Servlet以及COBRA、RMI技術,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) TimLindholm and Frank Yellin


/**
*站在巨人的肩上才能看得更遠,一步一個腳印才能走得更遠。分享成長,交流進步,轉載請註明出處!
*/

發佈了14 篇原創文章 · 獲贊 23 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章