java字节码忍者禁术
2019獨角獸企業重金招聘Python工程師標準>>>
Java語言本身是由Java語言規格說明(JLS)所定義的,而Java虛擬機的可執行字節碼則是由一個完全獨立的標準,即Java虛擬機規格說明(通常也被稱為VMSpec)所定義的。
JVM字節碼是通過javac對Java源代碼文件進行編譯后生成的,生成的字節碼與原本的Java語言存在著很大的不同。比方說,在Java語言中為人熟知的一些高級特性,在編譯過程中會被移除,在字節碼中完全不見蹤影。
這方面最明顯的一個例子莫過于Java中的各種循環關鍵字了(for、while等等),這些關鍵字在編譯過程中會被消除,并替換為字節碼中的分支指令。這就意味著在字節碼中,每個方法內部的流程控制只包含if語句與jump指令(用于循環)。
在閱讀本文前,我假設讀者對于字節碼已經有了基本的了解。如果你需要了解一些基本的背景知識,請參考《Java程序員修煉之道》(Well-Grounded Java Developer)一書(作者為Evans與Verburg,由Manning于 2012年出版),或是來自于RebelLabs的這篇報告(下載PDF需要注冊)。
讓我們來看一下這個示例,它對于還不熟悉的JVM字節碼的新手來說很可能會感到困惑。該示例使用了javap工具,它本質上是一個Java字節碼的反匯編工具,在下載的JDK或JRE中可以找到它。在這個示例中,我們將討論一個簡單的類,它實現了Callable接口:
public?class?ExampleCallable?implements?Callable?{public?Double?call()?{return?3.1415;}}我們可以通過對javap工具進行最簡單形式的使用,對這個類進行反匯編后得到以下結果:
$?javap?kathik/java/bytecode_examples/ExampleCallable.classCompiled?from?"ExampleCallable.java"public?class?kathik.java.bytecode_examples.ExampleCallable?implements?java.util.concurrent.Callable?{public?kathik.java.bytecode_examples.ExampleCallable();public?java.lang.Double?call();public?java.lang.Object?call()?throws?java.lang.Exception;}這個反匯編后的結果看上去似乎是錯誤的,畢竟我們只寫一個call方法,而不是兩個。而且即使我們嘗試手工創建這兩個方法,javac也會提示,代碼中有兩個具有相同名稱和參數的方法,它們僅有返回類型的不同,因此這段代碼是無法編譯的。然而,這個類確確實實是由上面那個真實的、有效的Java源文件所生成的。
這個示例能夠清晰地表明在使用Java中廣為人知的一種限制:不可對返回類型進行重載,其實這只是Java語言的一種限制,而不是JVM字符碼本身的強制要求。javac確實會在代碼中插入一些不存在于原始的類文件中的內容,如果你為此感到擔憂,那大可放心,因為這種事每時每刻都在發生!每一位Java程序員最先學到的一個知識點就是:“如果你不提供一個構造函數,那么編譯器會為你自動添加一個簡單的構造函數”。在javap的輸出中,你也能看到其中有一個構造函數存在,而它并不存在于我們的代碼中。
這些額外的方法從某種程度上表明,語言規格說明的需求比VM規格說明中的細節更為嚴格。如果我們能夠直接編寫字節碼,就可以實現許多“不可能”實現的功能,而這種字節碼雖然是合法的,卻沒有任何一個Java編譯器能夠生成它們。
舉例來說,我們可以創建出完全不含構造函數的類。Java語言規格說明中要求每個類至少要包含一個構造函數,而如果我們在代碼中沒有加入構造函數,javac會自動加入一個簡單的void構造函數。但是,如果我們能夠直接編寫字節碼,我們完全可以忽略構造函數。這種類是無法實例化的,即使通過反射也不行。
我們的最后一個例子已經接近成功了,但還是差一口氣。在字節碼中,我們可以編寫一個方法,它將試圖調用一個其它類中定義的私有方法。這段字節碼是有效的,但如果任何程序打算加載它,它將無法正確地進行鏈接。這是因為在類型加載器中(classloader)的校驗器會檢測出這個方法調用的訪問控制限制,并且拒絕這個非法訪問。
介紹ASM
如果我們打算在創建的代碼中實現這些超越Java語言的行為,那就需要完全手動創建這樣的一個類文件。由于這個類文件的格式是兩進制的,因此可以選擇使用某種類庫,它能夠讓我們對某個抽象的數據結構進行操作,隨后將其轉換為字節碼,并通過流方式將其寫入磁盤。
具備這種功能的類庫有多個選擇,但在本文中我們將關注于ASM。這是一個非常常見的類庫,在Java 8分發包中有一個以內部API的形式提供的版本(其內容稍有不同)。對于用戶代碼來說,我們選擇使用通用的開源類庫,而不是JDK中提供的版本,畢竟我們不應當依賴于內部API來實現所需的功能。
ASM的核心功能在于,它提供了一種API,雖然它看上去有些神秘莫測(有時也會顯得有些粗糙),但能夠以一種直接的方式反映出字節碼的數據結構。
我們看到的Java運行時是由多年之前的各種設計決策所產生的結果,而在后續各個版本的類文件格式中,我們能夠清晰地看到各種新增的內容。
ASM致力于盡量使構建的類文件接近于真實形態,因此它的基礎API會分解為一系列相對簡單的方法片段(而這些片段正是用于建模的二進制所關注的)。
如果程序員打算完全手動編寫類文件,就必需理解類文件的整體結構,而這種結構是會隨時改變的。幸運的是,ASM能夠處理多個不同Java版本中的類文件格式之間的細微差別,而Java平臺本身對于可兼容性的高要求也側面幫助了我們。
一個類文件依次包含以下內容:
某個特殊的數字(在傳統的Unix平臺上,Java中的特殊數字是這個歷史悠久的、人見人愛的0xCAFEBABE)
正在使用中的類文件格式版本號
常量
訪問控制標記(例如類的訪問范圍是public、protected還是package等等)
該類的類型名稱
該類的超類
該類所實現的接口
該類擁有的字段(處于超類中的字段上方)
該類擁有的方法(處于超類中的方法上方)
屬性(類級別的注解)
可以用下面這個方法幫助你記憶JVM類文件中的主要部分:
ASM中提供了兩個API,其中最簡單的那個依賴于訪問者模式。在常見的形式中,ASM只包含最簡單的字段以及ClassWrite類(當已經熟悉了ASM的使用和直接操作字節碼的方式之后,許多開發者會發現CheckClassAdapter是一個很實用的起點,作為一個ClassVisitor,它對代碼進行檢查的方式,與Java的類加載子系統中的校驗器的工作方式非常想像。)
可以查看ASM OSChina 網頁
讓我們看幾個簡單的類生成的例子,它們都是按照常規的模式創建的:
啟動一個ClassVisitor(在我們的示例中就是一個ClassWriter)
寫入頭信息
生成必要的方法和構造函數
將ClassVisitor轉換為字節數組,并寫入輸出
示例
public?class?Simple?implements?ClassGenerator?{//?Helpful?constants?private?static?final?String?GEN_CLASS_NAME?=?"GetterSetter";private?static?final?String?GEN_CLASS_STR?=?PKG_STR?+?GEN_CLASS_NAME;@Override?public?byte[]?generateClass()?{ClassWriter?cw?=?new?ClassWriter(0);CheckClassAdapter?cv?=?new?CheckClassAdapter(cw);//?Visit?the?class?header???cv.visit(V1_7,?ACC_PUBLIC,?GEN_CLASS_STR,?null,?J_L_O,?new?String[0]);generateGetterSetter(cv);generateCtor(cv);cv.visitEnd();return?cw.toByteArray();}private?void?generateGetterSetter(ClassVisitor?cv)?{//?Create?the?private?field?myInt?of?type?int.?Effectively:???//?private?int?myInt;???cv.visitField(ACC_PRIVATE,?"myInt",?"I",?null,?1).visitEnd();//?Create?a?public?getter?method???//?public?int?getMyInt();???MethodVisitor?getterVisitor?=?cv.visitMethod(ACC_PUBLIC,?"getMyInt",?"()I",?null,?null);//?Get?ready?to?start?writing?out?the?bytecode?for?the?method???getterVisitor.visitCode();//?Write?ALOAD_0?bytecode?(push?the?this?reference?onto?stack)???getterVisitor.visitVarInsn(ALOAD,?0);//?Write?the?GETFIELD?instruction,?which?uses?the?instance?on???//?the?stack?(&?consumes?it)?and?puts?the?current?value?of?the???//?field?onto?the?top?of?the?stack???getterVisitor.visitFieldInsn(GETFIELD,?GEN_CLASS_STR,?"myInt",?"I");//?Write?IRETURN?instruction?-?this?returns?an?int?to?caller.???//?To?be?valid?bytecode,?stack?must?have?only?one?thing?on?it???//?(which?must?be?an?int)?when?the?method?returns???getterVisitor.visitInsn(IRETURN);//?Indicate?the?maximum?stack?depth?and?local?variables?this???//?method?requires???getterVisitor.visitMaxs(1,?1);//?Mark?that?we've?reached?the?end?of?writing?out?the?method???getterVisitor.visitEnd();//?Create?a?setter???//?public?void?setMyInt(int?i);???MethodVisitor?setterVisitor?=?cv.visitMethod(ACC_PUBLIC,?"setMyInt",?"(I)V",?null,?null);setterVisitor.visitCode();//?Load?this?onto?the?stack???setterVisitor.visitVarInsn(ALOAD,?0);//?Load?the?method?parameter?(which?is?an?int)?onto?the?stack???setterVisitor.visitVarInsn(ILOAD,?1);//?Write?the?PUTFIELD?instruction,?which?takes?the?top?two?//?entries?on?the?execution?stack?(the?object?instance?and???//?the?int?that?was?passed?as?a?parameter)?and?set?the?field?//?myInt?to?be?the?value?of?the?int?on?top?of?the?stack.?//?Consumes?the?top?two?entries?from?the?stack???setterVisitor.visitFieldInsn(PUTFIELD,?GEN_CLASS_STR,?"myInt",?"I");setterVisitor.visitInsn(RETURN);setterVisitor.visitMaxs(2,?2);setterVisitor.visitEnd();}private?void?generateCtor(ClassVisitor?cv)?{//?Constructor?bodies?are?methods?with?special?name?MethodVisitor?mv?=?cv.visitMethod(ACC_PUBLIC,?INST_CTOR,?VOID_SIG,?null,?null);mv.visitCode();mv.visitVarInsn(ALOAD,?0);//?Invoke?the?superclass?constructor?(we?are?basically?//?mimicing?the?behaviour?of?the?default?constructor?//?inserted?by?javac)???//?Invoking?the?superclass?constructor?consumes?the?entry?on?the?top???//?of?the?stack.???mv.visitMethodInsn(INVOKESPECIAL,?J_L_O,?INST_CTOR,?VOID_SIG);//?The?void?return?instruction???mv.visitInsn(RETURN);mv.visitMaxs(2,?2);mv.visitEnd();}@Override?public?String?getGenClassName()?{return?GEN_CLASS_NAME;}}這段代碼使用了一個簡單的接口,用一個單一的方法生成類的字節,一個輔助方法以返回生成的類名,以及一些實用的常量:
interface?ClassGenerator?{public?byte[]?generateClass();public?String?getGenClassName();//?Helpful?constantspublic?static?final?String?PKG_STR?=?"kathik/java/bytecode_examples/";public?static?final?String?INST_CTOR?=?"";public?static?final?String?CL_INST_CTOR?=?"";public?static?final?String?J_L_O?=?"java/lang/Object";public?static?final?String?VOID_SIG?=?"()V";}為了駕馭生成的類,我們需要使用一個harness類,它叫做Main。Main類提供了一個簡單的類加載器,并且提供了一種反射式的方式對生成類中的方法進行回調。為了簡便起見,我們將生成的類定入Maven的目標文件夾的正確位置,讓IDE中的classpath能夠順利地找到它:
public?class?Main?{public?static?void?main(String[]?args)?{Main?m?=?new?Main();ClassGenerator?cg?=?new?Simple();byte[]?b?=?cg.generateClass();try?{Files.write(Paths.get("target/classes/"?+?PKG_STR?+cg.getGenClassName()?+?".class"),?b,?StandardOpenOption.CREATE);}?catch?(IOException?ex)?{Logger.getLogger(Simple.class.getName()).log(Level.SEVERE,?null,?ex);}m.callReflexive(cg.getGenClassName(),?"getMyInt");}下面的類提供了一種方法,能夠對受保護的defineClass()進行訪問,這樣一來我們就能夠將一個字節數組轉換為某個類對象,以便在反射中使用。
private?static?class?SimpleClassLoader?extends?ClassLoader?{public?Class?simpleDefineClass(byte[]?clazzBytes)?{return?defineClass(null,?clazzBytes,?0,?clazzBytes.length);}}private?void?callReflexive(String?typeName,?String?methodName)?{byte[]?buffy?=?null;try?{buffy?=?Files.readAllBytes(Paths.get("target/classes/"?+?PKG_STR?+typeName?+?".class"));if?(buffy?!=?null)?{SimpleClassLoader?myCl?=?new?SimpleClassLoader();Class?newClz?=?myCl.simpleDefineClass(buffy);Object?o?=?newClz.newInstance();Method?m?=?newClz.getMethod(methodName,?new?Class[0]);if?(o?!=?null?&&?m?!=?null)?{Object?res?=?m.invoke(o,?new?Object[0]);System.out.println("Result:?"?+?res);}}}?catch?(IOException?|?InstantiationException?|?IllegalAccessException?|?NoSuchMethodException?|?SecurityException?|?IllegalArgumentException?|?InvocationTargetException?ex)?{Logger.getLogger(Simple.class.getName()).log(Level.SEVERE,?null,?ex);}}有了這個類以后,我們只要通過細微的改動,就可以方便地測試各種不同的類生成器,以此對字節碼生成器的各個方面進行探索。
實現無構造函數的類的方式也很相似。舉例來說,以下這種方式可以在生成的類中僅包含一個靜態字段,以及它的getter和setter(生成器不會調用generateCtor()方法):
private?void?generateStaticGetterSetter(ClassVisitor?cv)?{//?Generate?the?static?field??cv.visitField(ACC_PRIVATE?|?ACC_STATIC,?"myStaticInt",?"I",?null,1).visitEnd();MethodVisitor?getterVisitor?=?cv.visitMethod(ACC_PUBLIC?|?ACC_STATIC,?"getMyInt",?"()I",?null,?null);getterVisitor.visitCode();getterVisitor.visitFieldInsn(GETSTATIC,?GEN_CLASS_STR,?"myStaticInt",?"I");getterVisitor.visitInsn(IRETURN);getterVisitor.visitMaxs(1,?1);getterVisitor.visitEnd();MethodVisitor?setterVisitor?=?cv.visitMethod(ACC_PUBLIC?|?ACC_STATIC,?"setMyInt",?"(I)V",?null,?null);setterVisitor.visitCode();setterVisitor.visitVarInsn(ILOAD,?0);setterVisitor.visitFieldInsn(PUTSTATIC,?GEN_CLASS_STR,?"myStaticInt",?"I");}setterVisitor.visitInsn(RETURN);setterVisitor.visitMaxs(2,2);setterVisitor.visitEnd();請留意一下該方法在生成時使用了ACC_STATIC標記,此外還請注意方法的參數是位于本地變量列表中的最前面的(這里使用的ILOAD 0 模式暗示了這一點 —— 而在生成實例方法時,此處應該改為ILOAD 1,這是因為實例方法中的“this”引用存儲在本地變量表中的偏移量為0)。
通過使用javap,我們就能夠確認在生成的類中確實不包括任何構造函數:
$?javap?-c?kathik/java/bytecode_examples/StaticOnly.class?public?class?kathik.StaticOnly?{public?static?int?getMyInt();?Code:0:?getstatic????#11????????????????//?Field?myStaticInt:I3:?ireturnpublic?static?void?setMyInt(int);?Code:0:?iload_01:?putstatic????#11????????????????//?Field?myStaticInt:I4:?return}使用生成的類
目前為止,我們是使用反射的方式調用我們通過ASM所生成的類的。這有助于保持這個示例的自包含性,但在很多情況下,我們希望能夠將這些代碼生成在常規的Java文件中。要實現這一點非常簡單。以下示例將生成的類保存在Maven的目標目錄下,寫法很簡單:
$?cd?target/classes $?jar?cvf?gen-asm.jar?kathik/java/bytecode_examples/GetterSetter.class?kathik/java/bytecode_examples/StaticOnly.class$?mv?gen-asm.jar?../../lib/gen-asm.jar這樣一來我們就得到了一個JAR文件,可以作為依賴項在其它代碼中使用。比方說,我們可以這樣使用這個GetterSetter類:
import?kathik.java.bytecode_examples.GetterSetter;public?class?UseGenCodeExamples?{public?static?void?main(String[]?args)?{UseGenCodeExamples?ugcx?=?new?UseGenCodeExamples();ugcx.run();}private?void?run()?{GetterSetter?gs?=?new?GetterSetter();gs.setMyInt(42);System.out.println(gs.getMyInt());}}這段代碼在IDE中是無法通過編譯的(因為GetterSetter類沒有配置在classpath中)。但如果我們直接使用命令行,并且在classpath中指向正確的依賴,就可以正確地運行了:
$?cd?../../src/main/java/$?javac?-cp?../../../lib/gen-asm.jar?kathik/java/bytecode_examples/withgen/UseGenCodeExamples.java $?java?-cp?.:../../../lib/gen-asm.jar?kathik.java.bytecode_examples.withgen.UseGenCodeExamples42結論
在本文中,我們通過使用ASM類庫中所提供的簡單API,學習了完全手動生成類文件的基礎知識。我們也為讀者展示了Java語言和字節碼有哪些不同的要求,并且了解到Java中的某些規則其實只是語言本身的規范,而不是運行時所強制的要求。我們還看到,一個正確編寫的手工類文件可以直接在語言中使用,與通過javac生成的文件沒有區別。這一點也是Java與其它非Java語言,例如Groovy或Scala進行互操作的基礎。
這方面的應用還有許多高級技巧,通過本文的學習,讀者應該已經掌握了基本的知識,并且能夠進一步深入研究JVM的運行時,以及如何對它進行各種操作的技術。
轉載于:https://my.oschina.net/u/2308739/blog/517806
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的java字节码忍者禁术的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MapReduce原理及其主要实现平台分
- 下一篇: 正则表达式元字符整理