[Java] JVMTIを使ってJVMエージェントを作ってみる その3

2018年2月19日月曜日

C Java JVMTI

ブレークポイントを設定する

JVMTIを使ってJVMエージェントを作ってみる その1JVMTIを使ってJVMエージェントを作ってみる その2に引き続き、今度はmainメソッドの先頭にブレークポイントを設定して、その時点のスレッドを表示してみよう。

ポイントはClassPrepareの中でSetBreakpointを使って、Breakpoint関数をmainメソッドの先頭(インデックス:0)のイベントハンドラとして登録しているところ。

ブレークポイントを設定するソースコード

jvmtitest.c
#include <stdio.h>
#include <string.h>
#include <jvmti.h>

static jvmtiEnv *jvmti = 0;

static void JNICALL ClassPrepare(jvmtiEnv *jvmti_env, JNIEnv *jni_env,
                                 jthread thread, jclass klass);
static void JNICALL Breakpoint(jvmtiEnv *jvmti_env, JNIEnv *jni_env,
                               jthread thread, jmethodID method,
                               jlocation location);
static jstring getClassName(JNIEnv *env, jclass klass);

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
    (*vm)->GetEnv(vm, (void**)&jvmti, JVMTI_VERSION);

    jvmtiCapabilities capabilities = { 0 };
    capabilities.can_generate_breakpoint_events = 1;
    (*jvmti)->AddCapabilities(jvmti, &capabilities);

    jvmtiEventCallbacks callbackTable = { 0 };
    callbackTable.ClassPrepare = ClassPrepare;
    callbackTable.Breakpoint = Breakpoint;

    (*jvmti)->SetEventCallbacks(jvmti, &callbackTable, sizeof(callbackTable));

    (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
        JVMTI_EVENT_CLASS_PREPARE, NULL);
    (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
        JVMTI_EVENT_BREAKPOINT, NULL);
    
    printf("agent is loading...\n");
    return 0;
}

JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) {
    (*jvmti)->DisposeEnvironment(jvmti);
    printf("agent is unloading...\n");
}

static void JNICALL ClassPrepare(jvmtiEnv *jvmti_env, JNIEnv *jni_env,
                                 jthread thread, jclass klass) {
    jstring strObj = getClassName(jni_env, klass);
    const char *str = (*jni_env)->GetStringUTFChars(jni_env, strObj, NULL);

    if (strcmp(str, "local.Main") == 0) {
        printf("local.Main prepared.\n");
        jmethodID mid = (*jni_env)->GetStaticMethodID(jni_env, klass,
                            "main", "([Ljava/lang/String;)V");


        (*jvmti_env)->SetBreakpoint(jvmti_env, mid, 0);
    }

    (*jni_env)->ReleaseStringUTFChars(jni_env, strObj, str);
}

static void JNICALL Breakpoint(jvmtiEnv *jvmti_env, JNIEnv *jni_env,
                               jthread thread, jmethodID method,
                               jlocation location) {
    printf("breakpoint.\n");

    jint tc;
    jthread *threads;
    (*jvmti_env)->GetAllThreads(jvmti_env, &tc, &threads);

    for (int i = 0; i < tc; i++) {
        jvmtiThreadInfo info;
        jthread thd = threads[i];
        (*jvmti_env)->GetThreadInfo(jvmti_env, thd, &info);
        
        printf("%s:%d\n", info.name, info.is_daemon);

        (*jvmti_env)->Deallocate(jvmti_env, (void*)info.name);
        (*jni_env)->DeleteLocalRef(jni_env, thd);
    }
    (*jvmti_env)->Deallocate(jvmti_env, (void*)threads);
}

static jstring getClassName(JNIEnv *env, jclass klass) {
    jclass cls = (*env)->GetObjectClass(env, klass);
    jmethodID mid =
         (*env)->GetMethodID(env, cls, "getName", "()Ljava/lang/String;"); 
    jstring strObj = (jstring)(*env)->CallObjectMethod(env, klass, mid);

    return strObj;
}

Breakpoint関数ではGetAllThreadsを使って、JVM中の全てのスレッドを取得して、各スレッドの情報をGetThreadInfoを使って取得している。

コンパイル&実行

これをコンパイルして、実行すると以下になる。
※local/Main.javaの内容はを参照。

$ gcc -Wall -std=c99 -shared -fPIC -D_REENTRANT -o libjvmtitest.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux jvmtitest.c
$ java -agentpath:./libjvmtitest.so local.Main
agent is loading...
local.Main prepared.
breakpoint.
Signal Dispatcher:1
Finalizer:1
Reference Handler:1
main:0
Hello, JVMTI in Java.
agent is unloading...


mainメソッド呼び出し時にすでにmain以外に"Signal Dispatcher","Finalizer","Reference Handler"という3つのデーモンスレッドが起動していることがわかる。