字節碼加強:原理與實戰

本文由一個攔截器邏輯的使用場景及演變歷程,引入字節碼加強技術。介紹字節碼的本質,字節碼加強的原理及JVM 啓動過程當中的 Agent 加載、生效流程,並對常見字節碼操做工具進行了簡單應用。java

注:本文僅討論 javaagent 「啓動時加載」。linux

1、技術爲業務需求服務

技術是工具,是解決問題的途徑。針對不一樣的業務需求場景,可使用不一樣的技術實現。c++

經過一部攔截器的流浪史來引入主題:segmentfault

一個簡單的demo

一、基礎版:新建一個Dog對象,而後調用成員方法輸出到控制檯

被調用方windows

調用方api

二、增強版:須要統計方法執行的時間

常規開發:數組

被調用方架構

調用方框架

三、從被調用方剝離非業務邏輯

面向對象設計原則,對象應該儘量專一本身職責範圍內的事情,狗只負責叫,不負責統計本身叫了多長時間,所以統計代碼應該移出Dog類。jvm

3.1 方法提取

3.2 類提取–(參考SpringMVC-Interceptor)

3.3 類解耦合(使用動態代理方式-CGLib/JDK Proxy,這裏Dog類沒有實現接口,使用CGLib)

至此,非業務邏輯由從被調用方剝離出來了,同時咱們也發現調用方代碼卻遭到改變,Main class裏面須要添加動態代理類的處理邏輯。假如不容許改變調用方代碼,進一步處理。

四、調用方代碼剝離(切面–AspectJ)

切面

被調方

調用方

注意:此時直接運行Main class切面不會生效,運行前先進行編譯期織入 java -jar $ASPECTJ\_TOOLS -cp $ASPECTJ_RT  -sourceroots src/main/java/ -d target/classes ...

至此,調用方不用顯式地調用動態代理邏輯,編譯期織入到class中去了(這裏已經聞到了代碼加強的氣味了)。

切面邏輯雖然與具體的業務邏輯解耦合了,獨立出切面類。可是是否生效仍然由業務代碼(切面類)去控制。不管如何,都須要業務方改造,添加切面邏輯代碼。

能不能更進一步,連切面都不寫,也讓切面邏輯生效呢?

五、javaagent 版本–隱式地,無侵入地添加切面邏輯
  • 新建獨立的agent工程
  • 添加MANIFEST.MF文件以及Premain-Class,premain屬性
  • 編譯包含目標邏輯的源文件生成class文件
  • 註冊ClassFileTransfer,在transform方法中替換byte[]
  • MANIFEST.MF指定premain函數和打開類加強開關
  • 編譯輸出jar包

MANIFEST.MF文件。

待替換的新class文件(忽略中文亂碼)。

class轉換器,將新的Dog.class替換舊的Dog.class。

maven打包輸出Agent.jar

上面的javaagent實現細節能夠先存疑,後面會深刻描述,只須要知道按照這樣的步驟能夠實現咱們的需求。

對於業務方而言:代碼徹底沒有變化:

被調用方

調用方

想要使切面邏輯生效,只須要在啓動命令參數中加入-javaagent 選項,指向 Agent 的 jar 包。

這樣,攔截器邏輯以一種插件的形式抽取出來了,使用的時候加載插件就能夠了。

小結一下

  1. 不一樣需求場景下,能夠不一樣的方式實現切面攔截邏輯;
  2. AspectJ或者SpringAop只是一種對開發者友好的快捷方式,本質上仍是修改的業務代碼,只不過隱藏了調用邏輯,並不能真正「無侵入「;
  3. javaagent能夠無侵入的修改一個已發佈的java組件的運行邏輯。

2、什麼是字節碼?

byte[]

一、迴歸原始:JDK 裏面提供了不少有用的工具

在咱們剛開始學習 Java 語言時候的 demo 運行:

編寫原始 Java 文件:

使用 Javac 編譯字節碼文件:

Javac生產的 class 文件有什麼做用呢?

Java 語言一次編譯,處處運行的核心基礎-JVM。

二、class文件究竟是個什麼東西?

先用文本編輯器暴力打開看看:

看不懂?換個方式:

想看個明白?繼續整:使用010editor打開。

各個數據項按順序緊密的從前向後排列, 相鄰的項之間沒有間隙, 這樣可使得class文件很是緊湊, 體積輕巧, 能夠被JVM快速的加載至內存, 而且佔據較少的內存空間。

主要包含的信息:

(1)魔數

(2)版本號(參考文末例子:JRE版本錯誤)

(3)常量池容量

(4)常量池:

  • 文字字符串, 常量值
  • 當前類的類名, 字段名, 方法名, 各個字段和方法的描述符
  • 對當前類的字段和方法的引用信息, 當前類中對其餘類的引用信息等等

(5)其餘屬性

常量池如何索引:

相互索引:

例如方法索引,獲取classIndex和nameAndTypeIndex,經過數組下標,能夠找到該方法所屬的class和方法名稱。

MethodRef

|-----|classIndex
|-----|-----|nameIndex     --→ classNmae
|-----|nameAndTypeIndex
|-----|-----|nameIndex     --→ methodName

常量池索引和字節碼指令的執行。

使用jre自帶工具javap反編譯class文件以下:

Main.class字節碼:

Dog.class字節碼:

能夠看到字節碼具有必定的可讀性,對照着源碼,能夠按照執行邏輯走一遍字節碼執行流程,相關指令的含義很容易從網上查詢到。

至此,咱們經過一個簡單的demo執行流程,大體瞭解了常量的引用以及一個簡單java方法對應的字節碼指令執行過程。

注:

**stack:**最大操做數棧,JVM運行時會根據這個值來分配棧幀(Frame)中的操做棧深度。

**locals:**局部變量所需的存儲空間,單位爲Slot。

args_size: 方法參數的個數。

壓棧:字節碼指令執行過程當中涉及到了不少壓棧操做:JVM是一個基於棧的架構。方法執行的時候(包括main方法),在棧上會分配一個新的幀,這個棧幀包含一組局部變量。

這組局部變量包含了方法運行過程當中用到的全部變量,包括this引用,全部的方法參數,以及其它局部定義的變量。

小結一下

  1. class文件即字節碼是全部屬性,方法邏輯的合集。
  2. 經過字節碼二進制文件將開發者與虛擬機進行了「解耦」。
  3. 推理:修改某些字節或者替換整個二進制流能夠修改運行時邏輯 。

3、如何加強字節碼?

byte[] → byte[]

思路:

  1. 如前述方式直接替換爲目標邏輯編譯後的字節碼。
  2. 手術刀式精準操做,修改/添加某些位置的byte。
  3. 高級API。

工具集:/ASM/javaassist/ByteBuddy 等等。

示例:

ASM

指令級別的字節碼操做(性能強悍)。

指令→ASM api 對應關係(這裏將原始類作了簡化,將字符串拼接邏輯去掉,僅僅輸出時間。由於一個簡單的字符串拼接過程,轉換成字節碼指令可能須要不少行)。

先看看目標源碼與字節碼指令的一一映射關係。

再看看加強字節碼邏輯與目標源碼的字節碼的一一映射關係。

經過對比咱們能夠發現,ASM的API精確到字節碼指令級別,全部的臨時變量存儲,壓棧操做,靜態/實例方法的調用都有對應的API操做。

javassist:(dubbo)

提供字節碼級別的API,相似ASM,再也不贅述。

提供源碼級別的API,針對本文的案例,實現以下:

ByteBuddy

基於ASM的高級API,使咱們對字節碼的操做提高到更抽象層次。開發者只須要知道要實現什麼目標,如何使用對應的API,不用關心底層的字節碼指令排列,甚至能夠不用瞭解字節碼指令。

關於相關框架的API不詳細說,有興趣的同窗能夠自行查詢相關資料。

小結一下

  1. 各類級別的API能夠幫助開發者輕鬆實現字節碼加強,實現特定邏輯。
  2. 不論什麼奇技淫巧,都離不開Instrumentation機制。

4、加強的 byte[] 是如何影響 JVM 的?

Event --> CallBack

由前文總結,引入Instrumentation機制。

一、鋪墊知識點:

(1)JVMTI

JVMTI 是基於事件驅動的,JVM 每執行到必定的邏輯就會調用一些事件的回調接口(若是有的話),這些接口能夠供開發者擴展本身的邏輯。

JVMTIAgent 使用JVMTI來查詢或控制JVM,JVMTIAgent與目標JVM運行在同一個進程中,經過JVMTI進行通訊,最大化控制能力,最小化通訊成本。

典型場景下,JVMTI代理會被實現的很是緊湊,其餘的進程會與JVMTI代理進行通訊。好比jdwp(IDEA遠程調試)。

(2)JVMTIAgent

表現形式:

(1)linux:        .so文件

  • windows:  .dll文件
  • c/c++    動態連接庫

(2)JPLISAgent:      .jar文件

命令行參數

(1)-agentlib:agent-lib-name=options

(2)-agentpath:path-to-agent=options

(3)-javaagent:/data/../../Agent.jar

  • 可加載多個,經過options區分
實現接口

(1)JNIEXPORT jint JNICALL

  • Agent_OnLoad(JavaVM *vm, char *options, void *reserved);

(2)JNIEXPORT jint JNICALL

  • Agent_OnAttach(JavaVM vm, char options, void* reserved);

(3)JNIEXPORT void JNICALL

  • Agent_OnUnload(JavaVM *vm);
JPLISAgent(Java Programming Language Instrumentation Services Agent)--   Instrumentation機制

(1)JavaSE1.5 啓動時加載(本文重點)。

(2)JavaSE1.6 運行時加載。

二、簡化了的核心流程邏輯

命令參數:-javaagent:/data/../../Agent.jar=optoions。

虛擬機建立-構建並初始化Agent-註冊VMInit事件。

虛擬機初始化-觸發VMInit事件-Agent start方法-註冊回調函數並監聽ClassFileLoadHook。

類加載-觸發jvmtiEventClassFileLoadHook事件-替換byt[]-ClassLoader解析。

三、Java 虛擬機啓動過程當中 Agent 相關的流程:

(1)建立JVM的時候初始化agent

  1. 啓動時讀取jvm命令,-agentlib -agentpath -javaagent,並構建了Agent Library鏈表構建了Agent Library鏈表。
  2. 對agent鏈表中的每一個agent,加載所指定的動態庫(如instrument.so), 並調用裏面的Agent_OnLoad方法。
  3. 建立並初始化 JPLISAgent,初始化了Premain class和包裏的配置文件。
  4. 註冊VMInit事件。
Agent_onLoad
|-----|createNewJPLISAgent
|-----|-----|initializeJPLISAgent
|-----|-----|-----|eventHandlerVMInit  ---- >   VMInit

(2)虛擬機初始化

其實是調用 java 類 sun.instrument.InstrumentationImpl 類裏的方法loadClassAndCallPremain。

(3)觸發ClassFileLoadHook事件

|parseClassFile
|-----|post_class_file_load_hook
|-----|-----|post_to_env
|-----|-----|-----|eventHandlerClassFileLoadHook(jvmtiEventClassFileLoadHook回調函數)
|-----|-----|-----|-----|transformClassFile
|-----|-----|-----|-----|-----|CallObjectMethod
|-----|-----|-----|-----|-----|-----|sun.instrument.InstrumentationImpl.transform()

實際調用的java方法 Instrumentationimpl.transform。

debug過程當中經過ClassFileTransformer的transform函數的執行堆棧印證。

到這裏,加強的byte[]如何生效並影響運行時class的過程基本能夠串起來。

小結一下

  1. 虛擬機建立階段,初始化agent,解析,加載javaagent jar,註冊回調函數監聽VMInt事件。
  2. 虛擬機初始化階段,觸發VMInt回調函數,註冊回調函數監聽ClassFileHook事件,同時執行loadClassAndCallPremain函數,註冊transformer。
  3. ClassLoader加載類的時候觸發tranform回調,判斷是否目標類,進行對應字節碼替換。

5、應用

  • 監控
  • 調試
  • 混淆
  • AOP加強
  • 日誌記錄

很是規應用:IDEA破解。

部分破解教程裏面下載插件jar後,會要求你在IDEA的啓動參數文件idea.vmoptions中添加一行,就是javaagent參數。

咱們能夠反編譯這個插件jar包看看,發現不少class由於加了混淆,反編譯後沒法正常識別,可是核心入口Agent.class的主要工做就是註冊Transformer,能夠推測這些Transformer的功能就是在IDEA啓動時以前修改某些鑑定Lisence的邏輯。

6、總結回顧

經過介紹字節碼,字節碼操做工具以及openJDK關於Instrumention機制的部分源碼,探索了字節碼加強的實現原理。

簡單介紹了相關技術的應用場景。

7、附錄

  • SpringMVC-Interceptor

  • IDEA 遠程調試

  • JRE版本錯誤


做者: Neo
相關文章
相關標籤/搜索