字節(jié)碼增強(qiáng):原理與實(shí)戰(zhàn)

本文由一個(gè)攔截器邏輯的使用場(chǎng)景及演變歷程,引入字節(jié)碼增強(qiáng)技術(shù)。介紹字節(jié)碼的本質(zhì),字節(jié)碼增強(qiáng)的原理及JVM 啟動(dòng)過(guò)程中的 Agent 加載、生效流程,并對(duì)常見(jiàn)字節(jié)碼操作工具進(jìn)行了簡(jiǎn)單應(yīng)用。

注:本文僅討論 javaagent “啟動(dòng)時(shí)加載”。

一、技術(shù)為業(yè)務(wù)需求服務(wù)

技術(shù)是工具,是解決問(wèn)題的途徑。針對(duì)不同的業(yè)務(wù)需求場(chǎng)景,可以使用不同的技術(shù)實(shí)現(xiàn)。

通過(guò)一部攔截器的流浪史來(lái)引入主題:

一個(gè)簡(jiǎn)單的demo

1、基礎(chǔ)版:新建一個(gè)Dog對(duì)象,然后調(diào)用成員方法輸出到控制臺(tái)

被調(diào)用方

調(diào)用方


2、加強(qiáng)版:需要統(tǒng)計(jì)方法執(zhí)行的時(shí)間

常規(guī)開(kāi)發(fā):

被調(diào)用方


調(diào)用方


3、從被調(diào)用方剝離非業(yè)務(wù)邏輯

面向?qū)ο笤O(shè)計(jì)原則,對(duì)象應(yīng)該盡可能專注自己職責(zé)范圍內(nèi)的事情,狗只負(fù)責(zé)叫,不負(fù)責(zé)統(tǒng)計(jì)自己叫了多長(zhǎng)時(shí)間,因此統(tǒng)計(jì)代碼應(yīng)該移出Dog類。

3.1 方法提取

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


3.3 類解耦合(使用動(dòng)態(tài)代理方式-CGLib/JDK Proxy,這里Dog類沒(méi)有實(shí)現(xiàn)接口,使用CGLib)



至此,非業(yè)務(wù)邏輯由從被調(diào)用方剝離出來(lái)了,同時(shí)我們也發(fā)現(xiàn)調(diào)用方代碼卻遭到改變,Main class里面需要添加動(dòng)態(tài)代理類的處理邏輯。假如不允許改變調(diào)用方代碼,進(jìn)一步處理。

4、調(diào)用方代碼剝離(切面–AspectJ)

切面

被調(diào)方

調(diào)用方

注意:此時(shí)直接運(yùn)行Main class切面不會(huì)生效,運(yùn)行前先進(jìn)行編譯期織入 java -jar ASPECTJ\_TOOLS -cpASPECTJ_RT -sourceroots src/main/java/ -d target/classes ...

至此,調(diào)用方不用顯式地調(diào)用動(dòng)態(tài)代理邏輯,編譯期織入到class中去了(這里已經(jīng)聞到了代碼增強(qiáng)的氣味了)。

切面邏輯雖然與具體的業(yè)務(wù)邏輯解耦合了,獨(dú)立出切面類。但是是否生效仍然由業(yè)務(wù)代碼(切面類)去控制。無(wú)論如何,都需要業(yè)務(wù)方改造,添加切面邏輯代碼。

能不能更進(jìn)一步,連切面都不寫(xiě),也讓切面邏輯生效呢?

5、javaagent 版本–隱式地,無(wú)侵入地添加切面邏輯

  • 新建獨(dú)立的agent工程

  • 添加MANIFEST.MF文件以及Premain-Class,premain屬性

  • 編譯包含目標(biāo)邏輯的源文件生成class文件

  • 注冊(cè)ClassFileTransfer,在transform方法中替換byte[]

  • MANIFEST.MF指定premain函數(shù)和打開(kāi)類增強(qiáng)開(kāi)關(guān)

  • 編譯輸出jar包

MANIFEST.MF文件。

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

class轉(zhuǎn)換器,將新的Dog.class替換舊的Dog.class。

maven打包輸出Agent.jar

上面的javaagent實(shí)現(xiàn)細(xì)節(jié)可以先存疑,后面會(huì)深入描述,只需要知道按照這樣的步驟可以實(shí)現(xiàn)我們的需求。

對(duì)于業(yè)務(wù)方而言:代碼完全沒(méi)有變化:

被調(diào)用方

調(diào)用方

想要使切面邏輯生效,只需要在啟動(dòng)命令參數(shù)中加入-javaagent 選項(xiàng),指向 Agent 的 jar 包。

這樣,攔截器邏輯以一種插件的形式抽取出來(lái)了,使用的時(shí)候加載插件就可以了。

小結(jié)一下

  1. 不同需求場(chǎng)景下,可以不同的方式實(shí)現(xiàn)切面攔截邏輯;

  2. AspectJ或者SpringAop只是一種對(duì)開(kāi)發(fā)者友好的快捷方式,本質(zhì)上還是修改的業(yè)務(wù)代碼,只不過(guò)隱藏了調(diào)用邏輯,并不能真正“無(wú)侵入“;

  3. javaagent可以無(wú)侵入的修改一個(gè)已發(fā)布的java組件的運(yùn)行邏輯。

二、什么是字節(jié)碼?

byte[]

1、回歸原始:JDK 里面提供了很多有用的工具

在我們剛開(kāi)始學(xué)習(xí) Java 語(yǔ)言時(shí)候的 demo 運(yùn)行:

編寫(xiě)原始 Java 文件:




使用 Javac 編譯字節(jié)碼文件:

Javac生產(chǎn)的 class 文件有什么作用呢?

Java 語(yǔ)言一次編譯,到處運(yùn)行的核心基礎(chǔ)-JVM。

2、class文件到底是個(gè)什么東西?

先用文本編輯器暴力打開(kāi)看看:

看不懂?換個(gè)方式:


想看個(gè)明白?繼續(xù)整:使用010editor打開(kāi)。


各個(gè)數(shù)據(jù)項(xiàng)按順序緊密的從前向后排列, 相鄰的項(xiàng)之間沒(méi)有間隙, 這樣可以使得class文件非常緊湊, 體積輕巧, 可以被JVM快速的加載至內(nèi)存, 并且占據(jù)較少的內(nèi)存空間。

主要包含的信息:

(1)魔數(shù)

(2)版本號(hào)(參考文末例子:JRE版本錯(cuò)誤)

(3)常量池容量

(4)常量池:

  • 文字字符串, 常量值

  • 當(dāng)前類的類名, 字段名, 方法名, 各個(gè)字段和方法的描述符

  • 對(duì)當(dāng)前類的字段和方法的引用信息, 當(dāng)前類中對(duì)其他類的引用信息等等

(5)其他屬性

常量池如何索引:

相互索引:

例如方法索引,獲取classIndex和nameAndTypeIndex,通過(guò)數(shù)組下標(biāo),可以找到該方法所屬的class和方法名稱。

MethodRef

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

常量池索引和字節(jié)碼指令的執(zhí)行。

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

Main.class字節(jié)碼:

Dog.class字節(jié)碼:

可以看到字節(jié)碼具備一定的可讀性,對(duì)照著源碼,可以按照?qǐng)?zhí)行邏輯走一遍字節(jié)碼執(zhí)行流程,相關(guān)指令的含義很容易從網(wǎng)上查詢到。

至此,我們通過(guò)一個(gè)簡(jiǎn)單的demo執(zhí)行流程,大致了解了常量的引用以及一個(gè)簡(jiǎn)單java方法對(duì)應(yīng)的字節(jié)碼指令執(zhí)行過(guò)程。

注:

**stack:**最大操作數(shù)棧,JVM運(yùn)行時(shí)會(huì)根據(jù)這個(gè)值來(lái)分配棧幀(Frame)中的操作棧深度。

**locals:**局部變量所需的存儲(chǔ)空間,單位為Slot。

args_size: 方法參數(shù)的個(gè)數(shù)。

壓棧:字節(jié)碼指令執(zhí)行過(guò)程中涉及到了很多壓棧操作:JVM是一個(gè)基于棧的架構(gòu)。方法執(zhí)行的時(shí)候(包括main方法),在棧上會(huì)分配一個(gè)新的幀,這個(gè)棧幀包含一組局部變量。

這組局部變量包含了方法運(yùn)行過(guò)程中用到的所有變量,包括this引用,所有的方法參數(shù),以及其它局部定義的變量。

小結(jié)一下

  1. class文件即字節(jié)碼是所有屬性,方法邏輯的合集。

  2. 通過(guò)字節(jié)碼二進(jìn)制文件將開(kāi)發(fā)者與虛擬機(jī)進(jìn)行了“解耦”。

  3. 推理:修改某些字節(jié)或者替換整個(gè)二進(jìn)制流可以修改運(yùn)行時(shí)邏輯 。

三、如何增強(qiáng)字節(jié)碼?

byte[] → byte[]

思路:

  1. 如前述方式直接替換為目標(biāo)邏輯編譯后的字節(jié)碼。

  2. 手術(shù)刀式精準(zhǔn)操作,修改/添加某些位置的byte。

  3. 高級(jí)API。

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

示例:

ASM

指令級(jí)別的字節(jié)碼操作(性能強(qiáng)悍)。



指令→ASM api 對(duì)應(yīng)關(guān)系(這里將原始類做了簡(jiǎn)化,將字符串拼接邏輯去掉,僅僅輸出時(shí)間。因?yàn)橐粋€(gè)簡(jiǎn)單的字符串拼接過(guò)程,轉(zhuǎn)換成字節(jié)碼指令可能需要很多行)。

先看看目標(biāo)源碼與字節(jié)碼指令的一一映射關(guān)系。

再看看增強(qiáng)字節(jié)碼邏輯與目標(biāo)源碼的字節(jié)碼的一一映射關(guān)系。

38.png

通過(guò)對(duì)比我們可以發(fā)現(xiàn),ASM的API精確到字節(jié)碼指令級(jí)別,所有的臨時(shí)變量存儲(chǔ),壓棧操作,靜態(tài)/實(shí)例方法的調(diào)用都有對(duì)應(yīng)的API操作。

javassist:(dubbo)

提供字節(jié)碼級(jí)別的API,類似ASM,不再贅述。

提供源碼級(jí)別的API,針對(duì)本文的案例,實(shí)現(xiàn)如下:

ByteBuddy

基于ASM的高級(jí)API,使我們對(duì)字節(jié)碼的操作提升到更抽象層次。開(kāi)發(fā)者只需要知道要實(shí)現(xiàn)什么目標(biāo),如何使用對(duì)應(yīng)的API,不用關(guān)心底層的字節(jié)碼指令排列,甚至可以不用了解字節(jié)碼指令。



關(guān)于相關(guān)框架的API不詳細(xì)說(shuō),有興趣的同學(xué)可以自行查詢相關(guān)資料。

小結(jié)一下

  1. 各種級(jí)別的API可以幫助開(kāi)發(fā)者輕松實(shí)現(xiàn)字節(jié)碼增強(qiáng),實(shí)現(xiàn)特定邏輯。

  2. 不論什么奇技淫巧,都離不開(kāi)Instrumentation機(jī)制。

四、增強(qiáng)的 byte[] 是如何影響 JVM 的?

Event --> CallBack

由前文總結(jié),引入Instrumentation機(jī)制。

1、鋪墊知識(shí)點(diǎn):

(1)JVMTI

JVMTI 是基于事件驅(qū)動(dòng)的,JVM 每執(zhí)行到一定的邏輯就會(huì)調(diào)用一些事件的回調(diào)接口(如果有的話),這些接口可以供開(kāi)發(fā)者擴(kuò)展自己的邏輯。

JVMTIAgent 使用JVMTI來(lái)查詢或控制JVM,JVMTIAgent與目標(biāo)JVM運(yùn)行在同一個(gè)進(jìn)程中,通過(guò)JVMTI進(jìn)行通信,最大化控制能力,最小化通信成本。

典型場(chǎng)景下,JVMTI代理會(huì)被實(shí)現(xiàn)的非常緊湊,其他的進(jìn)程會(huì)與JVMTI代理進(jìn)行通信。比如jdwp(IDEA遠(yuǎn)程調(diào)試)。

(2)JVMTIAgent

表現(xiàn)形式:

(1)linux: .so文件

  • windows: .dll文件

  • c/c++ 動(dòng)態(tài)鏈接庫(kù)

(2)JPLISAgent: .jar文件

命令行參數(shù)

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

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

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

  • 可加載多個(gè),通過(guò)options區(qū)分

實(shí)現(xiàn)接口

(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機(jī)制

(1)JavaSE1.5 啟動(dòng)時(shí)加載(本文重點(diǎn))。

(2)JavaSE1.6 運(yùn)行時(shí)加載。

2、簡(jiǎn)化了的核心流程邏輯

命令參數(shù):-javaagent:/data/../../Agent.jar=optoions。

虛擬機(jī)創(chuàng)建-構(gòu)建并初始化Agent-注冊(cè)VMInit事件。

虛擬機(jī)初始化-觸發(fā)VMInit事件-Agent start方法-注冊(cè)回調(diào)函數(shù)并監(jiān)聽(tīng)ClassFileLoadHook。

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

3、Java 虛擬機(jī)啟動(dòng)過(guò)程中 Agent 相關(guān)的流程:

(1)創(chuàng)建JVM的時(shí)候初始化agent

  1. 啟動(dòng)時(shí)讀取jvm命令,-agentlib -agentpath -javaagent,并構(gòu)建了Agent Library鏈表構(gòu)建了Agent Library鏈表。

  2. 對(duì)agent鏈表中的每個(gè)agent,加載所指定的動(dòng)態(tài)庫(kù)(如instrument.so), 并調(diào)用里面的Agent_OnLoad方法。

  3. 創(chuàng)建并初始化 JPLISAgent,初始化了Premain class和包里的配置文件。

  4. 注冊(cè)VMInit事件。

Agent_onLoad
|-----|createNewJPLISAgent
|-----|-----|initializeJPLISAgent
|-----|-----|-----|eventHandlerVMInit  ---- >   VMInit



(2)虛擬機(jī)初始化




實(shí)際上是調(diào)用 java 類 sun.instrument.InstrumentationImpl 類里的方法loadClassAndCallPremain。


(3)觸發(fā)ClassFileLoadHook事件

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


實(shí)際調(diào)用的java方法 Instrumentationimpl.transform。


debug過(guò)程中通過(guò)ClassFileTransformer的transform函數(shù)的執(zhí)行堆棧印證。


到這里,增強(qiáng)的byte[]如何生效并影響運(yùn)行時(shí)class的過(guò)程基本可以串起來(lái)。

小結(jié)一下

  1. 虛擬機(jī)創(chuàng)建階段,初始化agent,解析,加載javaagent jar,注冊(cè)回調(diào)函數(shù)監(jiān)聽(tīng)VMInt事件。

  2. 虛擬機(jī)初始化階段,觸發(fā)VMInt回調(diào)函數(shù),注冊(cè)回調(diào)函數(shù)監(jiān)聽(tīng)ClassFileHook事件,同時(shí)執(zhí)行l(wèi)oadClassAndCallPremain函數(shù),注冊(cè)transformer。

  3. ClassLoader加載類的時(shí)候觸發(fā)tranform回調(diào),判斷是否目標(biāo)類,進(jìn)行對(duì)應(yīng)字節(jié)碼替換。

五、應(yīng)用

  • 監(jiān)控

  • 調(diào)試

  • 混淆

  • AOP增強(qiáng)

  • 日志記錄

非常規(guī)應(yīng)用:IDEA破解。

部分破解教程里面下載插件jar后,會(huì)要求你在IDEA的啟動(dòng)參數(shù)文件idea.vmoptions中添加一行,就是javaagent參數(shù)。


我們可以反編譯這個(gè)插件jar包看看,發(fā)現(xiàn)很多class因?yàn)榧恿嘶煜淳幾g后無(wú)法正常識(shí)別,但是核心入口Agent.class的主要工作就是注冊(cè)Transformer,可以推測(cè)這些Transformer的功能就是在IDEA啟動(dòng)時(shí)之前修改某些鑒定Lisence的邏輯。


六、總結(jié)回顧

通過(guò)介紹字節(jié)碼,字節(jié)碼操作工具以及openJDK關(guān)于Instrumention機(jī)制的部分源碼,探索了字節(jié)碼增強(qiáng)的實(shí)現(xiàn)原理。

簡(jiǎn)單介紹了相關(guān)技術(shù)的應(yīng)用場(chǎng)景。

七、附錄

  • SpringMVC-Interceptor

  • IDEA 遠(yuǎn)程調(diào)試

  • JRE版本錯(cuò)誤

作者: Neo

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容