Java位元組碼忍者禁術
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.class\Compiled 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的類載入子系統中的校驗器的工作方式非常想像。)\
讓我們看幾個簡單的類生成的例子,它們都是按照常規的模式建立的:\
- 啟動一個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\
相關文章
- Java位元組碼增強技術Java
- Java 位元組碼Java
- Java位元組碼指令Java
- JAVA動態位元組碼Java
- 【Java】JVM位元組碼分析JavaJVM
- 【轉】動態位元組碼技術跟蹤Java程式Java
- Java位元組碼指令表Java
- 輕鬆看懂Java位元組碼Java
- Java的魔力:位元組碼(轉)Java
- 例項分析理解Java位元組碼Java
- cmd下檢視java位元組碼Java
- 如何閱讀JAVA 位元組碼(一)Java
- 淺談位元組碼增強技術系列1-位元組碼增強概覽
- Java 動態性(4) – 位元組碼操作Java
- 從 Java 位元組碼到 ASM 實踐JavaASM
- 學習 Java 之 位元組碼驗證Java
- 0停機遷移Nacos?Java位元組碼技術來幫忙Java
- 位元組碼指令
- JVM(三):深入分析Java位元組碼-上JVMJava
- JVM(四):深入分析Java位元組碼-下JVMJava
- JWebAssembly:Java 位元組碼到 WebAssembly 編譯器WebJava編譯
- Dalvik 和 Java 位元組碼的比較Java
- Java程式碼如何檢視位元組碼及彙編碼Java
- 位元組快取技術快取
- ASM位元組碼操作類庫(開啟java語言世界通往位元組碼世界的大門)ASMJava
- ASM位元組碼操作類庫:開啟java語言世界通往位元組碼世界的大門ASMJava
- 開啟java語言世界通往位元組碼世界的大門——ASM位元組碼操作類庫JavaASM
- 使用java動態位元組碼技術簡單實現arthas的trace功能。Java
- 位元組碼基礎
- 位元組碼詳解
- java 位元組陣列取反Java陣列
- 從1+1=2來理解Java位元組碼Java
- 使用javap分析Java位元組碼的一個例子Java
- 從位元組碼視角看java字串的拼接Java字串
- Java Class 位元組碼檔案結構詳解Java
- 什麼是位元組碼?python位元組碼詳細介紹!Python
- 位元組編碼轉換
- 理解 Python 位元組碼Python