[原創]ASM動態修改JAVA函數之函數字節碼初探

ASM是很是強大的JAVA字節碼生成和修改工具,具備性能優異、文檔齊全、比較易用等優勢。官方網站:http://asm.ow2.org/html

要想熟練的使用ASM,須要對java字節碼有必定的瞭解,本文重點對java函數的字節碼進行介紹。本文部份內容參考官方文檔:http://download.forge.objectweb.org/asm/asm4-guide.pdfjava

1.JAVA虛擬機執行模型android

在JVM執行模型裏,每一個方法都是在線程中執行,而每一個線程對應本身的棧,每一個棧由幀組成。每一個幀對應一個方法調用,每次調用一個方法,web

會將新幀壓入當前線程的執行棧,當方法返回時(異常退出也是返回),再將這個幀從執行棧彈出。數組

每一個幀主要包括兩部分,一個局部變量表和一個操做數棧,關係以下圖所示:數據結構

這裏注意,局部變量表是根據索引訪問的列表,相似數組;而操做數棧則是「後入先出」的棧,這裏很是重要,由於java函數的字節碼指令基本上都是對這兩個數據結構進行操做。jvm

局部變量表和操做數棧的大小取決於方法代碼,在編譯時計算,並隨字節碼指令一塊兒寫入class文件中,ide

    public int gogo() {
        Log.i("zkw", "hello");
        return 888;
    }

這是一個java方法,編譯成class以後內容以下:函數

  // access flags 0x1
  public gogo()I
    LDC "zkw"
    LDC "hello"
    INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
    POP
    SIPUSH 888
    IRETURN
    MAXSTACK = 2
    MAXLOCALS = 1

最下面兩行的MAXSTACK和MAXLOCALS的值就是操做數棧和局部變量表的大小。工具

局部變量表和操做數棧中的每一個槽(slot)能夠保存除long和double以外的任意java值,而long和double須要兩個槽,好比向局部變量表儲存一個int和一個long,則表中第一個位置是int值,第二和第三個位置存的是long值。

還有一點須要注意,若是是非靜態方法,局部變量表的第0個位置爲"this"。

2.字節代碼指令

 Java類型被編譯成class後,都是用類型描述符表示的,以下圖:

方法也一樣會被編譯成方法描述符,以下:

字節碼指令是由操做碼和參數組成:

  • 操做碼是一個字節代碼名,由助記符號表示,例如操做碼0,對應的是NOP,表示無任何操做的指令;操做碼21,對應ILOAD,表示讀取局部變量表某個位置的int值。
  • 參數是儲存在編譯後代碼中的靜態值。

字節碼指令分爲兩種:

  • 一種是用來在局部變量表和操做數棧之間傳送值的。好比FSTORE i指令從操做數棧彈出一個float值,並存入索引i對應的局部變量表中。而DLOAD j指令則是讀取局部變量表中索引j和j+1對應的double值(思考一下爲何是j和j+1),並將它壓入操做數棧。
  • 另外一部分字節碼指令僅用來處理操做數棧。好比xADD(x對應I、L、F、D)指令從操做數棧彈出兩個數值作加法,而後將結果壓入棧。再好比INVOKESTATIC用於調用靜態方法,該指令會從操做數棧彈出n+1個值(n是靜態方法的n個參數,+1對應目標對象),並壓回方法調用的結果。

仍是用上面的代碼舉例子,咱們直接看字節碼:

  // access flags 0x1
  public gogo()I
    LDC "zkw"
    LDC "hello"
    INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
    POP
    SIPUSH 888
    IRETURN
    MAXSTACK = 2
    MAXLOCALS = 1

LDC是將參數中的值壓入操做數棧,因此前兩行執行完,操做數棧應該長這樣[...,"zkw","hello"],前面...是以前壓入的值,

而後INVOKESTATIC指令彈出以前壓入的參數,而後調用Log.i靜態方法,最後將int結果壓入棧,此時操做數棧應該長這樣[...,int結果]

因爲沒有使用Log.i的返回值,因此直接將返回值從操做數棧POP出去,

接下來SIPUSH將888壓入操做數棧,此時棧長這樣[...,888]

而後IRETURN從操做數棧彈出int值並返回,方法調用結束。

這裏咱們沒有看到對局部變量表的操做,下面稍微修改下gogo方法:

    public int gogo() {
        int a = Log.i("zkw", "hello");
        return a;
    }

爲了看到如何操做局部變量表,咱們獲取Log.i返回的int值,並將其return,編譯以後以下:

  // access flags 0x1
  public gogo()I
    LDC "zkw"
    LDC "hello"
    INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
    ISTORE 1
    ILOAD 1
    IRETURN
    MAXSTACK = 2
    MAXLOCALS = 2

當INVOKESTATIC指令執行以後,操做數棧爲[...,int值],局部變量表爲[this]

看到INVOKESTATIC以後,多了個ISTORE指令,ISTORE 1指令是彈出操做數棧棧頂的值(也就是log.i的返回值),將其存入局部變量表索引爲1的位置(思考一下爲何不是0),當ISTORE執行完,操做數棧爲[...],局部變量表爲[this,int值]。

而後執行ILOAD 1,該指令取出局部變量表1位置的值,並壓入操做數棧,此時操做數棧爲[...int值],局部變量表爲[this]。

而後IRETURN從操做數棧彈出int值,並將其return,執行結束。

3.棧映射幀

java1.6以後還引入了棧映射幀,用於加快虛擬機中類驗證過程的速度。這個映射幀主要記錄每一個指令執行前的局部變量表和操做數棧中包含的類型狀態。這個幀和所謂的棧幀沒有關係,這個映射幀僅僅標示當前局部變量表和操做數棧的狀態。

當jvm進入一個方法時,根據方法描述符就能夠肯定初始幀的狀態,例如方法com.demo.Foo.gogo(int a)的局部變量表的初始狀態爲[com.demo.Foo, I],而操做數棧初始狀態確定是空的。因此這個方法的初始幀爲[com.demo.Foo, I],[]

爲了節省空間,編譯方法時並不會爲每條指令生成一個映射幀,事實上,它僅爲跳轉指令(包括if else,try cache等)生成映射幀。

爲了節省更多空間,對每一個須要生成映射幀的地方作壓縮,僅僅儲存與前一幀的差異,好比與前一幀的狀態同樣時,使用F_SAME助記符,當比前一幀增長了3個之內的局部變量時,使用F_APPEND [],當增長了3個以上的局部變量時,使用F_FULL []。說了這麼多可能有點暈了,看例子吧。

咱們修改上面的例子,增長一些局部變量和條件判斷:

    public int gogo(int c) {
        int a = Log.i("zkw", "hello");
        float f = 0.4f;
        if (a > 0) {
            Log.i("zkw", ">>0");
        } else {
            Log.i("zkw", "<<0");
        }
        return a;
    }

代碼中增長了兩個局部變量a和f,看看編譯後的字節碼:

  // access flags 0x1
  public gogo(I)I
    LDC "zkw"
    LDC "hello"
    INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
    ISTORE 2
    LDC 0.4
    FSTORE 3
    ILOAD 2 IFLE L0
    LDC "zkw"
    LDC ">>0"
    INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
    POP
    GOTO L1
   L0
   FRAME APPEND [I F]
    LDC "zkw"
    LDC "<<0"
    INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
    POP
   L1
   FRAME SAME
    ILOAD 2
    IRETURN
    MAXSTACK = 2
    MAXLOCALS = 4

咱們假定這個方法是com.demo.Foo類的,那麼這個方法的初始幀狀態應該是[com.demo.Foo, I],[],字節碼中不會標示初始幀狀態。

而後代碼繼續往下走,咱們增長了兩個局部變量int a和float f,因此幀狀態出現變化,這個變化會在第一個跳轉目標裏展現出來,請看L0下面的FRAME APPEND [I F],意思是相比於以前的幀狀態增長了兩個局部變量,類型是int和float,此時幀狀態更新成[com.demo.Foo, I, I, F],[]。

以後碰見了下一個跳轉目標L1,這時候的局部變量沒有變化,因此使用FRAME SAME標示。

這些FRAME指令僅僅是標示幀狀態的變化,沒有對局部變量表和操做數棧作任何操做,目的是加快java虛擬機中類驗證過程的速度。

以前說F_APPEND是標示增長3個以內的幀變化,那3個以外呢,咱們繼續修改gogo方法,增長兩個局部變量:

    public int gogo(int c) {
        int a = Log.i("zkw", "hello");
        float f = 0.4f;
        short s = 12;
        long l = 10003983839L;
        if (a > 0) {
            Log.i("zkw", ">>0");
        } else {
            Log.i("zkw", "<<0");
        }
        return a;
    }

看到咱們增長了short s和long l,看看編譯後啥樣:

  // access flags 0x1
  public gogo(I)I
    LDC "zkw"
    LDC "hello"
    INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
    ISTORE 2
    LDC 0.4
    FSTORE 3
    BIPUSH 12
    ISTORE 4
    LDC 10003983839
    LSTORE 5
    ILOAD 2
    IFLE L0
    LDC "zkw"
    LDC ">>0"
    INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
    POP
    GOTO L1
   L0
   FRAME FULL [com/demo/Foo I I F I J] []
    LDC "zkw"
    LDC "<<0"
    INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
    POP
   L1
   FRAME SAME
    ILOAD 2
    IRETURN
    MAXSTACK = 2
    MAXLOCALS = 7

看到標紅的那行,使用了FRAME FULL的指令,後面參數就是徹底的局部變量表狀態。

 

本文爲原創,轉載請註明出處:http://www.cnblogs.com/coding-way/p/6600647.html

相關文章
相關標籤/搜索