這裡要說的ASM,並不是指組合語言,而是一個操作Java bytecode的架構。對於Java平台而言,bytecode便是它的“組合語言”,所以,ASM這個名字倒也算是實至名歸。ASM本身很強大,有不少軟體和架構選擇它作為底層的實現,比如cglib。在這篇blog中,主要來關注一下它在代碼產生方面的威力。
在起步階段,Hello World總是一個很好的選擇,也就是說,我們產生的目標代碼是這樣的:
public class AsmExample {
public static void main(String[] args) {
System.out.println("Hello, World");
}
}
在Java中,代碼是以類的形式進行組織的,.class檔案便是bytecode的載體。對照上面這段代碼,首先,我們需要一個類。
public class AsmMain {
public static void main(String[] args) {
ClassWriter cw = new ClassWriter(true);
cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "AsmExample", null, "java/lang/Object", null);
...
cw.visitEnd();
}
}
在上面這段代碼中,ClassWriter就是ASM中用來產生bytecode形式的類。在這裡,我們要為我們產生的類設定一些屬性,比如類名、存取層級和超類,以及在bytecode層次上需要的版本號碼等等。至此,對應的Java代碼如下:
public class AsmExample {
}
有了類,接下來就是對應的方法了,先來看看基本的結構:
Method m = Method.getMethod("void main (String[])");
GeneratorAdapter mg = new GeneratorAdapter(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, m, null, null, cw);
...
mg.returnValue();
mg.endMethod();
首先我們設定了一個方法的簽名,包括方法名,參數和傳回型別。我們要產生這個方法,還需要設定一些方法的屬性,比如存取層級等等,通過cw這個參數,方法同類關聯在了一起。到這裡,對應的Java代碼是這樣的:
public class AsmExample {
public static void main(String args[]) {
}
}
前面所做的都是搭建靜態結構的工作,接下來,我們要進入的才是讓程式動起來的部分。
mg.getStatic(Type.getType(System.class), "out", Type.getType(PrintStream.class));
mg.push("Hello world!");
mg.invokeVirtual(Type.getType(PrintStream.class), Method.getMethod("void println (String)"));
在這裡,我們面對的實際上是JVM的指令,所以,如果面對彙編一樣,所有的一切都一步一步說清楚。
首先是獲得System.out。我們通過getStatic這個方法實現,它表示從哪個類中取出哪個static欄位,其類型是什麼。而且實際上,這條指令執行的結果是將這個取出的欄位推到了堆棧上。隨後,我們在把“Hello world!”也推入堆棧之中,很顯然,這一切都是在為調用方法做準備。
對於參數(這裡的“Hello world!”)入棧,我們很容易接受,但為什麼要把System.out也送入堆棧呢?再次提醒一下,這裡我們是在JVM一級進行思考,在這裡,方法調用被打回了最原始的形態,在Java程式中被隱藏的this這時也要作為參數顯式傳遞,也就是說,方法調用變成了這樣:
println(System.out, "Hello world!");
萬事俱備,調用方法。在Java中,方法調用需要區分類方法和執行個體方法,它們在虛擬機器中有著不同的指令,這裡我們要調用的是執行個體的方法,所以,這裡用的是invokeVirtual,指定了類型,指定了方法,方法就可以調用了。如果要調用類的方法,也就是static方法,那就需要讓invokeStatic上陣了。
對比一下invokeVirtual和invokeStatic的API定義,我們不難發現,它們之間實際上沒有什麼區別,之所以要弄出兩個來,與Java的設計不無關係,它把屬於類的東西看作了一種特殊的東西,沒有統一到對象體系之中。如果為Ruby設計虛擬機器,可以消除這樣的問題,因為在Ruby中,類的方法就是類對象的執行個體方法,這樣將類的東西統一到對象體系之中,不必額外區分。
到這裡,我們的目標便已完全實現:
public class AsmExample {
public static void main(String args[]) {
System.out.println("Hello world!");
}
}
之後,我們可以把定義的類轉為位元組,至於是載入到虛擬機器中運行,還是儲存到檔案中,那就由自己的喜好了。
byte[] code = cw.toByteArray();
和ASM打交道,需要我們放低自己姿態,站在指令一級進行思考。比如,在這個層次上,實現判斷語句,就需要設定label,然後進行相應的跳轉;這裡沒有迴圈語句,需要自己用判斷加跳轉打造迴圈結構。不過,總的來說,很容易同Java程式對應上,就像我們上面所做的那樣。《深入Java虛擬機器》可以讓我們更好的瞭解JVM,也可以讓協助我們更好的理解ASM的程式。
有幾個幫手可以讓我們更好進行bytecode產生這個遊戲。javap,JDK帶的一個工具,可以用來反組譯碼Java bytecode。在接觸ASM的最初,我們對指令不是很熟悉的時候,可以考慮先把自己的目標寫成Java程式,編譯之後用“javap -c”來查看,所有的指令便一覽無餘,我們就可以照方抓藥了。jad,它為我們提供了一個將Java class檔案反編譯為Java檔案,通過它,我們就可以知道產生的bytecode究竟是不是自己想要的,我所展示與產生過程對應的Java代碼便是藉助於jad的力量完成的。
ASM很強大,這裡只介紹了ASM中的代碼產生,實際上,就連代碼產生這一項工作介紹的都不那麼完整,ASM還提供了另外一種產生方式,不過,用起來不如這裡的GeneratorAdapter,需要更多的JVM指令的智慧,優勢在於速度稍快一些。
讀後感:這玩意沒怎麼把玩過,但我知道為了逃避開原始碼使用問題,為了要改開原始碼,使用ASM動態修改class檔案。自己寫的代碼根本就根本使用ASM那麼麻煩了,ASM畢竟犧牲效率為代價。