翻譯:Cherami
開(kāi)始使用JAVAP
大多Java
程序員知道他們的
程序通常不會(huì )被編譯為本機代碼而是被編譯為由
java虛擬機(JVM)執行的字節碼格式。然而,很少有
java程序員曾經(jīng)看過(guò)字節碼因為他們的工具不鼓勵他們去看。大多Java 調試工具不允許單步執行字節碼,它們要么顯示源代碼行,要么什么也不顯示。
幸運的是JDK提供了
javap,一個(gè)命令行工具,它使得查看字節碼很容易。讓我們看一個(gè)范例:
public class ByteCodeDemo {
public static void main(String[] args) {
System.out.println("Hello world");
}
}
在編譯這個(gè)類(lèi)后,你可以用十六進(jìn)制編輯器打開(kāi).class
文件然后參照虛擬機規范翻譯字節碼。幸運的是有更簡(jiǎn)單的方法。JDK包含一個(gè)命令行的反匯編器:
javap,它可以轉換字節碼為一種可讀的助記符形式,可以像下面這樣通過(guò)傳遞‘-c‘參數給
javap得到字節碼列表:
javap -c ByteCodeDemo
你應該會(huì )看到輸出類(lèi)似這樣:
public class ByteCodeDemo extends
java.lang.Object {
public ByteCodeDemo();
public static void main(
java.lang.String[]);
}
Method ByteCodeDemo()
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 "Hello world">
5 invokevirtual #4 <Method void println(
java.lang.String)>
8 return
僅僅從這個(gè)短小的列表你可以學(xué)到很多字節碼的知識。從main方法的第一個(gè)指令開(kāi)始:
0 getstatic #2 <Field
java.io.PrintStream out>
開(kāi)始的整數是方法中的指令的偏移值,因此第一個(gè)指令以0開(kāi)始。緊隨偏移量是指令的助記符(mnemonic)。在這個(gè)范例中,‘getstatic‘ 指令將一個(gè)靜態(tài)成員壓入一個(gè)稱(chēng)為操作數堆棧的數據結構,后續的指令可以引用這個(gè)數據結構中的成員。getstatic 指令后是要壓入的成員。在這個(gè)例子中,要壓入的成員是"#2 <Field
java類(lèi)使用的常量那樣存儲在一個(gè)共享池中。將成員信息存儲在一個(gè)常量池中可以減小字節碼指令的大小,因為指令只需要存儲常量池中的一個(gè)索引而不是整個(gè)常量。在這個(gè)例子中,成員信息位于常量池中的#2處。常量池中的項目的順序是和編譯器相關(guān)的,因此在你的環(huán)境中看到的可能不是‘#2‘ 。
分析完第一個(gè)指令后很容易猜到其它指令的意思?!甽dc‘ (load constant) 指令將常量"Hello, World."壓入操作數棧?!甶nvokevirtual‘指令調用println方法,它從操作數棧彈出它的兩個(gè)參數。不要忘記一個(gè)像println這樣的實(shí)例方法有兩個(gè)參數:上面的
字符串,加上隱含的‘this‘引用。
字節碼如何預防內存錯誤
Java語(yǔ)言經(jīng)常被吹捧為開(kāi)發(fā)互聯(lián)網(wǎng)軟件的"安全的"語(yǔ)言。表面上和c++如此相似的代碼如何體現安全呢?它引入的一個(gè)重要的安全概念是防止內存相關(guān)的錯誤。計算機罪犯利用內存錯誤在其它情況下安全的
程序中插入自己的惡意的代碼。Java字節碼是第一個(gè)可以預防這種攻擊的,像下面的范例展示的:
public float add(float f, int n) {
return f + n;
}
如果你將這個(gè)方法加入上面的范例中,重新編譯它,然后運行
javap,你將看到的字節碼類(lèi)似這個(gè):
Method float add(float, int)
0 fload_1
1 iload_2
2 i2f
3 fadd
4 freturn
在方法的開(kāi)始,虛擬機將方法的參數放入一個(gè)稱(chēng)為局部變量表的數據結構中。將像名字暗示的那樣,局部變量表也包含了你聲明的任何局部變量。在這個(gè)例子中,方法以三個(gè)局部變量表的項開(kāi)始,這些都是add方法的參數,位置0保存this引用,而位置1和2分別保存float和int參數。
為了實(shí)際的操作這些變量,它們必須被加載(壓入)到操作數棧。第一個(gè)指令fload_1將位置1處的float壓入操作數棧,第二個(gè)指令iload_2將位置2處的int壓入操作數棧。這些指令的一個(gè)引起注意的事情是指令中的‘i‘和‘f‘前綴,這說(shuō)明Java字節碼指令是強類(lèi)型的。如果參數的類(lèi)型和字節碼的類(lèi)型不匹配,VM將該字節碼作為不安全的而加以拒絕。更好的是,字節碼被設計為只需在類(lèi)被加載時(shí)執行一次這樣的類(lèi)型安全檢查。
這個(gè)類(lèi)型安全是如何加強安全的?如果一個(gè)攻擊者能夠欺騙虛擬機將一個(gè)int作為一個(gè)float或者相反,它就可以很容易的以一個(gè)預期的的方法破壞計算。如果這些計算涉及銀行結余,那么隱含的安全性是很明顯的。更危險的是欺騙VM將一個(gè)int作為一個(gè)Object引用。在大多情況下,這將導致VM崩潰,但是攻擊者只需要找到一個(gè)漏洞。不要忘記攻擊者不會(huì )手工搜索這個(gè)漏洞--寫(xiě)出一個(gè)
程序產(chǎn)生數以?xún)|計的錯誤字節碼的排列是相當容易的,這些排列試圖找到危害VM的幸運的那個(gè)。
字節碼的另一個(gè)內存安全防護是數組操作?!產(chǎn)astore‘ 和 ‘a(chǎn)aload‘ 字節碼操作Java數組并且它們總是檢查數組邊界。如果調用
程序越過(guò)了數組尾,這些字節碼將拋出一個(gè)ArrayIndexOutOfBoundsException。也許所有最重要的檢查都使用分支指令,例如,以if開(kāi)始的字節碼。在字節碼中,分支指令只能轉移到同一方法中的其它指令。在方法外可以傳遞的唯一控制是使它返回:拋出一個(gè)異?;蛘邎绦幸粋€(gè)‘invoke‘指令。這不僅關(guān)閉了很多攻擊,同時(shí)也防止由于搖蕩引用(dangling reference)或者堆棧沖突而引發(fā)的令人厭惡的錯誤。如果你曾經(jīng)使用
系統調試器打開(kāi)你的
程序并定位到代碼中的一個(gè)隨機的位置,那么你會(huì )很熟悉這些錯誤。
所有這些檢查中需要記住的重要的一點(diǎn)是它們是由虛擬機在字節碼級進(jìn)行的而不是僅僅由編譯器在源代碼級進(jìn)行的。一個(gè)例如c++這樣的語(yǔ)言的編譯器可能在編譯時(shí)預防上面討論的某些內存錯誤,但是這些保護只是在源代碼級應用。操作
系統將很樂(lè )意加載執行任何機器碼,無(wú)論這些代碼是由精細的c++編譯器產(chǎn)生的還是心懷惡意的攻擊者產(chǎn)生的。簡(jiǎn)單的講,C++僅僅是在源代碼級上面向對象而Java的面向對象的特性擴展到編譯過(guò)的代碼級。
分析字節碼提升代碼質(zhì)量
Java字節碼的內存和安全保護無(wú)論我們是否注意都是存在地,那么我們?yōu)槭裁催€費心查看字節碼呢?在很多情況下,知道編譯器如何將你的代碼轉換為字節碼可以幫助你寫(xiě)出更高效的代碼,而且在某些情況下可以防止不易發(fā)覺(jué)的錯誤??紤]下面的例子:
//返回 str1+str2 的串連
String concat(String str1, String str2) {
return str1 + str2;
}
//將 str2 附加到 str1
void concat(StringBuffer str1, String str2) {
str1.append(str2);
}
猜猜每個(gè)方法需要多少個(gè)方法調用?,F在編譯這些方法并且運行
javap,你會(huì )得到類(lèi)似下面的輸出:
Method
java.lang.String concat1(
java.lang.String,
java.lang.String)
0 new #5 <Class
java.lang.StringBuffer>
3 dup
4 invokespecial #6 <Method
java.lang.StringBuffer()>
7 aload_1
8 invokevirtual #7 <Method
java.lang.StringBuffer append(
java.lang.String)>
11 aload_2
12 invokevirtual #7 <Method
java.lang.StringBuffer append(
java.lang.String)>
15 invokevirtual #8 <Method
java.lang.String toString()>
18 areturn
Method void concat2(
java.lang.StringBuffer,
java.lang.String)
0 aload_1
1 aload_2
2 invokevirtual #7 <Method
java.lang.StringBuffer append(
java.lang.String)>
5 pop
6 return
concat1方法執行了5個(gè)方法調用s: new, invokespecial和三個(gè)invokevirtuals,這比concat2方法執行了更多的工作,后者只執行了一個(gè)invokevirtual調用。大多Java
程序員已經(jīng)得到過(guò)警告,因為String是不可變的,而使用StringBuffer進(jìn)行
字符串連接效率更高。使用
javap分析這個(gè)使得這點(diǎn)變得很生動(dòng)。如果你不能肯定兩個(gè)語(yǔ)言構造在性能上是否相等,你應該使用
javap分析字節碼。然而,對just-in-time (JIT)編譯器要小心,因為JIT編譯器將字節碼重新編譯為本機代碼而能執行一些
javap不能揭示的附加優(yōu)化。除非你有你的虛擬機的源代碼,否則你應該補充你的字節碼的基準性能分析。
最后的一個(gè)范例展示了檢查字節碼如何幫助防止
程序中的錯誤。像下面那樣創(chuàng )建兩個(gè)類(lèi),確保它們在獨立的
文件中。
public class ChangeALot {
public static final boolean debug=false;
public static boolean log=false;
}
public class EternallyConstant {
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,你會(huì )得到信息:
EternallyConstant beginning execution.
現在試著(zhù)編輯ChangeALot,修改debug和log變量的值為true(兩個(gè)都為true)。只重新編譯ChangeALot。再次運行EternallyConstant,你將看到下面的輸出:
EternallyConstant beginning execution
Logging mode is on
debug變量怎么了?即使你將debug設置為true,信息"Debug mode is on"并沒(méi)有出現。答案在字節碼中。對 EternallyConstant運行
javap你會(huì )看到:
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
驚奇吧!在log成員上有一個(gè)‘ifeq‘檢查,而代碼根本沒(méi)有檢查debug成員。因為debug成員被標記為final類(lèi)型,編譯器知道debug成員在運行時(shí)永遠不會(huì )改變,因此它通過(guò)移除‘if‘聲明進(jìn)行優(yōu)化。這確實(shí)是一個(gè)非常有用的優(yōu)化,因為它允許你在
程序中嵌入調試代碼而在將它設置為false時(shí)不用付出運行時(shí)的代價(jià)。不幸的是這個(gè)優(yōu)化能夠導致主要的編譯時(shí)混亂。如果你改變一個(gè)final成員,你必須記住重新編譯任何可能引用該成員的類(lèi)。這是因為這個(gè)‘reference‘可能已經(jīng)經(jīng)過(guò)優(yōu)化了。Java開(kāi)發(fā)環(huán)境不能總是發(fā)現這個(gè)微妙的相關(guān)性,一些能導致非常奇怪的錯誤。因此,古老的C++格言對于
字節碼和VM是相當的復雜的,這已經(jīng)超過(guò)這個(gè)技巧可以涵蓋的范圍。要知道更多東西,看看Bill Venners寫(xiě)的《Inside the Java Virtual Machine》(譯者注: