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

2018年2月13日火曜日

C Java JVMTI

バイトコード表示

JVMTIを使ってJVMエージェントを作ってみる その1ではClassPrepareイベントハンドラを登録するところまでだったけど、今回はmainメソッドのバイトコードを表示するところまでやってみよう。

バイトコード表示にはバイトコードを取得する許可を与える必要があるため、AddCapabilitiesを使って、許可を与える。
    jvmtiCapabilities capabilities = { 0 };
    capabilities.can_get_bytecodes = 1;
    (*jvmti)->AddCapabilities(jvmti, &capabilities);

また、バイトコードはクラスが準備できた後でなければ取得できないため、ClassPrepareの中で、GetBytecodesを使って取得する。

エージェントのソースコード

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 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_get_bytecodes = 1;
    (*jvmti)->AddCapabilities(jvmti, &capabilities);

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

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

    (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
        JVMTI_EVENT_CLASS_PREPARE, 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");

        jint bc;
        unsigned char *bcp;
        (*jvmti_env)->GetBytecodes(jvmti_env, mid, &bc, &bcp);
        for (int i = 0; i < bc; i++) {
            printf("%0x\n", bcp[i]);
        }

        (*jvmti_env)->Deallocate(jvmti_env, bcp);

    }

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

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;
}

コンパイル&実行

これをコンパイルして、実行すると以下になる。
※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.
b2
0
2
12
3
b6
0
4
b1
Hello, JVMTI in Java.
agent is unloading...


"local.Main prepared."と"Hello, JVMTI in Java."の間に数字がずらずらと出力されている箇所がmainメソッドのバイトコード。

GetBytecodes(jvmti_env, mid, &bc, &bcp)呼び出しでbcpにバイトコードの配列、bcに配列の要素数が入る。
bcpはオペコードとオペランドがそのまま並んでいるので、改行を整理するとこんな感じ。
b2 0 2
12 3
b6 0 4
b1

各命令は以下になる。
getstatic 0 2
ldc 3
invokevirtual 0 4
areturn

これはそれぞれ"Hello, JVMTI in Java."のオペランドスタックへのロードとSystem.out.printlnの呼び出し、メソッドからの復帰に相当している。

Java Virtual Machine Specification
Chapter 6. The Java Virtual Machine Instruction Set
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html

Java仮想マシン#命令セット仕様
https://ja.wikipedia.org/wiki/Java%E4%BB%AE%E6%83%B3%E3%83%9E%E3%82%B7%E3%83%B3#%E5%91%BD%E4%BB%A4%E3%82%BB%E3%83%83%E3%83%88%E4%BB%95%E6%A7%98