1. 程式人生 > >通過javap命令分析java彙編指令

通過javap命令分析java彙編指令

一、javap命令簡述

javap是jdk自帶的反解析工具。它的作用就是根據class位元組碼檔案,反解析出當前類對應的code區(彙編指令)、本地變量表、異常表和程式碼行偏移量對映表、常量池等等資訊。
當然這些資訊中,有些資訊(如本地變量表、指令和程式碼行偏移量對映表、常量池中方法的引數名稱等等)需要在使用javac編譯成class檔案時,指定引數才能輸出,比如,你直接javac xx.java,就不會在生成對應的區域性變量表等資訊,如果你使用javac -g xx.java就可以生成所有相關資訊了。如果你使用的eclipse,則預設情況下,eclipse在編譯時會幫你生成區域性變量表、指令和程式碼行偏移量對映表等資訊的。
通過反編譯生成的彙編程式碼,我們可以深入的瞭解java程式碼的工作機制。比如我們可以檢視i++;這行程式碼實際執行時是先獲取變數i的值,然後將這個值加1,最後再將加1後的值賦值給變數i。
通過區域性變量表,我們可以檢視區域性變數的作用域範圍、所在槽位等資訊,甚至可以看到槽位複用等資訊。

javap的用法格式:
javap <options> <classes>
其中classes就是你要反編譯的class檔案。
在命令列中直接輸入javap或javap -help可以看到javap的options有如下選項:

 -help  --help  -?        輸出此用法訊息
 -version                 版本資訊,其實是當前javap所在jdk的版本資訊,不是class在哪個jdk下生成的。
 -v  -verbose             輸出附加資訊(包括行號、本地變量表,反彙編等詳細資訊)
 -l                         輸出行號和本地變量表
 -public                    僅顯示公共類和成員
 -protected               顯示受保護的/公共類和成員
 -package                 顯示程式包/受保護的/公共類 和成員 (預設)
 -p  -private             顯示所有類和成員
 -c                       對程式碼進行反彙編
 -s                       輸出內部型別簽名
 -sysinfo                 顯示正在處理的類的系統資訊 (路徑, 大小, 日期, MD5 雜湊)
 -constants               顯示靜態最終常量
 -classpath <path>        指定查詢使用者類檔案的位置
 -bootclasspath <path>    覆蓋引導類檔案的位置

一般常用的是-v -l -c三個選項。
javap -v classxx,不僅會輸出行號、本地變量表資訊、反編譯彙編程式碼,還會輸出當前類用到的常量池等資訊。
javap -l 會輸出行號和本地變量表資訊。
javap -c 會對當前class位元組碼進行反編譯生成彙編程式碼。
檢視彙編程式碼時,需要知道里面的jvm指令,可以參考官方文件:
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
另外通過jclasslib工具也可以看到上面這些資訊,而且是視覺化的,效果更好一些。

二、javap測試及內容詳解

前面已經介紹過javap輸出的內容有哪些,東西比較多,這裡主要介紹其中code區(彙編指令)、區域性變量表和程式碼行偏移對映三個部分。
如果需要分析更多的資訊,可以使用javap -v進行檢視。
另外,為了更方便理解,所有彙編指令不單拎出來講解,而是在反彙編程式碼中以註釋的方式講解(吐槽一下,簡書的markdown貌似不能改字型顏色,這一點很不爽)。

下面寫段程式碼測試一下:
例子1:分析一下下面的程式碼反彙編之後結果:

public class TestDate {
    
    private int count = 0;
    
    public static void main(String[] args) {
        TestDate testDate = new TestDate();
        testDate.test1();
    }
    
    public void test1(){
        Date date = new Date();
        String name1 = "wangerbei";
        test2(date,name1); 
        System.out.println(date+name1);
    }

    public void test2(Date dateP,String name2){
        dateP = null;
        name2 = "zhangsan";
    }

    public void test3(){
        count++;
    }
    
    public void  test4(){
        int a = 0;
        {
            int b = 0;
            b = a+1;
        }
        int c = a+1;
    }
}

上面程式碼通過JAVAC -g 生成class檔案,然後通過javap命令對位元組碼進行反彙編:
$ javap -c -l TestDate
得到下面內容(指令等部分是我參照著官方文件總結的):

Warning: Binary file TestDate contains com.justest.test.TestDate
Compiled from "TestDate.java"
public class com.justest.test.TestDate {
  //預設的構造方法,在構造方法執行時主要完成一些初始化操作,包括一些成員變數的初始化賦值等操作
  public com.justest.test.TestDate();
    Code:
       0: aload_0 //從本地變量表中載入索引為0的變數的值,也即this的引用,壓入棧
       1: invokespecial #10  //出棧,呼叫java/lang/Object."<init>":()V 初始化物件,就是this指定的物件的init()方法完成初始化
       4: aload_0  // 4到6表示,呼叫this.count = 0,也即為count複製為0。這裡this引用入棧
       5: iconst_0 //將常量0,壓入到運算元棧
       6: putfield     //出棧前面壓入的兩個值(this引用,常量值0), 將0取出,並賦值給count
       9: return
//指令與程式碼行數的偏移對應關係,每一行第一個數字對應程式碼行數,第二個數字對應前面code中指令前面的數字
    LineNumberTable:
      line 5: 0
      line 7: 4
      line 5: 9
    //區域性變量表,start+length表示這個變數在位元組碼中的生命週期起始和結束的偏移位置(this生命週期從頭0到結尾10),slot就是這個變數在區域性變量表中的槽位(槽位可複用),name就是變數名稱,Signatur區域性變數型別描述
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
         0      10     0  this   Lcom/justest/test/TestDate;
 
  public static void main(java.lang.String[]);
    Code:
// new指令,建立一個class com/justest/test/TestDate物件,new指令並不能完全建立一個物件,物件只有在初,只有在呼叫初始化方法完成後(也就是呼叫了invokespecial指令之後),物件才建立成功,
       0: new  //建立物件,並將物件引用壓入棧
       3: dup //將運算元棧定的資料複製一份,並壓入棧,此時棧中有兩個引用值
       4: invokespecial #20  //pop出棧引用值,呼叫其建構函式,完成物件的初始化
       7: astore_1 //pop出棧引用值,將其(引用)賦值給區域性變量表中的變數testDate
       8: aload_1  //將testDate的引用值壓入棧,因為testDate.test1();呼叫了testDate,這裡使用aload_1從區域性變量表中獲得對應的變數testDate的值並壓入運算元棧
       9: invokevirtual #21 // Method test1:()V  引用出棧,呼叫testDate的test1()方法
      12: return //整個main方法結束返回
    LineNumberTable:
      line 10: 0
      line 11: 8
      line 12: 12
    //區域性變量表,testDate只有在建立完成並賦值後,才開始宣告週期
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
         0      13     0  args   [Ljava/lang/String;
         8       5     1 testDate   Lcom/justest/test/TestDate;
 
  public void test1();
    Code:
       0: new           #27                 // 0到7建立Date物件,並賦值給date變數
       3: dup
       4: invokespecial #29                 // Method java/util/Date."<init>":()V
       7: astore_1
       8: ldc           #30     // String wangerbei,將常量“wangerbei”壓入棧
      10: astore_2  //將棧中的“wangerbei”pop出,賦值給name1
      11: aload_0 //11到14,對應test2(date,name1);預設前面加this.
      12: aload_1 //從區域性變量表中取出date變數
      13: aload_2 //取出name1變數
      14: invokevirtual #32                 // Method test2: (Ljava/util/Date;Ljava/lang/String;)V  呼叫test2方法
  // 17到38對應System.out.println(date+name1);
      17: getstatic     #36                 // Field java/lang/System.out:Ljava/io/PrintStream;
  //20到35是jvm中的優化手段,多個字串變數相加,不會兩兩建立一個字串物件,而使用StringBuilder來建立一個物件
      20: new           #42                 // class java/lang/StringBuilder
      23: dup
      24: invokespecial #44                 // Method java/lang/StringBuilder."<init>":()V
      27: aload_1
      28: invokevirtual #45                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      31: aload_2
      32: invokevirtual #49                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      35: invokevirtual #52                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      38: invokevirtual #56                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V  invokevirtual指令表示基於類呼叫方法
      41: return
    LineNumberTable:
      line 15: 0
      line 16: 8
      line 17: 11
      line 18: 17
      line 19: 41
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
             0      42     0  this   Lcom/justest/test/TestDate;
             8      34     1  date   Ljava/util/Date;
            11      31     2 name1   Ljava/lang/String;
 
  public void test2(java.util.Date, java.lang.String);
    Code:
       0: aconst_null //將一個null值壓入棧
       1: astore_1 //將null賦值給dateP
       2: ldc           #66       // String zhangsan 從常量池中取出字串“zhangsan”壓入棧中
       4: astore_2 //將字串賦值給name2
       5: return
    LineNumberTable:
      line 22: 0
      line 23: 2
      line 24: 5
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
             0       6     0  this   Lcom/justest/test/TestDate;
             0       6     1 dateP   Ljava/util/Date;
             0       6     2 name2   Ljava/lang/String;
 
  public void test3();
    Code:
       0: aload_0 //取出this,壓入棧
       1: dup   //複製運算元棧棧頂的值,並壓入棧,此時有兩個this物件引用值在運算元組棧
       2: getfield #12// Field count:I this出棧,並獲取其count欄位,然後壓入棧,此時棧中有一個this和一個count的值
       5: iconst_1 //取出一個int常量1,壓入運算元棧
       6: iadd  // 從棧中取出count和1,將count值和1相加,結果入棧
       7: putfield      #12 // Field count:I  一次彈出兩個,第一個彈出的是上一步計算值,第二個彈出的this,將值賦值給this的count欄位
      10: return
    LineNumberTable:
      line 27: 0
      line 28: 10
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
             0      11     0  this   Lcom/justest/test/TestDate;
 public void test4();
    Code:
       0: iconst_0
       1: istore_1
       2: iconst_0
       3: istore_2
       4: iload_1
       5: iconst_1
       6: iadd
       7: istore_2
       8: iload_1
       9: iconst_1
      10: iadd
      11: istore_2
      12: return
    LineNumberTable:
      line 33: 0
      line 35: 2
      line 36: 4
      line 38: 8
      line 39: 12
    //看下面,b和c的槽位slot一樣,這是因為b的作用域就在方法塊中,方法塊結束,區域性變量表中的槽位就被釋放,後面的變數就可以複用這個槽位
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
             0      13     0  this   Lcom/justest/test/TestDate;
             2      11     1     a   I
             4       4     2     b   I
            12       1     2     c   I
}

例子2:下面一個例子
先有一個User類:

public class User {
    private String name;
    private int age;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public int getAge() {
        return age;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
}

然後寫一個操作User物件的測試類:

public class TestUser {
     
    private int count;
     
    public void test(int a){
        count = count + a;
    }
     
    public User initUser(int age,String name){
        User user = new User();
        user.setAge(age);
        user.setName(name);
        return user;
    }
     
    public void changeUser(User user,String newName){
        user.setName(newName);
    }
}

先javac -g 編譯成class檔案。
然後對TestUser類進行反彙編:
$ javap -c -l TestUser
得到反彙編結果如下:

Warning: Binary file TestUser contains com.justest.test.TestUser
Compiled from "TestUser.java"

public class com.justest.test.TestUser {

//預設的建構函式
  public com.justest.test.TestUser();

    Code:
       0: aload_0
       1: invokespecial #10                 // Method java/lang/Object."<init>":()V
       4: return

    LineNumberTable:
      line 3: 0

    LocalVariableTable:
      Start  Length  Slot  Name   Signature
             0       5     0  this   Lcom/justest/test/TestUser;

  public void test(int);

    Code:
       0: aload_0 //取this對應的對應引用值,壓入運算元棧
       1: dup //複製棧頂的資料,壓入棧,此時棧中有兩個值,都是this物件引用
       2: getfield      #18 // 引用出棧,通過引用獲得對應count的值,並壓入棧
       5: iload_1 //從區域性變量表中取得a的值,壓入棧中
       6: iadd //彈出棧中的count值和a的值,進行加操作,並將結果壓入棧
       7: putfield      #18 // 經過上一步操作後,棧中有兩個值,棧頂為上一步操作結果,棧頂下面是this引用,這一步putfield指令,用於將棧頂的值賦值給引用物件的count欄位
      10: return //return void

    LineNumberTable:
      line 8: 0
      line 9: 10

    LocalVariableTable:
      Start  Length  Slot  Name   Signature
             0      11     0  this   Lcom/justest/test/TestUser;
             0      11     1     a   I

  public com.justest.test.User initUser(int, java.lang.String);

    Code:
       0: new           #23   // class com/justest/test/User 建立User物件,並將引用壓入棧
       3: dup //複製棧頂值,再次壓入棧,棧中有兩個User物件的地址引用
       4: invokespecial #25   // Method com/justest/test/User."<init>":()V 呼叫user物件初始化
       7: astore_3 //從棧中pop出User物件的引用值,並賦值給區域性變量表中user變數
       8: aload_3 //從區域性變量表中獲得user的值,也就是User物件的地址引用,壓入棧中
       9: iload_1 //從區域性變量表中獲得a的值,並壓入棧中,注意aload和iload的區別,一個取值是物件引用,一個是取int型別資料
      10: invokevirtual #26  // Method com/justest/test/User.setAge:(I)V 運算元棧pop出兩個值,一個是User物件引用,一個是a的值,呼叫setAge方法,並將a的值傳給這個方法,setAge操作的就是堆中物件的欄位了
      13: aload_3 //同7,壓入棧
      14: aload_2 //從區域性變量表取出name,壓入棧
      15: invokevirtual #29  // MethodUser.setName:(Ljava/lang/String;)V 運算元棧pop出兩個值,一個是User物件引用,一個是name的值,呼叫setName方法,並將a的值傳給這個方法,setName操作的就是堆中物件的欄位了
      18: aload_3 //從區域性變數取出User引用,壓入棧
      19: areturn //areturn指令用於返回一個物件的引用,也就是上一步中User的引用,這個返回值將會被壓入呼叫當前方法的那個方法的棧中objectref is popped from the operand stack of the current frame ([§2.6](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.6)) and pushed onto the operand stack of the frame of the invoker

    LineNumberTable:
      line 12: 0
      line 13: 8
      line 14: 13
      line 15: 18

    LocalVariableTable:
      Start  Length  Slot  Name   Signature
             0      20     0  this   Lcom/justest/test/TestUser;
             0      20     1   age   I
             0      20     2  name   Ljava/lang/String;
             8      12     3  user   Lcom/justest/test/User;

  public void changeUser(com.justest.test.User, java.lang.String);

    Code:
       0: aload_1 //區域性變量表中取出this,也即TestUser物件引用,壓入棧
       1: aload_2 //區域性變量表中取出newName,壓入棧
       2: invokevirtual #29 // Method User.setName:(Ljava/lang/String;)V pop出棧newName值和TestUser引用,呼叫其setName方法,並將newName的值傳給這個方法
       5: return

    LineNumberTable:
      line 19: 0
      line 20: 5

    LocalVariableTable:
      Start  Length  Slot  Name   Signature
             0       6     0  this   Lcom/justest/test/TestUser;
             0       6     1  user   Lcom/justest/test/User;
             0       6     2 newName   Ljava/lang/String;

public static void main(java.lang.String[]);

    Code:
       0: new      #1 // class com/justest/test/TestUser 建立TestUser物件,將引用壓入棧
       3: dup //複製引用,壓入棧
       4: invokespecial #43   // Method "<init>":()V 引用值出棧,呼叫構造方法,物件初始化
       7: astore_1 //引用值出棧,賦值給區域性變量表中變數tu
       8: aload_1 //取出tu值,壓入棧
       9: bipush    10 //將int值10壓入棧
      11: ldc           #44   // String wangerbei 從常量池中取出“wangerbei” 壓入棧
      13: invokevirtual #46    // Method initUser(ILjava/lang/String;)Lcom/justest/test/User; 呼叫tu的initUser方法,並返回User物件 ,出棧三個值:tu引用,10和“wangerbei”,並且initUser方法的返回值,即User的引用,也會被壓入棧中,參考前面initUser中的areturn指令
      16: astore_2 //User引用出棧,賦值給user變數
      17: aload_1 //取出tu值,壓入棧
      18: aload_2 //取出user值,壓入棧
      19: ldc           #48     // String lisi 從常量池中取出“lisi”壓入棧
      21: invokevirtual #50     // Method changeUser:(Lcom/justest/test/User;Ljava/lang/String;)V 呼叫tu的changeUser方法,並將user引用和lisi傳給這個方法
      24: return //return void
   
 LineNumberTable:
      line 23: 0
      line 24: 8
      line 25: 17
      line 26: 24

    LocalVariableTable:
      Start  Length  Slot  Name   Signature
             0      25     0  args   [Ljava/lang/String;
             8      17     1    tu   Lcom/justest/test/TestUser;
            17       8     2  user   Lcom/justest/test/User;

}

三、總結

1、通過javap命令可以檢視一個java類反彙編、常量池、變量表、指令程式碼行號表等等資訊。

2、平常,我們比較關注的是java類中每個方法的反彙編中的指令操作過程,這些指令都是順序執行的,可以參考官方文件檢視每個指令的含義,很簡單:

https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.areturn

3、通過對前面兩個例子程式碼反彙編中各個指令操作的分析,可以發現,一個方法的執行通常會涉及下面幾塊記憶體的操作:

(1)java棧中:區域性變量表、運算元棧。這些操作基本上都值操作。
(2)java堆。通過物件的地址引用去操作。
(3)常量池。
(4)其他如幀資料區、方法區(jdk1.8之前,常量池也在方法區)等部分,測試中沒有顯示出來,這裡說明一下。

在做值相關操作時:
一個指令,可以從區域性變量表、常量池、堆中物件、方法呼叫、系統呼叫中等取得資料,這些資料(可能是指,可能是物件的引用)被壓入運算元棧。
一個指令,也可以從運算元數棧中取出一到多個值(pop多次),完成賦值、加減乘除、方法傳參、系統呼叫等等操作。



作者:王二北
連結:https://www.jianshu.com/p/6a8997560b05
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。

Java位元組碼指令列表

http://www.linmuxi.com/2016/02/26/jvm-class-read-01/

https://www.jianshu.com/p/6a8997560b05

https://blog.csdn.net/w372426096/article/details/81664431   附錄檢視