基於JVMTI的Agent實現

猿碼道發表於2019-03-04

0 前言

上一節《淺談JPDA中JVMTI模組》講解了JVMTI功能作用,本節我們將通過一個具體的例子,來闡述如何開發一個簡單的 Agent 。Agent主要功能描述

通過 C++ 編寫,監聽 JVMTI_EVENT_METHOD_ENTRY 事件,註冊對應的回撥函式來響應這個事件,來輸出所有被呼叫函式名;

1 Agent 設計實現

具體實現都在 MethodTraceAgent 這個類裡提供。按照順序,它會處理環境初始化、引數解析、註冊功能、註冊事件響應,每個功能都被抽象在一個具體的函式裡。

MethodTraceAgent.h 程式碼如下

#include "jvmti.h"

class AgentException 
{
 public:
	AgentException(jvmtiError err) {
		m_error = err;
	}

	char* what() const throw() { 
		return "AgentException"; 
	}

	jvmtiError ErrCode() const throw() {
		return m_error;
	}

 private:
	jvmtiError m_error;
};


class MethodTraceAgent 
{
 public:

	MethodTraceAgent() throw(AgentException){}

	~MethodTraceAgent() throw(AgentException);

	void Init(JavaVM *vm) const throw(AgentException);
        
	void ParseOptions(const char* str) const throw(AgentException);

	void AddCapability() const throw(AgentException);
        
	void RegisterEvent() const throw(AgentException);
    
	static void JNICALL HandleMethodEntry(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread, jmethodID method);

 private:
	static void CheckException(jvmtiError error) throw(AgentException)
	{
		// 可以根據錯誤型別擴充套件對應的異常,這裡只做簡單處理
		if (error != JVMTI_ERROR_NONE) {
			throw AgentException(error);
		}
	}
    
	static jvmtiEnv * m_jvmti;
	static char* m_filter;
};
複製程式碼

MethodTraceAgent.cpp 程式碼如下

#include <iostream>

#include "MethodTraceAgent.h"
#include "jvmti.h"

using namespace std;

jvmtiEnv* MethodTraceAgent::m_jvmti = 0;
char* MethodTraceAgent::m_filter = 0;

MethodTraceAgent::~MethodTraceAgent() throw(AgentException)
{
    // 必須釋放記憶體,防止記憶體洩露
    m_jvmti->Deallocate(reinterpret_cast<unsigned char*>(m_filter));
}

void MethodTraceAgent::Init(JavaVM *vm) const throw(AgentException){
    jvmtiEnv *jvmti = 0;
	jint ret = (vm)->GetEnv(reinterpret_cast<void**>(&jvmti), JVMTI_VERSION_1_0);
	if (ret != JNI_OK || jvmti == 0) {
		throw AgentException(JVMTI_ERROR_INTERNAL);
	}
	m_jvmti = jvmti;
}

void MethodTraceAgent::ParseOptions(const char* str) const throw(AgentException)
{
    if (str == 0)
        return;
	const size_t len = strlen(str);
	if (len == 0) 
		return;

  	// 必須做好記憶體複製工作
	jvmtiError error;
    error = m_jvmti->Allocate(len + 1,reinterpret_cast<unsigned char**>(&m_filter));
	CheckException(error);
    strcpy(m_filter, str);

    // 可以在這裡進行引數解析的工作
	// ...
}

void MethodTraceAgent::AddCapability() const throw(AgentException)
{
    // 建立一個新的環境
    jvmtiCapabilities caps;
    memset(&caps, 0, sizeof(caps));
    caps.can_generate_method_entry_events = 1;
    
    // 設定當前環境
    jvmtiError error = m_jvmti->AddCapabilities(&caps);
	CheckException(error);
}
  
void MethodTraceAgent::RegisterEvent() const throw(AgentException)
{
    // 建立一個新的回撥函式
    jvmtiEventCallbacks callbacks;
    memset(&callbacks, 0, sizeof(callbacks));
    callbacks.MethodEntry = &MethodTraceAgent::HandleMethodEntry;
    
    // 設定回撥函式
    jvmtiError error;
    error = m_jvmti->SetEventCallbacks(&callbacks, static_cast<jint>(sizeof(callbacks)));
	CheckException(error);

	// 開啟事件監聽
	error = m_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, 0);
	CheckException(error);
}

void JNICALL MethodTraceAgent::HandleMethodEntry(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread, jmethodID method)
{
	try {
        jvmtiError error;
        jclass clazz;
        char* name;
		char* signature;
        
		// 獲得方法對應的類
        error = m_jvmti->GetMethodDeclaringClass(method, &clazz);
        CheckException(error);
        // 獲得類的簽名
        error = m_jvmti->GetClassSignature(clazz, &signature, 0);
        CheckException(error);
        // 獲得方法名字
        error = m_jvmti->GetMethodName(method, &name, NULL, NULL);
        CheckException(error);
        
        // 根據引數過濾不必要的方法
		if(m_filter != 0){
			if (strcmp(m_filter, name) != 0)
				return;
		}			
		cout << signature<< " -> " << name << "(..)"<< endl;

        // 必須釋放記憶體,避免記憶體洩露
        error = m_jvmti->Deallocate(reinterpret_cast<unsigned char*>(name));
		CheckException(error);
        error = m_jvmti->Deallocate(reinterpret_cast<unsigned char*>(signature));
		CheckException(error);

	} catch (AgentException& e) {
		cout << "Error when enter HandleMethodEntry: " << e.what() << " [" << e.ErrCode() << "]";
    }
}
複製程式碼

Agent_OnLoad 函式 會在 Agent 被載入的時候建立這個類,並依次呼叫上述各個方法,從而實現這個 Agent 的功能。Agent.cpp 程式碼如下:

#include <iostream>

#include "MethodTraceAgent.h"
#include "jvmti.h"

using namespace std;

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
{
    cout << "Agent_OnLoad(" << vm << ")" << endl;
    try{
        
        MethodTraceAgent* agent = new MethodTraceAgent();
		agent->Init(vm);
        agent->ParseOptions(options);
        agent->AddCapability();
        agent->RegisterEvent();
        
    } catch (AgentException& e) {
        cout << "Error when enter HandleMethodEntry: " << e.what() << " [" << e.ErrCode() << "]";
		return JNI_ERR;
	}
    
	return JNI_OK;
}

JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm)
{
    cout << "Agent_OnUnload(" << vm << ")" << endl;
}
複製程式碼

Agent執行過程時序圖,如圖所示:

Agent 時序圖

2 Agent 編譯執行

Agent 編譯非常簡單,和編譯普通的動態連結庫沒有本質區別,只是需要將 JDK 提供的一些標頭檔案包含進來

Windows:

cl /EHsc -I${JAVA_HOME}include -I${JAVA_HOME}includewin32 
-LD MethodTraceAgent.cpp Main.cpp -FeAgent.dll
複製程式碼

Linux:

g++ -I${JAVA_HOME}/include/ -I${JAVA_HOME}/include/linux 
MethodTraceAgent.cpp Agent.cpp -fPIC -shared -o libagent.so
複製程式碼

提供了一個可執行的 Java 類 MethodTraceTest.java,程式碼如下:

public class MethodTraceTest {

	public static void main(String[] args){
		MethodTraceTest test = new MethodTraceTest();
		test.first();
		test.second();
	}
	
	public void first(){
		System.out.println("=> Call first()");
	}
	
	public void second(){
		System.out.println("=> Call second()");
	}
}
複製程式碼

預設執行後輸出,結果如下:

無Agent預設輸出

現在,執行程式前告訴 Java 先載入編譯出來的 Agent:

java -agentlib:Agent=first MethodTraceTest
複製程式碼

帶有Agent執行後輸出,結果如下:

帶有Agent後輸出

當程式執行到到 MethodTraceTest 的 first 方法時,Agent 會輸出這個事件。“ first ”是 Agent 執行的引數,如果不指定話,所有的進入方法的觸發的事件都會被輸出,如果把這個引數去掉再執行的話,會發現在執行 main 函式前,已經有非常基本的類庫函式被呼叫了

相關文章