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


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

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

1.JAVA虛擬機執行模型

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

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

每個幀主要包括兩部分,一個局部變量表和一個操作數棧,關系如下圖所示:

這里注意,局部變量表是根據索引訪問的列表,類似數組;而操作數棧則是“后入先出”的棧,這里非常重要,因為java函數的字節碼指令基本上都是對這兩個數據結構進行操作。

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

    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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM