JVMTI那些事——c++编写的agent

简介

前文介绍过JVMTI的基本知识,事实上JVMTI本身就是JVM通过c语言对外提供的一组接口。
tools.jar中提供的插装接口,最终是通过libinstrument.so这个动态链接库调用JVMTI接口实现的。
因此本文通过一个简单的示例,实现一个监控JVM异常并打印出来的agent。

代码结构

使用JVMTI接口需要依赖jvmti.hjni.hjni_md.h这几个头文件,当然使用的时候只需要include jvmti.h这一个头文件即可。

为了方便管理依赖,这里使用cmake进行依赖管理(实际上只是查找了下jni相关的东西):

#CMakeLists.txt
cmake_minimum_required(VERSION 2.6)
project(javaagent)

find_package(JNI REQUIRED)

include_directories(${JAVA_INCLUDE_PATH} ${JAVA_INCLUDE_PATH2})

add_library(javaagent SHARED main.cpp exception.cpp)

在cmake提供的FindJNI.cmake文件中,会设置两个头文件相关的环境变量:JAVA_INCLUDE_PATHJAVA_INCLUDE_PATH2
刚好包含前面提到的头文件以及平台相关头文件所在目录。

入口

JVMTI文档中介绍过,agent有2个入口函数,分别是启动时和挂载时。这个示例只实现了随JVM启动功能,因此只实现了启动时的入口函数。

#include <jvmti.h>

#include "exception.h"

ExceptionMonitor* monitor;

JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
{
    monitor = new ExceptionMonitor(vm);
    return JNI_OK;
}

JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm)
{
    delete monitor;
}

Agent_OnLoad函数会在agent被加载入JVM的时候执行,此时我们就能够获取到jvmti相关上下文。这里简单的将这个上下文传入给
ExceptionMonitor类,并初始化。

JVMTI接口调用

所有JVMTI函数都依赖jvmti上下文,因此需要保存从入口函数传入的jvmti上下文信息。

“`c++
#ifndef EXCEPTION_MONITOR_H
#define EXCEPTION_MONITOR_H

#include <jvmti.h>

/**
* 插装
*/
class ExceptionMonitor
{
public:
ExceptionMonitor(JavaVM *vm);

private:
void dealError(jvmtiError err);

private:
jvmtiEnv *jvmti;
};

#endif // EXCEPTION_MONITOR_H

头文件定义了类的定义,因为我们的功能依赖JVMTI的事件回调,因此不需要提供对外接口,只需要在构造函数中完成初始化即可。私有字段只有一个,既jvmti的上下文`jvmtiEnv`指针。

实现也比较简单,实现一个JVMTI的agent,主要有3步:

1. 获取jvmtiEnv对象
2. 添加对应的capability
3. 调用对应的函数

对于事件回调,需要再多一个过程,就是注册回调函数并开启事件监听。

这些流程都在构造函数中一次性完成了:
```c++
ExceptionMonitor::ExceptionMonitor(JavaVM* vm)
{
    jint ret = vm->GetEnv(reinterpret_cast<void**>(&jvmti), JVMTI_VERSION_1_2);

    if(ret != JNI_OK) {
        throw std::invalid_argument("init jvmti env fail");
    }

    // capbilities
    jvmtiCapabilities caps;
    std::memset(&caps, 0, sizeof(caps));
    caps.can_generate_exception_events = 1;
    auto err = jvmti->AddCapabilities(&caps);
    if(err != JVMTI_ERROR_NONE) {
        dealError(err);
        throw std::runtime_error("fail to add can_generate_exception_events capability");
    }

    // callback
    jvmtiEventCallbacks cb;
    cb.Exception = &deal_exception;

    jvmti->SetEventCallbacks(&cb, sizeof(cb));
    //enable
    jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION,NULL);
}
</code></pre>

首先通过传入的<code>JavaVM</code>对象,获取<code>jvmtiEnv</code>对象。然后设置capability,最后设置回调,并启用对应的事件。

对应的回调函数:

```c++
void JNICALL
deal_exception(jvmtiEnv jvmti_env,
            JNIEnv jni_env,
            jthread thread,
            jmethodID method,
            jlocation location,
            jobject exception,
            jmethodID catch_method,
            jlocation catch_location)
{
    char *classSign, *classGenericSign;
    auto exceptionClass = jni_env->GetObjectClass(exception);
    jvmti_env->GetClassSignature(exceptionClass, &classSign, &classGenericSign);
    std::cout << "Get exception: " << classSign << "\n";

char *methodName, *methodSign, *methodGenericSign;
jvmti_env->GetMethodName(method, &methodName, &methodSign, &methodGenericSign);
std::cout << " in method: " << methodName << methodSign << "\n";

jvmtiThreadInfo threadInfo;
jvmti_env->GetThreadInfo(thread, &threadInfo);
std::cout << "in Thread: " << threadInfo.name << "\n";

std::cout << std::endl;

jvmti_env->Deallocate(reinterpret_cast<unsigned char*>(classSign));
jvmti_env->Deallocate(reinterpret_cast<unsigned char*>(classGenericSign));
jvmti_env->Deallocate(reinterpret_cast<unsigned char*>(methodName));
jvmti_env->Deallocate(reinterpret_cast<unsigned char*>(methodSign));
jvmti_env->Deallocate(reinterpret_cast<unsigned char*>(methodGenericSign));
jvmti_env->Deallocate(reinterpret_cast<unsigned char*>(threadInfo.name));

}


这里是当JVM出现任何异常时都会进行的回调。首先我们可以通过回调参数`jthread`获取异常产生的线程信息。 通过`jmethodID`获取方法信息,通过`jobject`对象获取实际抛出的异常类型。 前几个数据都有JVMTI函数可以调用,注意最后一个获取类的信息,是通过JNI接口获取的,因此相关函数在JVMTI文档中是查不到的。 ## 运行 运行前首先需要构建出动态链接库,这个用cmake就很方便了,直接安装标准构建即可: ```bash mkdir build cd build cmake .. make

构建完成后在build目录中会有libjavaagent.so文件,这个就是我们需要的动态链接库了。在java启动的时候增加这个参数:

-agentpath:PATH_TO_BUILD_DIR/libjavaagent.so

启动之后就可以从标准输出看见JVM产生的所有异常。例如:

Get exception: Ljava/lang/Exception;
 in method: genException()Ljava/lang/String;
in Thread: http-nio-7001-exec-5

当然,实际JVM会产生超级多异常,可以在打印之前过滤下,或者订阅事件的时候指定线程即可。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.