0 前言
前一節講述了基於JVMTI如何實現Agent,還有一種是基於Java Instrument API實現Agent,可以在Java程式碼層面編寫Agent程式碼,而非基於C++/C的程式碼,具體使用可參考《Java Instrument 功能使用及原理》:
以 -javaagent:為開頭的預設為instrument的agent;
那麼以上這兩種Agent實現方式,又是在JVMTI原始碼中如何執行工作呢?
1 初始化 Agent
在JVM啟動時,會讀取JVM命令列引數 -agentlib -agentpath -javaagent
,並構建了Agent Library連結串列。初始化 Agent 程式碼如下:
if (match_option(option, "-agentlib:", &tail) || (is_absolute_path = match_option(option, "-agentpath:", &tail))) {
if(tail != NULL) {
const char* pos = strchr(tail, '=');
size_t len = (pos == NULL) ? strlen(tail) : pos - tail;
char* name = strncpy(NEW_C_HEAP_ARRAY(char, len + 1), tail, len);
name[len] = '\0';
char *options = NULL;
if(pos != NULL) {
options = strcpy(NEW_C_HEAP_ARRAY(char, strlen(pos + 1) + 1), pos + 1);
}
if ((strcmp(name, "hprof") == 0) || (strcmp(name, "jdwp") == 0)) {
warning("profiling and debugging agents are not supported with Kernel VM");
} else if
// JVMTI_KERNEL 構建Agent Library連結串列
add_init_agent(name, options, is_absolute_path);
}
} else if (match_option(option, "-javaagent:", &tail)) {
// -javaagent
if(tail != NULL) {
char *options = strcpy(NEW_C_HEAP_ARRAY(char, strlen(tail) + 1), tail);
// 構建Agent Library連結串列
add_init_agent("instrument", options, false);
}
// -Xnoclassgc
}
複製程式碼
2 載入Agent連結庫
在啟動JVM create_vm時,對agent連結串列中的每個agent庫,載入所指定的動態庫, 並呼叫裡面的Agent_OnLoad方法,比如:對於Java Instrument Agent載入就是對libinstrument的動態庫instrument.so載入:
// Create agents for -agentlib: -agentpath: and converted -Xrun
void Threads::create_vm_init_agents() {
extern struct JavaVM_ main_vm;
AgentLibrary* agent;
JvmtiExport::enter_onload_phase();
for (agent = Arguments::agents(); agent != NULL; agent = agent->next()) {
OnLoadEntry_t on_load_entry = lookup_agent_on_load(agent);
if (on_load_entry != NULL) {
// 呼叫 Agent_OnLoad 函式
jint err = (*on_load_entry)(&main_vm, agent->options(), NULL);
if (err != JNI_OK) {
vm_exit_during_initialization("agent library failed to init", agent->name());
}
} else {
vm_exit_during_initialization("Could not find Agent_OnLoad function in the agent library", agent->name());
}
}
JvmtiExport::enter_primordial_phase();
}
複製程式碼
3 建立 Instrument JPLISAgent
在方法Agent_OnLoad中建立一個新的 JPLISAgent(Java Programming Language Instrumentation Services Agent),初始化了類和包裡的配置檔案,並且同時從Vm環境中獲取了 jvmtiEnv 的環境。
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *tail, void * reserved) {
JPLISInitializationError initerror = JPLIS_INIT_ERROR_NONE;
jint result = JNI_OK;
JPLISAgent * agent = NULL;
// 建立一個新的JPLISAgent物件
initerror = createNewJPLISAgent(vm, &agent);
if ( initerror == JPLIS_INIT_ERROR_NONE ) {
if (parseArgumentTail(tail, &jarfile, &options) != 0) {
fprintf(stderr, "-javaagent: memory allocation failure.\n");
return JNI_ERR;
}
attributes = readAttributes(jarfile);
if (attributes == NULL) {
fprintf(stderr, "Error opening zip file or JAR manifest missing : %s\n", jarfile);
free(jarfile);
if (options != NULL) free(options);
return JNI_ERR;
}
premainClass = getAttribute(attributes, "Premain-Class");
if (premainClass == NULL) {
fprintf(stderr, "Failed to find Premain-Class manifest attribute in %s\n", jarfile);
free(jarfile);
if (options != NULL) free(options);
freeAttributes(attributes);
return JNI_ERR;
}
/*
* Add to the jarfile 把jar檔案追加到agent的classpath中。
*/
appendClassPath(agent, jarfile);
……
}
複製程式碼
在程式碼中,可以看到在 讀取jar的配置檔案MANIFEST裡Premain-Class,並且把jar檔案追加到agent的class path中。
4 JVMTI 回撥介面註冊與執行
以下是JVMTI的一些回撥介面,通過這些回撥介面設定回撥函式指標:
typedef struct {
/* 50 : VM Initialization Event */
jvmtiEventVMInit VMInit;
/* 51 : VM Death Event */
jvmtiEventVMDeath VMDeath;
/* 52 : Thread Start */
jvmtiEventThreadStart ThreadStart;
/* 53 : Thread End */
jvmtiEventThreadEnd ThreadEnd;
/* 54 : Class File Load Hook */
jvmtiEventClassFileLoadHook ClassFileLoadHook;
/* 55 : Class Load */
jvmtiEventClassLoad ClassLoad;
/* 56 : Class Prepare */
jvmtiEventClassPrepare ClassPrepare;
/* 57 : VM Start Event */
jvmtiEventVMStart VMStart;
/* 58 : Exception */
jvmtiEventException Exception;
/* 59 : Exception Catch */
jvmtiEventExceptionCatch ExceptionCatch;
/* 60 : Single Step */
jvmtiEventSingleStep SingleStep;
/* 61 : Frame Pop */
jvmtiEventFramePop FramePop;
/* 62 : Breakpoint */
jvmtiEventBreakpoint Breakpoint;
/* 63 : Field Access */
jvmtiEventFieldAccess FieldAccess;
/* 64 : Field Modification */
jvmtiEventFieldModification FieldModification;
/* 65 : Method Entry */
jvmtiEventMethodEntry MethodEntry;
/* 66 : Method Exit */
jvmtiEventMethodExit MethodExit;
/* 67 : Native Method Bind */
jvmtiEventNativeMethodBind NativeMethodBind;
/* 68 : Compiled Method Load */
jvmtiEventCompiledMethodLoad CompiledMethodLoad;
/* 69 : Compiled Method Unload */
jvmtiEventCompiledMethodUnload CompiledMethodUnload;
/* 70 : Dynamic Code Generated */
jvmtiEventDynamicCodeGenerated DynamicCodeGenerated;
/* 71 : Data Dump Request */
jvmtiEventDataDumpRequest DataDumpRequest;
/* 72 */
jvmtiEventReserved reserved72;
/* 73 : Monitor Wait */
jvmtiEventMonitorWait MonitorWait;
/* 74 : Monitor Waited */
jvmtiEventMonitorWaited MonitorWaited;
/* 75 : Monitor Contended Enter */
jvmtiEventMonitorContendedEnter MonitorContendedEnter;
/* 76 : Monitor Contended Entered */
jvmtiEventMonitorContendedEntered MonitorContendedEntered;
/* 77 */
jvmtiEventReserved reserved77;
/* 78 */
jvmtiEventReserved reserved78;
/* 79 */
jvmtiEventReserved reserved79;
/* 80 : Resource Exhausted */
jvmtiEventResourceExhausted ResourceExhausted;
/* 81 : Garbage Collection Start */
jvmtiEventGarbageCollectionStart GarbageCollectionStart;
/* 82 : Garbage Collection Finish */
jvmtiEventGarbageCollectionFinish GarbageCollectionFinish;
/* 83 : Object Free */
jvmtiEventObjectFree ObjectFree;
/* 84 : VM Object Allocation */
jvmtiEventVMObjectAlloc VMObjectAlloc;
} jvmtiEventCallbacks;
複製程式碼
4.1 執行jvmtiEventVMInit的回撥函式
虛擬機器在建立create_vm的時候,初始化了JVMTI環境後會執行JvmtiExport::post_vm_initialized()
; 方法,程式碼如下:
void JvmtiExport::post_vm_initialized() {
EVT_TRIG_TRACE(JVMTI_EVENT_VM_INIT, ("JVMTI Trg VM init event triggered" ));
// can now enable events
JvmtiEventController::vm_init();
JvmtiEnvIterator it;
for (JvmtiEnv* env = it.first(); env != NULL; env = it.next(env)) {
if (env->is_enabled(JVMTI_EVENT_VM_INIT)) {
EVT_TRACE(JVMTI_EVENT_VM_INIT, ("JVMTI Evt VM init event sent" ));
JavaThread *thread = JavaThread::current();
JvmtiThreadEventMark jem(thread);
JvmtiJavaThreadEventTransition jet(thread);
jvmtiEventVMInit callback = env->callbacks()->VMInit;
if (callback != NULL) {
// 呼叫了VMInit的回撥函式
(*callback)(env->jvmti_external(), jem.jni_env(), jem.jni_thread());
}
}
}
}
複製程式碼
4.2 執行jvmtiEventClassFileLoadHook的回撥函式
鉤子方法是jvmtiEventClassFileLoadHook的回撥方法,程式碼在classFileParser的檔案中:
instanceKlassHandle ClassFileParser::parseClassFile(symbolHandle name, Handle class_loader,Handle protection_domain, KlassHandle host_klass, GrowableArray<Handle>* cp_patches, symbolHandle& parsed_name,bool verify, TRAPS) {
……
if (JvmtiExport::should_post_class_file_load_hook()) {
unsigned char* ptr = cfs->buffer();
unsigned char* end_ptr = cfs->buffer() + cfs->length();
JvmtiExport::post_class_file_load_hook(name, class_loader, protection_domain,
&ptr, &end_ptr,
&cached_class_file_bytes,
&cached_class_file_length);
if (ptr != cfs->buffer()) {
// JVMTI agent has modified class file data.
// Set new class file stream using JVMTI agent modified
// class file data.
cfs = new ClassFileStream(ptr, end_ptr - ptr, cfs->source());
set_stream(cfs);
}
}
…
}
複製程式碼
在jvmtiexport::post_class_file_load_hook
函式最後 呼叫了post_to_env()函式:
void post_to_env(JvmtiEnv* env, bool caching_needed) {
unsigned char *new_data = NULL;
jint new_len = 0;
JvmtiClassFileLoadEventMark jem(_thread, _h_name, _class_loader,
_h_protection_domain,
_h_class_being_redefined);
JvmtiJavaThreadEventTransition jet(_thread);
JNIEnv* jni_env = (JvmtiEnv::get_phase() == JVMTI_PHASE_PRIMORDIAL)? NULL : jem.jni_env();
jvmtiEventClassFileLoadHook callback = env->callbacks()->ClassFileLoadHook;
if (callback != NULL) {
(*callback)(env->jvmti_external(), jni_env,
jem.class_being_redefined(),
jem.jloader(), jem.class_name(),
jem.protection_domain(),
_curr_len, _curr_data,
&new_len, &new_data);
}
......
}
複製程式碼
函式中呼叫了jvmtiEventClassFileLoadHook的回撥函式,也就是剛才在結構體中定義的jvmtiEventCallbacks。鉤子函式eventHandlerClassFileLoadHook:
void JNICALL eventHandlerClassFileLoadHook( jvmtiEnv * jvmtienv,
JNIEnv * jnienv,
jclass class_being_redefined,
jobject loader,
const char* name,
jobject protectionDomain,
jint class_data_len,
const unsigned char* class_data,
jint* new_class_data_len,
unsigned char** new_class_data) {
JPLISEnvironment * environment = NULL;
environment = getJPLISEnvironment(jvmtienv);
/* if something is internally inconsistent (no agent), just silently return without touching the buffer */
if ( environment != NULL ) {
jthrowable outstandingException = preserveThrowable(jnienv);
transformClassFile(environment->mAgent,
jnienv,
loader,
name,
class_being_redefined,
protectionDomain,
class_data_len,
class_data,
new_class_data_len,
new_class_data,
environment->mIsRetransformer);
restoreThrowable(jnienv, outstandingException);
}
}
複製程式碼
重要的是transformClassFile函式,看看它究竟做了啥事情:
transformedBufferObject = (*jnienv)->CallObjectMethod(
jnienv,
agent->mInstrumentationImpl,
agent->mTransform,
loaderObject,
classNameStringObject,
classBeingRedefined,
protectionDomain,
classFileBufferObject,
is_retransformer);
複製程式碼
也就是呼叫了InstrumentationImpl裡的transform方法,在InstrumentationImpl類裡通過TransformerManager的transform的方法最終呼叫我們自定義的MyTransformer的類的transform方法。
private byte[] transform(ClassLoader var1, String var2, Class var3, ProtectionDomain var4, byte[] var5, boolean var6) {
TransformerManager var7 = var6 ? this.mRetransfomableTransformerManager : this.mTransformerManager;
return var7 == null ? null : var7.transform(var1, var2, var3, var4, var5);
}
複製程式碼
4.3 註冊鉤子函式jvmtiEventClassFileLoadHook
如上,那麼鉤子函式jvmtiEventClassFileLoadHook是何時註冊的,回到剛才建立新的JPLISAgent程式碼中:
JPLISInitializationError createNewJPLISAgent(JavaVM * vm, JPLISAgent **agent_ptr) {
JPLISInitializationError initerror = JPLIS_INIT_ERROR_NONE;
jvmtiEnv * jvmtienv = NULL;
jint jnierror = JNI_OK;
*agent_ptr = NULL;
jnierror = (*vm)->GetEnv(vm,(void **) &jvmtienv,JVMTI_VERSION);
if (jnierror != JNI_OK) {
initerror = JPLIS_INIT_ERROR_CANNOT_CREATE_NATIVE_AGENT;
} else {
JPLISAgent * agent = allocateJPLISAgent(jvmtienv);
if (agent == NULL) {
initerror = JPLIS_INIT_ERROR_ALLOCATION_FAILURE;
} else {
initerror = initializeJPLISAgent(agent, vm, jvmtienv);
if (initerror == JPLIS_INIT_ERROR_NONE) {
*agent_ptr = agent;
} else {
deallocateJPLISAgent(jvmtienv, agent);
}
}
/* don't leak envs */
if ( initerror != JPLIS_INIT_ERROR_NONE ) {
jvmtiError jvmtierror = (*jvmtienv)->DisposeEnvironment(jvmtienv);
jplis_assert(jvmtierror == JVMTI_ERROR_NONE);
}
}
return initerror;
}
複製程式碼
函式initializeJPLISAgent初始化了JPLISAgent:
JPLISInitializationError initializeJPLISAgent( JPLISAgent * agent,JavaVM * vm,jvmtiEnv * jvmtienv) {
……
checkCapabilities(agent);
jvmtierror == (*jvmtienv)->GetPhase(jvmtienv, &phase);
jplis_assert(jvmtierror == JVMTI_ERROR_NONE);
if (phase == JVMTI_PHASE_LIVE) {
return JPLIS_INIT_ERROR_NONE;
}
/* now turn on the VMInit event */
if ( jvmtierror == JVMTI_ERROR_NONE ) {
jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.VMInit = &eventHandlerVMInit;
jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv, &callbacks, sizeof(callbacks));
jplis_assert(jvmtierror == JVMTI_ERROR_NONE);
}
……
}
複製程式碼
JPLISAgent裡首先 註冊了一個VMInit的初始化函式eventHandlerVMInit,跟蹤eventHandlerVMInit函式:
void JNICALL eventHandlerVMInit( jvmtiEnv * jvmtienv,
JNIEnv * jnienv,
jthread thread) {
JPLISEnvironment * environment = NULL;
jboolean success = JNI_FALSE;
environment = getJPLISEnvironment(jvmtienv);
/* process the premain calls on the all the JPL agents */
if ( environment != NULL ) {
jthrowable outstandingException = preserveThrowable(jnienv);
success = processJavaStart( environment->mAgent,
jnienv);
restoreThrowable(jnienv, outstandingException);
}
/* if we fail to start cleanly, bring down the JVM */
if ( !success ) {
abortJVM(jnienv, JPLIS_ERRORMESSAGE_CANNOTSTART);
}
}
複製程式碼
在processJavaStart裡:
jboolean processJavaStart(JPLISAgent * agent, JNIEnv * jnienv) {
jboolean result;
result = initializeFallbackError(jnienv);
jplis_assert(result);
if ( result ) {
result = createInstrumentationImpl(jnienv, agent);
jplis_assert(result);
}
if ( result ) {
result = setLivePhaseEventHandlers(agent);
jplis_assert(result);
}
if ( result ) {
result = startJavaAgent(agent, jnienv, agent->mAgentClassName, agent->mOptionsString,agent->mPremainCaller);
}
if ( result ) {
deallocateCommandLineData(agent);
}
return result;
}
複製程式碼
在setLivePhaseEventHandler函式中註冊了,程式碼如下:
callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook;
複製程式碼
5 JPLISAgent結構體
struct _JPLISAgent {
JavaVM * mJVM; /* handle to the JVM */
JPLISEnvironment mNormalEnvironment; /* for every thing but retransform stuff */
JPLISEnvironment mRetransformEnvironment;/* for retransform stuff only */
jobject mInstrumentationImpl; /* handle to the Instrumentation instance */
jmethodID mPremainCaller; /* method on the InstrumentationImpl that does the premain stuff (cached to save lots of lookups) */
jmethodID mAgentmainCaller; /* method on the InstrumentationImpl for agents loaded via attach mechanism */
jmethodID mTransform; /* method on the InstrumentationImpl that does the class file transform */
jboolean mRedefineAvailable; /* cached answer to "does this agent support redefine" */
jboolean mRedefineAdded; /* indicates if can_redefine_classes capability has been added */
jboolean mNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing" */
jboolean mNativeMethodPrefixAdded; /* indicates if can_set_native_method_prefix capability has been added */
char const * mAgentClassName; /* agent class name */
char const * mOptionsString; /* -javaagent options string */
};
struct _JPLISEnvironment {
jvmtiEnv * mJVMTIEnv; /* the JVM TI environment */
JPLISAgent * mAgent; /* corresponding agent */
jboolean mIsRetransformer; /* indicates if special environment */
};
複製程式碼
- mNormalEnvironment:agent環境;
- mRetransformEnvironment:retransform環境;
- mInstrumentationImpl:sun自己提供的instrument物件;
- mPremainCaller:
sun.instrument.InstrumentationImpl.loadClassAndCallPremain
方法,agent啟動時載入會被呼叫該方法;- mAgentmainCaller:
sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain
方法,agent attach動態載入agent的時會被呼叫該方法;- mTransform:
sun.instrument.InstrumentationImpl.transform
方法;- mAgentClassName:javaagent的MANIFEST.MF裡指定的
Agent-Class
;- mOptionsString:agent初始引數;
- mRedefineAvailable:MANIFEST.MF裡的引數
Can-Redefine-Classes:true
;- mNativeMethodPrefixAvailable:MANIFEST.MF裡的引數
Can-Set-Native-Method-Prefix:true
;- mIsRetransformer:MANIFEST.MF裡的引數
Can-Retransform-Classes:true
;
在startJavaAgent的方法中呼叫了啟動JPLISAgent的方式,我們來看invokeJavaAgentMainMethod
:
jboolean invokeJavaAgentMainMethod(JNIEnv * jnienv,
jobject instrumentationImpl,
jmethodID mainCallingMethod,
jstring className,
jstring optionsString) {
jboolean errorOutstanding = JNI_FALSE;
jplis_assert(mainCallingMethod != NULL);
if (mainCallingMethod != NULL ) {
(*jnienv)->CallVoidMethod(jnienv,
instrumentationImpl,
mainCallingMethod,
className,
optionsString);
errorOutstanding = checkForThrowable(jnienv);
if ( errorOutstanding ) {
logThrowable(jnienv);
}
checkForAndClearThrowable(jnienv);
}
return !errorOutstanding;
}
複製程式碼
在函式裡,實際上是呼叫java類sun.instrument.InstrumentationImpl 類裡的方法loadClassAndCallPremain。
private void loadClassAndCallPremain(String var1, String var2) throws Throwable {
this.loadClassAndStartAgent(var1, "premain", var2);
}
private void loadClassAndCallAgentmain(String var1, String var2) throws Throwable {
this.loadClassAndStartAgent(var1, "agentmain", var2);
}
複製程式碼
繼續檢視Java的sun.instrument.InstrumentationImpl類的方法loadClassAndStartAgent:
private void loadClassAndStartAgent(String classname,
String methodname,
String optionsString) throws Throwable {
...
try {
m = javaAgentClass.getDeclaredMethod( methodname,
new Class<?>[] {
String.class,
java.lang.instrument.Instrumentation.class
}
);
twoArgAgent = true;
} catch (NoSuchMethodException x) {
// remember the NoSuchMethodException
firstExc = x;
}
if (m == null) {
// now try the declared 1-arg method
try {
m = javaAgentClass.getDeclaredMethod(methodname, new Class<?>[] { String.class });
} catch (NoSuchMethodException x) {
// ignore this exception because we'll try
// two arg inheritance next
}
}
if (m == null) {
// now try the inherited 2-arg method
try {
m = javaAgentClass.getMethod( methodname,
new Class<?>[] {
String.class,
java.lang.instrument.Instrumentation.class
}
);
twoArgAgent = true;
} catch (NoSuchMethodException x) {
// ignore this exception because we'll try
// one arg inheritance next
}
}
if (m == null) {
// finally try the inherited 1-arg method
try {
m = javaAgentClass.getMethod(methodname, new Class<?>[] { String.class });
} catch (NoSuchMethodException x) {
// none of the methods exists so we throw the
// first NoSuchMethodException as per 5.0
throw firstExc;
}
}
// the premain method should not be required to be public,
// make it accessible so we can call it
// Note: The spec says the following:
// The agent class must implement a public static premain method...
setAccessible(m, true);
// invoke the 1 or 2-arg method
if (twoArgAgent) {
m.invoke(null, new Object[] { optionsString, this });
} else {
m.invoke(null, new Object[] { optionsString });
}
// don't let others access a non-public premain method
setAccessible(m, false);
}
複製程式碼
在InstrumentationImpl的類中初始化了我們自定義的Transformer的premain方法:
public class MyInjectTransformer implements ClassFileTransformer{
public static void premain(String options, Instrumentation ins) {
ins.addTransformer(new SQLInjectTransformer());
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
return null;
}
}
複製程式碼
6 工作原理及執行時序圖
6.1 啟動並建立JVM,註冊vmInit回撥函式
6.2 執行vmInit回撥函式,註冊jvmtiEventClassFileLoadHook回撥函式,載入並初始化 Instrument Agent
6.3 載入解析Class檔案,執行jvmtiEventClassFileLoadHook回撥函式
6.4 以-javaagent為例,工作原理
- 在JVM啟動時,通過JVM引數-javaagent,傳入agent jar,Instrument Agent被載入;
- 在Instrument Agent 初始化時,註冊了JVMTI初始化函式eventHandlerVMinit;
- 在JVM啟動時,會呼叫初始化函式eventHandlerVMinit,啟動了Instrument Agent,用sun.instrument.instrumentationImpl類裡的方法loadClassAndCallPremain方法去初始化Premain-Class指定類的premain方法;
- 初始化函式eventHandlerVMinit,註冊了class解析的ClassFileLoadHook函式;
- 在解析Class之前,JVM呼叫JVMTI的ClassFileLoadHook函式,鉤子函式呼叫sun.instrument.instrumentationImpl類裡的transform方法,通過TransformerManager的transformer方法最終呼叫我們自定義的Transformer類的transform方法;
- 因為位元組碼在解析Class之前改的,直接使用修改後的位元組碼的資料流替代,最後進入Class解析,對整個Class解析無影響;
- 重新載入Class依然重新走5-6步驟;