JavaAgent學習筆記
什麼是JavaAgent?
前幾天和同學在排查一個線上問題時,發現一個有漏洞的HSF請求,急需對該HSF進行遮蔽,但是發現該系統未接入限流,這下懵逼了。但是一個同學靈機一動,使用了故障演練平臺對該介面模擬hsf呼叫方異常。遮蔽了該請求。頓時對這個平臺的技術產生了興趣。故障演練平臺。
粗略檢視了它的手冊,發現其使用了JavaAgent的技術。那麼,什麼是JavaAgent呢?
JavaAgent是基於JVMTI實現的。(從jdk1.5.0 版本加入 Java 虛擬機器工具介面,用於監控JVM各項資訊)。以下是JavaAgent的主要功能。
- 可以在載入class檔案之前做攔截把位元組碼做修改
- 可以在執行期將已經載入的類的位元組碼做變更,但是這種情況下會有很多的限制,後面會詳細說
- 還有其他的一些小眾的功能
- 獲取所有已經被載入過的類
- 獲取所有已經被初始化過了的類(執行過了clinit方法,是上面的一個子集)
- 獲取某個物件的大小
- 將某個jar加入到bootstrapclasspath裡作為高優先順序被bootstrapClassloader載入
- 將某個jar加入到classpath裡供AppClassloard去載入
- 設定某些native方法的字首,主要在查詢native方法的時候做規則匹配
所以 ,初步認為是故障演練平臺使用了mkagent,對hsf介面進行了mock,讓其模擬異常。實現介面呼叫的失敗。
如何實現簡單的JavaAgent
現階段實現agent至少有二種方式,
一種是基於JVMTI的“java -agentpath:”
通過載入使用C編譯的動態庫的方式實現。在linux與mac下動態庫是“libname.so”,windows下動態庫是“libname.dll”。一般會實現以下三個方法。
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved);
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved);
JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm);
-
Agent_OnLoad
函式,如果agent是在啟動的時候載入的,也就是在vm引數裡通過-agentlib來指定,那在啟動過程中就會去執行這個agent裡的Agent_OnLoad
函式。 -
Agent_OnAttach
函式,如果agent不是在啟動的時候載入的,是我們先attach到目標程式上,然後給對應的目標程式傳送load命令來載入agent,在載入過程中就會呼叫Agent_OnAttach
函式。 -
Agent_OnUnload
函式,在agent做解除安裝的時候呼叫,不過貌似基本上很少實現它。
首先是一段C++的動態庫程式碼,注意標頭檔案jvmti.h與jni_md.h
/*
* JVMTI agent
*/
#include <jvmti.h>
#include <string>
#include <cstring>
#include <iostream>
#include <list>
#include <map>
#include <set>
#include <stdlib.h>
#include <jni_md.h>
JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *jvm, char *options, void *reserved) {
printf("Agent_OnAttach success!
");
jvmtiEnv *jvmti;
jint result = jvm->GetEnv((void **) &jvmti, JVMTI_VERSION_1_1);
if (result != JNI_OK) {
printf("ERROR: Unable to access JVMTI!
");
}
jvmtiError err = (jvmtiError) 0;
jclass *classes;
jint count;
err = jvmti->GetLoadedClasses(&count, &classes);//獲取class
if (err) {
printf("ERROR: JVMTI GetLoadedClasses failed!
");
}
for (int i = 0; i < count; i++) {
char *sig;
jvmti->GetClassSignature(classes[i], &sig, NULL);//獲取並列印class簽名
printf("cls sig=%s
", sig);
}
return err;
}
JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) {
// nothing to do
}
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved) {
printf("Agent_OnLoad success!
");
jvmtiError err = (jvmtiError) 0;
return err;
}
以上程式碼主要實現了2個方法,Agent_OnAttach是在程式在執行時載入,並列印所有的載入類。Agent_OnLoad是在程式啟動時載入,列印一個語句。
然後對這個CPP進行編譯。生成一個.so的動態庫檔案。
g++ -I${JAVA_HOME}/include/ -I${JAVA_HOME}/include/darwin Agent.cpp -fPIC -shared -o libagent.so
然後是被agent的目標的類,並且對其進行編譯
public class TestMain {
public static void main(String[] args) throws InterruptedException {
System.out.println("JVMTI agent Test start");
int i = 0;
while (i < 100) {
Thread.sleep(1000);
i++;
System.out.println(i);
}
}
}
然後開始嘗試程式啟動階段的agent,使用以下指令進行執行
java -agentpath:/Users/archersblood/Desktop/libagent.so TestMain
可以看到日誌檔案的輸出。
Agent_OnLoad success!
JVMTI agent Test start
1
2
3
4
5
6
7
8
9
10
11
12
這是程式在啟動的時候進行agent了,jvm執行了動態庫中的Agent_OnLoad程式碼。
另外就是程式執行中的agent了。現在需要以下程式碼。
import java.io.IOException;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
public class TestAgent {
public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException,
AgentInitializationException {
String pid = "1136"; // java程式pid
String agentPath = "/Users/archersblood/Desktop/libagent.so"; // agent.so的路徑
String options = null;// 傳入agent的引數
VirtualMachine virtualMachine = com.sun.tools.attach.VirtualMachine.attach(pid);
virtualMachine.loadAgentPath(agentPath, options);
virtualMachine.detach();
}
}
當執行TestMain的main函式的時候,ps aux | grep java 檢視TestMain的PID,然後修改以上程式碼中的pid引數,讓Agent載入到指定的java程式中。然後立馬執行TestAgent,就會看到以下日誌。
66
67
68
69
70
71
72
Agent_OnAttach success!
cls sig=Lagent/TestMain;
cls sig=Lorg/objectweb/asm/MethodWriter;
cls sig=Lorg/objectweb/asm/FieldWriter;
cls sig=[Lorg/objectweb/asm/Item;
cls sig=Lorg/objectweb/asm/Item;
cls sig=Lorg/objectweb/asm/ByteVector;
cls sig=Lorg/objectweb/asm/FieldVisitor;
cls sig=Lorg/objectweb/asm/MethodVisitor;
cls sig=Lorg/objectweb/asm/AnnotationVisitor;
cls sig=Lorg/objectweb/asm/ClassWriter;
cls sig=Lorg/objectweb/asm/ClassVisitor;
cls sig=Lagent/MyAgent;
cls sig=Ljava/lang/Void;
cls sig=Ljava/lang/Class$MethodArray;
cls sig=Lsun/launcher/LauncherHelper$FXHelper;
cls sig=[Lsun/launcher/LauncherHelper;
cls sig=Lsun/launcher/LauncherHelper;
cls sig=Lsun/usagetracker/UsageTrackerClient$3;
cls sig=Lsun/usagetracker/UsageTrackerClient$4;
cls sig=Lsun/usagetracker/UsageTrackerClient$1;
cls sig=Ljava/util/concurrent/atomic/AtomicBoolean;
cls sig=Lsun/usagetracker/UsageTrackerClient;
cls sig=Lsun/misc/PostVMInitHook;
cls sig=Ljava/lang/invoke/MethodHandleStatics$1;
cls sig=Ljava/lang/invoke/MethodHandleStatics;
cls sig=Ljava/lang/invoke/MemberName$Factory;
cls sig=Ljava/lang/ClassValue$Version;
cls sig=Ljava/lang/ClassValue$Identity;
cls sig=[Ljava/lang/ClassValue$Entry;
cls sig=Ljava/lang/ClassValue$Entry;
cls sig=Ljava/lang/invoke/MethodHandleImpl$4;
cls sig=Ljava/lang/ClassValue;
cls sig=[Ljava/lang/invoke/MethodHandle;
cls sig=Ljava/lang/invoke/MethodHandleImpl$3;
cls sig=Ljava/lang/invoke/MethodHandleImpl$2;
cls sig=Ljava/util/function/Function;
cls sig=Ljava/lang/invoke/MethodHandleImpl$1;
cls sig=Ljava/lang/invoke/MethodHandleImpl;
cls sig=Ljava/io/FileOutputStream$1;
cls sig=Ljava/lang/IllegalStateException;
cls sig=Ljava/util/concurrent/ConcurrentHashMap$ForwardingNode;
cls sig=Lsun/security/util/ManifestEntryVerifier;
cls sig=Ljava/io/ByteArrayOutputStream;
cls sig=Ljava/util/jar/JarVerifier$3;
cls sig=[Ljava/security/CodeSigner;
cls sig=Ljava/security/CodeSigner;
cls sig=Ljava/util/jar/JarVerifier;
cls sig=Lsun/misc/ASCIICaseInsensitiveComparator;
cls sig=Ljava/util/jar/Attributes$Name;
cls sig=Ljava/util/jar/Manifest$FastInputStream;
cls sig=Ljava/util/jar/Attributes;
cls sig=Lsun/misc/URLClassPath$JarLoader$2;
cls sig=Lsun/misc/IOUtils;
cls sig=Lsun/misc/ExtensionDependency;
計數器列印到一半時,執行了動態庫中的Agent_OnAttach方法。
基於-javaagent的premain方法
以上是使用JVMTI實現的agent。但是故障演練平臺應該不是用這種方法。在改平臺的手冊上看到了這句話:
如圖可知,mk修改業務java啟動程式指令碼(setenv.sh)就是增加了如下一行:
-javaagent:/usr/alisys/dragoon/libexec/monkeyking/mkagent.jar
-Dproject.name=sonar 表示業務java程式是sanar
先看以下程式碼
package agent;
import java.io.File;
import java.io.FileOutputStream;
import java.lang.instrument.Instrumentation;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
public class MyAgent {
/**
* 該方法在main方法之前執行,與main方法執行在同一個JVM中
* 並被同一個System ClassLoader裝載
* 被統一的安全策略(security policy)和上下文(context)管理
*
* @param agentOps
* @param inst
* @throws Exception
*/
public static void premain(String agentOps, Instrumentation inst) throws Exception {
System.out.println("=========premain方法執行========");
System.out.println(agentOps);
ClassWriter cw = new ClassWriter(0);
//通過visit方法確定類的頭部資訊
cw.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC+Opcodes.ACC_ABSTRACT+Opcodes.ACC_INTERFACE,
"com/asm3/Comparable", null, "java/lang/Object", new String[]{"com/asm3/Mesurable"});
//定義類的屬性
cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC,
"LESS", "I", null, new Integer(-1)).visitEnd();
cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC,
"EQUAL", "I", null, new Integer(0)).visitEnd();
cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC,
"GREATER", "I", null, new Integer(1)).visitEnd();
//定義類的方法
cw.visitMethod(Opcodes.ACC_PUBLIC+Opcodes.ACC_ABSTRACT, "compareTo",
"(Ljava/lang/Object;)I", null, null).visitEnd();
cw.visitEnd(); //使cw類已經完成
//將cw轉換成位元組陣列寫到檔案裡面去
byte[] data = cw.toByteArray();
File file = new File("/Users/archersblood/Desktop/test.class");
FileOutputStream fout = new FileOutputStream(file);
fout.write(data);
fout.close();
}
/**
* 如果不存在 premain(String agentOps, Instrumentation inst)
* 則會執行 premain(String agentOps)
*
* @param agentOps
* @author
*/
public static void premain(String agentOps) {
System.out.println("=========premain方法執行2========");
System.out.println(agentOps);
}
}
該程式碼使用了javaagent與ASM(Java位元組碼操縱框架)技術,作用是在程式執行前生成一個com.asm3.Comparable介面,該agent預設是在程式main執行之前,執行premain方法。
除了該段程式碼,還要在該類包介面下建立META-INF資料夾,並建立MANIFEST.MF檔案,以下為檔案內容:
Manifest-Version: 1.0
Premain-Class: agent.MyAgent
Can-Redefine-Classes: true
指定agent.MyAgent為PremainClass。同時將MyAgent與MF檔案打包,我使用的是eclipse匯出。然後使用以下指令(可以參考故障演練平臺的配置)
java -javaagent:/Users/archersblood/Desktop/myAgent.jar TestMain
=========premain方法執行========
null
JVMTI agent Test start
1
2
3
4
5
6
7
8
9
10
11
12
13
最後,生成了一個test.class。對其反編譯
javap -c test.class >test.txt
開啟該檔案,就是利用asm位元組碼操作生成的一個介面
public interface com.asm3.Comparable extends com.asm3.Mesurable {
public static final int LESS;
public static final int EQUAL;
public static final int GREATER;
public abstract int compareTo(java.lang.Object);
}
推測,可能故障演練平臺上就是使用的就是javaagent+asm技術,在應用啟動或執行時,通過修改類的位元組碼,模擬系統的各種故障。(以上都是瞎猜,猜錯了我不負責,演練平臺的同學別找我^o^)
PS:文章中的程式碼大多來自於網路,但本人親測有效。
相關文章
- numpy的學習筆記\pandas學習筆記筆記
- IT學習筆記筆記
- 學習筆記筆記
- 【學習筆記】數學筆記
- 《JAVA學習指南》學習筆記Java筆記
- Elasticsearch學習筆記Elasticsearch筆記
- Scala學習筆記筆記
- MySql學習筆記MySql筆記
- jQuery 學習筆記jQuery筆記
- react學習筆記React筆記
- 學習筆記(4.3)筆記
- 學習筆記(4.4)筆記
- 學習筆記(3.29)筆記
- 學習筆記(4.1)筆記
- AOP學習筆記筆記
- AspectJ學習筆記筆記
- 學習筆記(3.27)筆記
- 學習筆記(4.2)筆記
- golang 學習筆記Golang筆記
- Zookeeper學習筆記筆記
- 學習筆記(3.24)筆記
- 學習筆記(3.25)筆記
- 學習筆記(3.21)筆記
- GitHub學習筆記Github筆記
- jest 學習筆記筆記
- typescript 學習筆記TypeScript筆記
- Echarts學習筆記Echarts筆記
- js學習筆記JS筆記
- shell學習筆記筆記
- Dubbo 學習筆記筆記
- SVN 學習筆記筆記
- 笨笨學習筆記筆記
- vue學習筆記Vue筆記
- wepack學習筆記筆記
- redis學習筆記Redis筆記
- PureMVC學習筆記REMMVC筆記
- gitee 學習筆記Gitee筆記
- 機器學習學習筆記機器學習筆記