Java 反彙編、反編譯、volitale解讀

擁抱心中的夢想發表於2018-08-14

曾經小小少年,到如今風度翩翩!曾幾何時,每次想了解Java中volatile關鍵字的實現原理時,小編都會去百度找部落格看,翻遍了許許多多的部落格,有講的深入的,有講的淺顯的,反正小編腦子是有點亂了。其中很多部落格講到其底層是給變數加了一條lock指令,真的是這樣的嗎?確實是。

下面我們就來驗證下到底這個lock指令是如何得出來的,以及介紹下檢視windows位元組碼的相關工具的使用,由於小編看得懂的位元組碼指令寥寥無幾,因此,暫時還不能每條指令具體分析,留到下篇部落格介紹。

一、Java位元組碼及class檔案反彙編

我們都知道,Java原始碼檔案想要執行,會被編譯器(javac)編譯為.class檔案,Java位元組碼檔案具備了相應的格式,而且非常嚴格(具體的class檔案格式,可以查閱《Java虛擬機器規範》)。由於.class檔案為二進位制檔案,因此我們無法直接使用文字檔案開啟檢視,如果要開啟,我們可以使用諸如Java Decompiler這類的工具來反編譯.class檔案。Java虛擬機器與傳統組合語言不同,它不直接使用底層的暫存器,而是設計成一臺基於棧的虛擬機器,在Java方法中,前面指令的執行結果先push進運算元棧,後面的指令如果需要使用到先前的結果,則從運算元棧中將值pop出來。而這些操作,底層Java虛擬器在讀取儲存出棧入棧等方面擁有許許多多的位元組碼指令支援,我們可以這樣子理解,如果說彙編指令屬於底層作業系統指令,那麼Java位元組碼指令屬於Java虛擬機器的指令,要想檢視.calss檔案中的位元組碼指令,我們可以使用JDK提供的工具javap進行反彙編。

Javap反彙編示例:

Java原始碼

/**
 * The class Hello.
 *
 * Description:反彙編、彙編測試用例
 *
 * @author: huangjiawei
 * @since: 2018年8月14日
 * @version: $Revision$ $Date$ $LastChangedBy$
 *
 */
public class Hello {
    private static volatile String name;
    public Hello() {}
    public static void say() {
    	for (int i = 0; i <= 1000; i++) {
    		System.out.println(i);
    		name = "huangjiawei";
    		System.out.println(name);
    	}
    }
    public static void main(String[] args) {
    	for (int i = 0; i <= 100; i++) {
    		say();
    	}
    }
}
複製程式碼

執行完javac Hello.java javap -c Hello.class之後得到下面位元組碼指令:

Compiled from "Hello.java"
public class Hello {
  public Hello();// 構造方法位元組碼指令
    Code:// 在x86架構中,組合語言.code標識代表指令程式碼區,.data表示資料區
       0: aload_0    // 這裡指令aload_0表示將this引用壓入棧中
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void say();// say方法
    Code:
       0: iconst_0
       1: istore_0
       2: iload_0
       3: sipush        1000
       6: if_icmpgt     36
       9: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: iload_0
      13: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      16: ldc           #4                  // String huang
      18: putstatic     #5                  // Field name:Ljava/lang/String;
      21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      24: getstatic     #5                  // Field name:Ljava/lang/String;
      27: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      30: iinc          0, 1
      33: goto          2
      36: return
  // main 方法指令區
  public static void main(java.lang.String[]);
    Code:
       0: iconst_0  // int 常量i
       1: istore_1
       2: iload_1
       3: bipush        100
       5: if_icmpgt     17
       8: invokestatic  #7                  // Method say:()V
      11: iinc          1, 1
      14: goto          2
      17: return
}
複製程式碼

安裝HSDIS 深入學習這些位元組碼指令,對於JVM調優非常有幫助,比如說判斷槽位是否複用、逃逸分析棧上分配等等。具體的位元組碼指令學習,可以查閱《Java虛擬機器規範》一書,英語好的話,建議線上官方地址:點我就好啦!

那麼,我們瞭解了Java的位元組碼指令,我們想一想,我們開發的Java應用程式最後還不是在Linux或者Windows上面進行執行,而我們又知道,和底層硬體最接近的就是組合語言了,那麼,我們能不能將class檔案轉換成特定平臺的彙編程式碼呢?答案是肯定的。下面我將介紹幾種檢視彙編程式碼的工具及其使用。

二、Hsdis 結合 JITWatch 檢視機器彙編程式碼

HSDIS是由Project Kenai(kenai.com/projects/ba… VM JIT編譯程式碼的反彙編外掛,作用是讓HotSpot的-XX:+PrintAssembly指令呼叫它來把動態生成的原生程式碼還原為彙編程式碼輸出,同時還生成了大量非常有價值的註釋,這樣我們就可以通過輸出的程式碼來分析問題。

windows上進行反彙編需要hsdis-amd64.dll這個外掛,因此我們需要生成這個外掛,然後將該外掛放置到我們的jreDir/bin/server目錄下,然後使用-XX:+PrintAssembly即可輸出彙編程式碼。這裡有個官方標準教程,由於是英文的,在這裡我將其中的步驟做一個簡單的總結:

  • 1、安裝Cygwinunix模擬環境

    安裝的過程中記得在select package視窗將下面的幾個包給加上:

    • gcc-core
    • mingw64-i686-gcc-core
    • mingw64-x86_64-gcc-core
    • patch
    • make
  • 2、下載GNU binutils 2.28,注意官方推薦是2.30版本,但是2.30版本後期make會有問題

  • 3、下載OpenJDK,詳細見官網。

  • 4,5,6步見官網描述吧!沒有坑,哈哈!

為了防止官網後面訪問不了,小編將html檔案下載儲存在github上了,詳見How to build hsdis-amd64.dll and hsdis-i386.dll on Windows

當你在命令列執行java -XX:+PrintAssembly -XX:+UnlockDiagnosticVMOptions Hello >> code.txt就會輸出大量彙編程式碼,如下圖:

Java 反彙編、反編譯、volitale解讀

Java 反彙編、反編譯、volitale解讀

但是,有沒有這樣一種更加直觀的方式,能夠具體檢視某個方法的彙編程式碼呢?答案是肯定的,下面出場的是jitwatch,它是一個開源專案,其github地址為:jitwatch

相對來說,jitwatch的安裝相對比較簡單,我們可以直接克隆專案,該專案支援三種編譯方式:

  • 如果使用ant編譯,請使用ant clean compile run執行
  • 如果使用gradle編譯構建,請使用gradlew clean build run
  • 如果使用maven構建,請使用mvn clean compile exec:java

啟動完專案之後大概就是這麼一個介面:

Java 反彙編、反編譯、volitale解讀

我們大概有兩種方式檢視我們的彙編程式碼:

  • 1、使用-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation -XX:LogFile=code.log指令執行,然後點選介面的Open Log按鈕將日誌檔案匯入,再start
  • 2、點選介面的Sandbox,配置相關引數,然後start

Java 反彙編、反編譯、volitale解讀

Java 反彙編、反編譯、volitale解讀

Java位元組碼檔案中有一個叫做行號表的屬性,存在於Code屬性中, 它建立了位元組碼偏移量到原始碼行號之間的聯絡。我們可以點選LNT按鈕,進行除錯:

Java 反彙編、反編譯、volitale解讀

最後,如果您正在使用JDK8,那麼您需要確保你寫的Java方法被呼叫的次數足夠多,以觸發C1(客戶端)編譯,並大約10000次觸發C2(伺服器)編譯器並開啟高階優化。換句話說,你要像檢視彙編程式碼,你寫的Java原始碼檔案不能太過於簡單,要足夠複雜,但我們第一節的Hello.java已經足夠了,同時jitwatch本身也提供了很多學習樣例,可以在JITWatchDir\sandbox\sources中獲得。

還記得最開始我們討論的volatile底層彙編程式碼lock指令嗎?檢視我們的彙編程式碼可以發現有這麼一行程式碼:

Java 反彙編、反編譯、volitale解讀

是吧!我們終於自己將lock指令找出來了,至於為什麼lock指令能夠保證記憶體一致性,我們首先需要從組合語言層面對lock指令的功能進行一番瞭解。在所有的 X86 CPU 上都具有鎖定一個特定記憶體地址的能力,當這個特定記憶體地址被鎖定後,它就可以阻止其他的系統匯流排讀取修改這個記憶體地址。這種能力是通過 LOCK 指令字首再加上下面的彙編指令來實現的。當使用 LOCK 指令字首時,它會使 CPU 宣告一個 LOCK# 訊號,這樣就能確保在多處理器系統或多執行緒競爭的環境下互斥地使用這個記憶體地址。當指令執行完畢,這個鎖定動作也就會消失。注意由於是記憶體互斥的,因此這個臨界區除了當前lock的執行緒擁有,其他執行緒都不能進入該臨界區。

2019年8月23日更新

匯流排加鎖lock是早起CPU的一種實現方式,最新的實現採用EMSI快取一致性協議來實現VOLITALE

大概流程就是多個執行緒都去嗅探(監聽)匯流排上的某個變數,一旦有執行緒將變數協會主記憶體時,其他執行緒將會第一時間監聽到變數的改變,效能比lock匯流排加鎖高很多。

相關文章