掌握Java字节码
嘿! Happy Advent:D我是ZeroTurnaround的技術布道者Simon Maple( @sjmaple) 。 您知道, JRebel伙計們! 由于編寫了類似于JRebel的產品,該產品與字節碼進行交互的結果比您想像的更多,因此,我們希望與他人分享很多有關它的知識。
讓我們從頭開始……Java是一種旨在在虛擬機上運行的語言,因此只需要編譯一次就可以在任何地方運行(是的,是的,一次編寫,可以在任何地方進行測試)。 結果,您安裝到系統上的JVM將是本機的,從而允許在其上運行的代碼與平臺無關。 Java字節碼是您作為源編寫的Java代碼的中間表示,并且是編譯代碼的結果。 因此,您的類文件是字節碼。
更簡潔地說,Java字節碼是Java虛擬機使用的代碼集,該代碼集在運行時被JIT編譯為本機代碼。
您曾經玩過匯編程序或機器代碼嗎? 從某種意義上說,字節碼有點類似,但是行業內很多人并沒有那么多地使用它,更多是出于缺乏必要性。 但是,了解正在發生的事情非常重要,如果您想讓酒吧里的某個人望而卻步,這很有用。
首先,讓我們看一些字節碼基礎知識。 我們將首先使用表達式“ 1 + 2”,并查看如何將其作為Java字節碼執行。 1 + 2可以用反向波蘭語表示為1 2 +。 為什么? 好吧,當我們將其放在堆棧上時,一切都變得清晰了……
好的,在字節碼中,我們實際上會看到操作碼(iconst_1和iconst_2)和一條指令(iadd),而不是推和加,但流程是相同的。 實際指令的長度為一個字節,因此為字節碼。 結果有256種可能的操作碼,但僅使用了200種左右。 操作碼的前綴是類型,后跟操作名稱。 因此,我們之前在iconst和iadd上看到的是整數類型的常量和整數類型的加法指令。
這一切都很好,但是如何讀取類文件。 通常,在打開的類文件中,通常在選擇的編輯器中看到的只是一堆笑臉以及一些正方形,圓點和其他奇怪的字符,對嗎? 答案是在Javap中,這是您隨JDK實際獲得的代碼實用程序。 讓我們看一個代碼示例,看看運行中的javap。
public class Main {public static void main(String[] args){MovingAverage app = new MovingAverage();}}將此類編譯為Main.class文件后,我們可以使用以下命令提取字節碼:javap -c Main
Compiled from "Main.java"public class algo.Main {public algo.Main();Code:0: aload_01: invokespecial #14: return // Method java/lang/Object."<init>":()V public static void main(java.lang.String[]);Code:0: new?????????? #23: dup4: invokespecial #37: astore_18: return }我們可以立即在字節碼中看到我們的默認構造函數和main方法。 順便說一下,這就是Java為無構造函數的類提供默認構造函數的方式! 構造函數中的字節碼只是對super()的調用,而我們的main方法創建了MovingAverage的新實例并返回。 #n字符實際上是指可以使用-verbose參數查看的常量,如下所示:javap -c -verbose Main。 返回內容的有趣部分如下所示:
public class algo.MainSourceFile: "Main.java"minor version: 0major version: 51flags: ACC_PUBLIC, ACC_SUPERConstant pool:#1 = Methodref??? #5.#21???????? //? java/lang/Object."<init>":()V#2 = Class??? ??? #22??????????? //? algo/MovingAverage#3 = Methodref??? #2.#21???????? //? algo/MovingAverage."<init>":()V#4 = Class??? ??? #23 ?????????? //? algo/Main#5 = Class??? ??? #24 ?????????? //? java/lang/Object現在,我們可以將指令與常量進行匹配,并且可以將實際發生的事情拼湊起來要容易得多。 關于上面的示例,您還有什么困擾嗎? 沒有? 那每條指令前面的數字呢?
0: new?????????? #23: dup4: invokespecial #37: astore_18: return現在真的很煩你,對吧? :)如果將這個方法體可視化為數組,這就是我們得到的:
請注意,每條指令都有一個十六進制表示,因此使用它我們實際上會看到以下內容:
如果在十六進制編輯器中打開它,我們實際上可以在類文件中看到它:
實際上,我們可以在HEX編輯器中更改字節碼,但是老實說,這不是您真正想做的事情,尤其是在強制性酒吧旅行之后的星期五下午。 更好的方法是使用ASM或javassist。
讓我們從基本示例繼續,添加一些存儲狀態并直接與堆棧交互的局部變量。 查看以下代碼:
public static void main(String[] args) {MovingAverage ma = new MovingAverage();int num1 = 1;int num2 = 2;ma.submit(num1);ma.submit(num2);double avg = ma.getAvg();}讓我們看看這次在字節碼中得到了什么:
Code:?0: new? #2??? // class algo/MovingAverage3: dup4: invokespecial #3? // Method algo/MovingAverage."<init>":()V7: astore_18: iconst_19: istore_210: iconst_211: istore_312: aload_113: iload_214: i2d15: invokevirtual #4??? ??? // Method algo/MovingAverage.submit:(D)V18: aload_119: iload_320: i2d21: invokevirtual #4??? ??? // Method algo/MovingAverage.submit:(D)V24: aload_125: invokevirtual #5??? ??? // Method algo/MovingAverage.getAvg:()D28: dstore???? 4LocalVariableTable:Start? Length? Slot? Name?? Signature0 ??? ? 31 ??? ??? 0??? args?? [Ljava/lang/String;8 ??? ? 23??? ??? 1? ??? ma???? Lalgo/MovingAverage;10 ???? 21 ??? ??? 2 ??? num1?? I12 ??? ? 19 ??? ??? 3? ??? num2?? I30 ??? ? 1??? ??? 4??? avg??? ?D這看起來更加有趣……我們可以看到,我們創建了一個類型為MovingAverage的對象,該對象通過astore_1指令(1是LocalVariableTable中的插槽號)存儲在本地變量ma中。 指令iconst_1和iconst_2在那里將常數1和2加載到堆棧中,并分別通過指令istore_2和istore_3將它們存儲在LocalVariableTable插槽2和3中。 一條加載指令將一個局部變量壓入堆棧,一條存儲指令從堆棧中彈出下一項并將其存儲在LocalVariableTable中。 重要的是要認識到,當使用存儲指令時,該項目將從堆棧中取出,如果要再次使用它,則需要加載它。
執行流程如何? 我們所看到的只是從一行到下一行的簡單進展。 我希望看到一些BASIC風格的GOTO 10混在一起! 讓我們再舉一個例子:
MovingAverage ma = new MovingAverage();for (int number : numbers) {ma.submit(number);}在這種情況下,當我們遍歷for循環時,執行流程將跳很多次。 假定數字變量是同一類中的靜態字段,則此字節碼如下所示:
0: new #2 // class algo/MovingAverage3: dup4: invokespecial #3 // Method algo/MovingAverage."<init>":()V7: astore_18: getstatic #4 // Field numbers:[I11: astore_212: aload_213: arraylength14: istore_315: iconst_016: istore 418: iload 420: iload_321: if_icmpge 4324: aload_225: iload 427: iaload28: istore 530: aload_131: iload 533: i2d34: invokevirtual #5 // Method algo/MovingAverage.submit:(D)V37: iinc 4, 140: goto 1843: returnLocalVariableTable:Start? Length? Slot? Name?? Signature30 ??? ? 7 ??? ??? 5??? number I 12 ??? ? 31??? ??? 2??? arr$??? ?[I15 ??? ? 28??? ??? 3??? len??? ?$I 18 ??? ? 25 ??? ??? 4 ??? i$ ??? ?I0 ??? ? 49 ??? ??? 0 ??? args? [Ljava/lang/String;8 ??? ? 41 ??? ??? 1??? ma ??? Lalgo/MovingAverage; 48??? ? 1 ??? ??? 2??? avg??? D從位置8到17的指令用于設置循環。 SourceVariable表中有三個在源代碼中并未真正提及的變量arr $,len $和i $。 這些是循環變量。 arr $存儲number字段的參考值,從中得出循環長度len $。 i $是循環計數器,由iinc指令遞增。
首先,我們需要測試我們的循環表達式,該表達式由比較指令執行:
18: iload 420: iload_321: if_icmpge 43我們將4和4加載到堆棧上,分別是循環計數器和循環長度。 我們正在檢查ID i $大于或等于len $。 如果是,則跳至語句43,否則繼續進行。 然后,我們可以在循環中執行邏輯,最后,我們增加計數器并跳回到檢查語句18的循環條件的代碼。
37: iinc?????? 4, 1?????? // increment i$40: goto?????? 18???????? // jump back to the beginning of the loop可以在字節碼中使用一堆算術操作碼和類型命令組合,包括以下內容:
以及許多類型轉換操作碼,這些類型轉換操作碼在為long類型的變量分配一個整數時很重要。
在我們的珍貴示例中,我們將一個整數傳遞給一個采用雙精度值的submit方法。 Java語法為我們完成了此操作,但是在字節碼中,您將看到使用了i2d操作碼:
31: iload 533: i2d?34: invokevirtual #5 // Method algo/MovingAverage.submit:(D)V因此,您已經做到了這一點。 做得好,您已經喝咖啡了! 了解這些內容是否真的有用還是僅僅是怪胎? 好吧,兩者都有! 首先,您現在可以告訴您的朋友,您是可以處理字節碼的JVM,其次,您可以更好地了解編寫字節碼時的操作。 例如,使用ObjectWeb ASM(這是使用最廣泛的字節碼操作工具之一)時,您會發現自己正在構造指令,并且這些知識將證明是無價的!
如果您發現這有趣并且想了解更多,請查看ZeroTurnaround的JRebel產品負責人Anton Arhipov的免費Mastering Java Bytecode報告。 (JRebel使用javassist,我們學習了很多有趣的東西,并且可以與Java字節碼進行交互!)該報告更深入地介紹了如何使用ASM。
謝謝閱讀! 讓我知道你的想法! ( @sjmaple )
翻譯自: https://www.javacodegeeks.com/2013/12/mastering-java-bytecode.html
總結
- 上一篇: 苹果怎么设置家人共享(苹果怎么设置家人共
- 下一篇: 甲骨文发布Java 8