大多Java程序員知道他們的程序通常不會被編譯為本機代碼而是被編譯為由java虛擬機(JVM)執行的字節碼格式然而很少有java程序員曾經看過字節碼因為他們的工具不鼓勵他們去看大多Java 調試工具不允許單步執行字節碼它們要麼顯示源代碼行要麼什麼也不顯示
幸運的是JDK提供了javap一個命令行工具它使得查看字節碼很容易讓我們看一個范例
public class ByteCodeDemo {
public static void main(String[] args) {
Systemoutprintln(Hello world);
}
}
在編譯這個類後你可以用十六進制編輯器打開class文件然後參照虛擬機規范翻譯字節碼幸運的是有更簡單的方法JDK包含一個命令行的反匯編器javap它可以轉換字節碼為一種可讀的助記符形式可以像下面這樣通過傳遞c參數給javap得到字節碼列表
javap c ByteCodeDemo
你應該會看到輸出類似這樣
public class ByteCodeDemo extends javalangObject {
public ByteCodeDemo();
public static void main(javalangString[]);
}
Method ByteCodeDemo()
aload_
invokespecial #
return
Method void main(javalangString[])
getstatic #
ldc #
invokevirtual #
return
僅僅從這個短小的列表你可以學到很多字節碼的知識從main方法的第一個指令開始
getstatic #
開始的整數是方法中的指令的偏移值因此第一個指令以開始緊隨偏移量是指令的助記符(mnemonic)在這個范例中getstatic 指令將一個靜態成員壓入一個稱為操作數堆棧的數據結構後續的指令可以引用這個數據結構中的成員getstatic 指令後是要壓入的成員在這個例子中要壓入的成員是# 如果你直接檢查字節碼你會看到成員信息沒有直接嵌入指令而是像所有由java類使用的常量那樣存儲在一個共享池中將成員信息存儲在一個常量池中可以減小字節碼指令的大小因為指令只需要存儲常量池中的一個索引而不是整個常量在這個例子中成員信息位於常量池中的#處常量池中的項目的順序是和編譯器相關的因此在你的環境中看到的可能不是#
分析完第一個指令後很容易猜到其它指令的意思ldc (load constant) 指令將常量Hello World壓入操作數棧invokevirtual指令調用println方法它從操作數棧彈出它的兩個參數不要忘記一個像println這樣的實例方法有兩個參數上面的字符串加上隱含的this引用
字節碼如何預防內存錯誤
Java語言經常被吹捧為開發互聯網軟件的安全的語言表面上和c++如此相似的代碼如何體現安全呢?它引入的一個重要的安全概念是防止內存相關的錯誤計算機罪犯利用內存錯誤在其它情況下安全的程序中插入自己的惡意的代碼Java字節碼是第一個可以預防這種攻擊的像下面的范例展示的
public float add(float f int n) {
return f + n;
}
如果你將這個方法加入上面的范例中重新編譯它然後運行javap你將看到的字節碼類似這個
Method float add(float int)
fload_
iload_
if
fadd
freturn
在方法的開始虛擬機將方法的參數放入一個稱為局部變量表的數據結構中將像名字暗示的那樣局部變量表也包含了你聲明的任何局部變量在這個例子中方法以三個局部變量表的項開始這些都是add方法的參數位置保存this引用而位置和分別保存float和int參數
為了實際的操作這些變量它們必須被加載(壓入)到操作數棧第一個指令fload_將位置處的float壓入操作數棧第二個指令iload_將位置處的int壓入操作數棧這些指令的一個引起注意的事情是指令中的i和f前綴這說明Java字節碼指令是強類型的如果參數的類型和字節碼的類型不匹配VM將該字節碼作為不安全的而加以拒絕更好的是字節碼被設計為只需在類被加載時執行一次這樣的類型安全檢查
這個類型安全是如何加強安全的?如果一個攻擊者能夠欺騙虛擬機將一個int作為一個float或者相反它就可以很容易的以一個預期的的方法破壞計算如果這些計算涉及銀行結余那麼隱含的安全性是很明顯的更危險的是欺騙VM將一個int作為一個Object引用在大多情況下這將導致VM崩潰但是攻擊者只需要找到一個漏洞不要忘記攻擊者不會手工搜索這個漏洞--寫出一個程序產生數以億計的錯誤字節碼的排列是相當容易的這些排列試圖找到危害VM的幸運的那個
字節碼的另一個內存安全防護是數組操作aastore 和 aaload 字節碼操作Java數組並且它們總是檢查數組邊界如果調用程序越過了數組尾這些字節碼將拋出一個ArrayIndexOutOfBoundsException也許所有最重要的檢查都使用分支指令例如以if開始的字節碼在字節碼中分支指令只能轉移到同一方法中的其它指令在方法外可以傳遞的唯一控制是使它返回拋出一個異常或者執行一個invoke指令這不僅關閉了很多攻擊同時也防止由於搖蕩引用(dangling reference)或者堆棧沖突而引發的令人厭惡的錯誤如果你曾經使用系統調試器打開你的程序並定位到代碼中的一個隨機的位置那麼你會很熟悉這些錯誤
所有這些檢查中需要記住的重要的一點是它們是由虛擬機在字節碼級進行的而不是僅僅由編譯器在源代碼級進行的一個例如c++這樣的語言的編譯器可能在編譯時預防上面討論的某些內存錯誤但是這些保護只是在源代碼級應用操作系統將很樂意加載執行任何機器碼無論這些代碼是由精細的c++編譯器產生的還是心懷惡意的攻擊者產生的簡單的講C++僅僅是在源代碼級上面向對象而Java的面向對象的特性擴展到編譯過的代碼級
[] []
From:http://tw.wingwit.com/Article/program/Java/hx/201311/27209.html