涉及技術:
JVMTI ,javaagent,Attach API, Instrument ,Javassist
JVMTI:
JVM TI(JVM TOOL INTERFACE,JVM 工具接口)是 JVM 提供的一套對 JVM 進行操作的工具接口。通過JVMTI,可以實現對 JVM 的多種操作,它通過接口註冊 各種事件勾子,在 JVM 事件觸發時,同時觸發預定義的勾子,以實現對各個 JVM 事件的響應,事件包括類文件加載、異常產生與捕獲、線程啓動和結束、進入和退 出臨界區、成員變量修改、GC開始和結束、方法調用進入和退出、臨界區競爭與等 待、VM 啓動與退出等等。
javaagent
Agent 就是 JVMTI 的一種實現,Agent 有兩種啓動方式,一是隨 Java 進 程啓動而啓動;二是運行時載入,通過 attach API,將模塊(jar 包)動態地 Attach 到指定進程 id 的 Java 進程內。
Attach API
Attach API 的作用是提供 JVM 進程間通信的能力,比如說我們爲了讓另外一 個 JVM 進程把線上服務的線程 Dump 出來,會運行 jstack 或 jmap 的進程,並傳 遞 pid 的參數,告訴它要對哪個進程進行線程 Dump,這就是 Attach API 做的事情。
Instrument
instrument 是 JVM 提供的一個可以修改已加載類的類庫,專門爲 Java 語言編 寫的插樁服務提供支持。它需要依賴 JVMTI的 Attach API 機制實現。
Javassist
Javassist是一個開源的分析、編輯和創建Java字節碼的類庫,可以直接使用java編碼的形式,而不需要了解虛擬機指令,就能動態改變類的結構,或者動態生成類。
實現
因爲javaagent有兩種實現方式,但是我們想要實現的是在不停機的情況下進行字節碼增強,所以選擇使用Attach API進行動態載入。使用 javaagent 需要幾個步驟:
- 定義一個 MANIFEST.MF 文件,必須包含 Agent-Class 選項,通常也會加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項。
- 創建一個Agent-Class 指定的類,類中包含 agentmain 方法,方法邏輯由用戶自己確定。
public class AgentMainTest{
public static void agentmain( String agentArgs, Instrumentation instrumentation ){
// 指定我們自己定義的 Transformer,在其中利用 Javassist 做字節碼替換
instrumentation.addTransformer( new DefineTransformer(), true );
try{
instrumentation.retransformClasses( Base.class );
System.out.println( "Agent Load Done." );
}
catch( UnmodifiableClassException e ){
System.out.println( "agent load failed!" );
}
}
}
public class DefineTransformer implements ClassFileTransformer{
// 類文件加載的時候調用
@Override
public byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer ){
try{
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get( "com.bj58.jvmti.Base" );
CtMethod m = cc.getDeclaredMethod( "run" );
// 解凍class,以便修改class內容
cc.defrost();
m.insertBefore( "{ System.out.println(\"run() start\"); }" );
m.insertAfter( "{ System.out.println(\"run() end\"); }" );
return cc.toBytecode();
}
catch( Exception e ){
e.printStackTrace();
}
return null;
}
}
- 將 agentmain 的類和 MANIFEST.MF 文件打成 jar 包。
- 最後利用 Attach API,將我們打包好的 jar 包 Attach 到指定的 JVM pid 上代碼如下
public class TestAgentMain {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException{
System.out.println("running JVM start ");
// 使用attach api, Attach到目標 JVM 上,建立通信管道
VirtualMachine vm = VirtualMachine.attach("15219");
// 讓目標JVM加載Agent
vm.loadAgent("/Users/benettchen/Desktop/javaagent.jar");
}
}
Base類
public class Base{
public static void main( String[] args ){
System.out.println( "pid:" + populateProcessId() );
while( true ){
try{
Thread.sleep( 3000L );
}
catch( Exception e ){
break;
}
run();
}
}
private static void run(){
System.out.println( "working..." );
}
private static String populateProcessId(){
/*
* runtimeMXBean.getName()取得的值包括兩個部分:PID和hostname,兩者用@連接。
*/
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
return runtimeMXBean.getName().split( "@" )[ 0 ];
}
}
運行流程梳理:
- 首先編寫agent項目,並其打包成jar包。
- 啓動Base這個Java進程,獲取該進程id。
- 啓動TestAgentMain,將jar包動態的attach到目標jvm(Base進程)上
- 在attach後,由於在MANIFEST.MF 中指定了 Agent-Class,所以會走到AgentMainTest的agentmain方法。
- 使用javassist修改Base類的字節碼,然後利用Instrumentation完成類的重新加載。
以下爲運行時重新載入jar的效果:
從運行結果可以看出,在載入jar包後,在run方法前後分別輸出了“run() start”和“run() end”,實現了在運行時字節碼增強並重新載入到jvm的目的。
閒話:
自己動手實現完對Java運行程序字節碼動態增強後,突然感覺可以利用這個技術對Java程序開掛,哈哈。想到自己的idea是Java語言編寫的,而且也是通過jar包進行破解的,就想看看它的實現是否是按照這個思路。於是乎興奮的將破解的jar包進行反編譯,發現了驚奇的一幕:
它用的正是javaagent的第一種實現,在JVM啓動時進行加載,所以在使用這個jar包後,需要重啓idea。