JAVA編譯器: Javac編譯器 Javac編譯器讀取Java原始碼,並將其編譯成位元組代碼(bytecode),位元組代碼就是在Java虛擬機器內執行的Java代碼的可執行形式。也稱為解釋程式碼(interpreted code)、虛擬碼或p-代碼。
Java 二進位位元組碼檔案解析:參考這篇文章:http://wenku.baidu.com/view/23692b60ddccda38376baf4f.html
還可以看看這篇文章:深入Java編程:Java的位元組代碼:http://tech.ddvip.com/2010-03/1269574451148518.html下面就是轉載的這篇文章,看了下,雖然不是很明白,但是還是懂那麼一點點。
Java程式員很少注意程式的編譯結果。事實上,Java的位元組代碼向我們提供了非常有價值的資訊。特別是在調試排除Java效能問題時,編譯結果讓我們可以更深入地理解如何提高程式執行的效率等問題。其實JDK使我們研究Java位元組代碼變得非常容易。本文闡述怎樣利用JDK中的工具查看解釋Java位元組代碼,主要包含以下方面的一些內容:
l Java類分解器——javap
l Java位元組代碼是怎樣使程式避免程式的記憶體錯誤
l 怎樣通過分析位元組代碼來提高程式的執行效率
l 利用第三方工具反編譯Java位元組代碼
一、Java類分解器——javap
大多數Java程式員知道他們的程式不是編譯成機器碼的。實際上,程式被編譯成中間位元組代碼,由Java虛擬機器來解釋執行。然而,很少程式員注意一下位元組代碼,因為他們使用的工具不鼓勵他們這樣做。大多數的Java調試工具不允許單步的位元組代碼調試。這些工具要麼顯示原始碼,要麼什麼都不顯示。
幸好JDK提供了Java類分解器javap,一個命令列工具。javap對類名給定的檔案(.class)提供的位元組代碼進行反編譯,列印出這些類的一個可讀版本。在預設情況下,javap列印出給定類內的公用域、方法、建構函式,以及靜態初始值。
1.javap的具體用法
文法: javap <選項> <類名>...
其中選項包括:
| 參數 |
含義 |
| b |
向後相容JDK 1.1中的javap |
| c |
反編譯代碼,列印出每個給定類中方法的Java虛擬機器指令。使用該選項後,將對包括私人及受保護方法在內的所有方法進行反編譯 |
| classpath <pathlist> |
指明到哪裡尋找使用者的類檔案。這個選項值覆蓋了缺少路徑以及由CLASSPATH環境變數定義的路徑。此處給出的路徑是一個目錄及zip檔案有序列表,其元素在Unix中以“:”,在Windows中以“;”分隔。要想在不覆蓋預設系統類別路徑的情況下增加一些要尋找的目錄或zip檔案,應使用CLASSPATH環境變數,使用方法與編譯器的-classpath相同。 |
| extdirs <dirs> |
覆蓋安裝擴充目錄 |
| help |
顯示協助資訊 |
| J<flag> |
將<flag>直接傳遞給運行系統 |
| l |
在原來列印資訊的基礎上,增加行號和局部變數表 |
| public |
只顯示公用類及其成員 |
| protected |
顯示受保護/公用類及其成員 |
| package |
顯示包受保護/公用類及其成員(預設) |
| private |
顯示所有類及其成員 |
| s |
列印內部類型標記 |
| bootclasspath <pathlist> |
覆蓋由引導類載入器載入的類檔案位置 |
| verbose |
列印堆棧大小,方法的局部變數和參數的數目。若可驗證,列印出錯原因 |
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儲存該方法傳回型別,位置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
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方法有五個方法調用: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 <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檔案,將debug和log變數的值都設定為true。只重新編譯ChangeALot檔案,再運行EternallyConstant,輸出結果如下:
EternallyConstant beginning execution
Logging mode is on
在偵錯模式下怎麼了?即使設定debug為true,“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)是其中使用起來比較方便的一種。
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位元組代碼檔案,Java class files中列出了所有與該檔案在同一個目錄的檔案。選擇要反編譯的檔案,然後在Process菜單中選擇Decompile或Dissasemble,反編譯好的檔案列在Souce-code files一欄。用NMI's Java Code Viewer提供的Programmer’s File Editor開啟該檔案,瞧,原始碼都列出來了。
// Processed by NMI's Java Code Viewer 4.8.3 ? 1997-2000 B. Lemaire
// Website: http://njcv.htmlplanet.com E-mail: info@njcv.htmlplanet.com
// 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優越的跨平台效能開發的應用軟體也越來越多。Oracle用Java編寫了Oracle 8i的Enterprise Manager,以及其資料庫的安裝程式;Inprise公司的Borland
JBuilder 3.5也用Java寫成;一些Internet電話也使用了Java技術,如MediaRing、DialPad的網路電話採用了Java的解決方案;甚至以上提到的NMI's Java Code Viewer也是用Java寫成的。Java2已使Java得運行效能基本接近C++程式的執行速度,結合Enterprise JavaBean、Servlet以及COBRA、RMI技術,Java的功能會越來越強大,其應用也將日益廣泛。
轉載:http://blog.163.com/xiaopengyan_109/blog/static/149832173201062504753376/