如何随心所欲调试HotSpot VM源代码?(改造为CMakeLists项目)

如何,随心所欲,调试,hotspot,vm,源代码,改造,cmakelists,项目 · 浏览次数 : 191

小编点评

**Code Analysis** The provided code contains the following key aspects: * PlatformStringMID variable checks for the existence of a platform string. * If PlatformStringMID is NULL, it loads a platform string using the method `LoadMainClass`. * The `LoadMainClass` method loads a Java class and extracts the main class name from its constructor parameter. * The code then loads a Java virtual machine (VM) with the specified class path and main class name. * It sets up Java VM initialization parameters and creates a JavaVM instance. * The `main` method loads the Java VM library and finds the main class class. * It then extracts the main method name and creates an object array with the main method's parameters. * The `main` method calls the `main` method on the main class, passing the prepared object array. **Code Outline** ```cpp #include #include #include #define JAVA_VM_INIT_ARGS_COUNT 65538 static jclass LoadMainClass(JNIEnv *env, int mode, char *name) { // ... } static jobjectArrayNewPlatformStringArray(JNIEnv *env, char **strv, int strc) { // ... } int main() { // ... return 0; } ``` **Comments** * The code uses JNI to load and initialize a Java virtual machine. * It sets up Java VM initialization parameters and creates a JavaVM instance. * The `LoadMainClass` method loads the Java class and extracts the main class name. * The `main` method calls the `main` method on the main class, passing the prepared object array. * The code comments out the dynamic linker configuration for the Java VM library to be compiled separately. **Benefits of the Code** * Provides a detailed implementation of HotSpot VM. * Offers the ability to debug Java applications through a JavaVM instance. * Shows how to load and initialize a Java VM with custom class path and main class name.

正文

常有小伙伴问我是怎么调试HotSpot VM源代码的,我之前通过视频和文章介绍过一种大家都用的调试方法,如下:

文章地址:第1.2篇-调试HotSpot VM源代码(配视频)

视频地址:https://space.bilibili.com/27533329 

网上所有的文章都介绍的是这种方式,先将HotSpot VM编译为动态链接库并生成对应的调试符号文件,然后在IDE中加载启动器这个二进制文件进行调试。不过这种方式对我这种频繁查看和修改HotSpot VM源代码的人来说有一些不方便。主要体现在如下几个方面:

(1)有些函数链接不过去,这个是正常的,因为没有被IDE识别为合法的Makefile项目。另外还有一些其它原因,如HotSpot VM源代码中包含有针对主流操作系统和CPU架构的不同实现,此时的IDE并不知道要跳转到哪个实现;

(2)崩溃的问题,在Ubuntu16.04 x86_64位操作系统上进行调试时,CLion频繁崩溃,Eclipse有时也会崩溃,无语,Visual Studio Code没有经常用,不知道。

第一个问题促使我下决心将HotSpot VM这个Makefile项目改为CMakeLists项目,因为CLion在我改造那时候还不支持创建Makefile项目,对CMakeLists项目支持的较好。

第二个问题在将CMakeLists项目改造完成后,突然有一次调试如下一行代码时遇到卡顿问题:

源代码来源:openjdk/hotspot/src/os/linux/os_linux.cpp
// 函数anon_mmap()在为堆分配内存时会调用
addr = (char*)::mmap(requested_addr, bytes,  PROT_NONE, flags, -1, 0);

调用函数mmap()为堆分配内存时,传递了一个参数PROT_NONE,这个表示映射的保护级别,PROT_NONE表示该映射不能被访问。所以如果在调试模式下,即使读取地址也会卡死,不过有些情况下会崩溃。我们将这个参数改为PROT_READ|PROT_WRITE(可读可写)即可。

我怀疑在CLion和Eclipse上崩溃也和这个有很大关系,不过我后来并没有试过原来的那种调试方式。

下面将HotSpot VM项目更改为一个合法的、能被CLion识别的CMakeLists项目,CLion识别后就不会有源代码报红的情况,也不会出现链接不过去的情况,如果有,那在CLion上是无法编译出虚拟机的动态链接库的。

1、按常见方式编译出OpenJDK

具体的编译可以参考我之前录制的视频和写的文章,如下:

第1.1篇-在Ubuntu 16.04上编译OpenJDK8的源代码(配视频)

编译时可参考官方文档:openjdk/README-builds.html

需要说明的是,要想启动Java应用程序,除了HotSpot VM外,还要有JDK类库以及一系列的、针对特定CPU和操作系统编译出的动态链接库,这些动态链接库大部分都是native方法的实现。由于我只编译HotSpot VM为动态链接库,所以还需要按之前的方式将除libjvm.so外的其它运行时环境准备好。我们自己编译libjvm.so并替换掉之前编译好的libjvm.so即可。

2、调整HotSpot VM源代码目录

            

左侧是我调整后的源代码目录,右侧为HotSpot VM调整前的目录结构。因为我只研究HotSpot VM在Linux下的x86_64位实现,所以删除了其它平台和CPU架构下的实现,只保留了linux、linux_x86和x86目录,并将所有的源代码都放在了src目录下。目录怎么调整无所谓,不过需要将其中每个源文件的引用路径都更正一遍才行。

原share目录中存储着共同的代码,如果要在共同代码中需要引入特定CPU架构和操作系统的实现时,可通过如下宏来实现: 

源代码位置:openjdk/hotspot/src/share/vm/runtime/os.hpp

#ifdef TARGET_OS_FAMILY_linux
# include "os_linux.hpp"
# include "os_posix.hpp"
#endif
#ifdef TARGET_OS_FAMILY_solaris
# include "os_solaris.hpp"
# include "os_posix.hpp"
#endif
#ifdef TARGET_OS_FAMILY_windows
# include "os_windows.hpp"
#endif
#ifdef TARGET_OS_FAMILY_bsd
# include "os_posix.hpp"
# include "os_bsd.hpp"
#endif

遇到类似如上的代码,可直接删除宏判断,保留特定的文件引用即可。如:

# include "os_linux.hpp"
# include "os_posix.hpp"

在share目录中的代码还有许多使用宏来选择编译特定的代码片段,如下:

源代码位置:openjdk/hotspot/src/share/interpreter/interpreterRuntime.cpp
#if defined(IA32) || defined(AMD64) || defined(ARM)
// 相关的实现
#endif

可以选择删除宏,保留特定的代码片段,不过由于这样的宏太多,所以这可以直接在CMakeLists.txt文件中定义相关的宏即可,如下:

add_definitions(-DAMD64 -D_LP64 -DCOMPILER1 -DCOMPILER2 -DINCLUDE_ALL_GCS -DASSERT -DVM_LITTLE_ENDIAN -D_GNU_SOURCE -DLINUX -DINCLUDE_JVMTI=1)

根据宏来选择对应的代码。 

另外,如果某些文件缺失,需要从之前编译好的目录下搜索出对应的文件,然后放到对应目录中即可。 

3、编写CMakeLists文件内容

具体内容如下:

cmake_minimum_required(VERSION 3.15)
project(jvm)

enable_language(C ASM)

set(CMAKE_C_STANDARD 99)
set(CMAKE_CXX_STANDARD 98)

add_compile_options(-fpermissive)
# 用到了操作系统线程,编译时需要加参数-pthread
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread")
# 将汇编文件和C++源代码一起编译
SET(ASM_OPTIONS "-x assembler-with-cpp")
SET(CMAKE_ASM_FLAGS "${CFLAGS} ${ASM_OPTIONS}")

# 针对操作系统和CPU架构定义了一些宏
add_definitions(-DAMD64 -D_LP64 -DCOMPILER1 -DCOMPILER2 -DINCLUDE_ALL_GCS -DASSERT -DVM_LITTLE_ENDIAN -D_GNU_SOURCE -DLINUX -DINCLUDE_JVMTI=1)

# 将编译出的动态链接库libjvm.so替换之前编译出的libjvm.so动态链接库
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY /media/mazhi/system2-ssd/openjdks/updated/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server)

aux_source_directory(./src/asm SOURCE_FILES)
aux_source_directory(./src/c1 SOURCE_FILES)
aux_source_directory(./src/ci SOURCE_FILES)
aux_source_directory(./src/classfile SOURCE_FILES)
aux_source_directory(./src/code SOURCE_FILES)
aux_source_directory(./src/compiler SOURCE_FILES)
aux_source_directory(./src/gc_implementation SOURCE_FILES)
aux_source_directory(./src/gc_implementation/concurrentMarkSweep SOURCE_FILES)
aux_source_directory(./src/gc_implementation/g1 SOURCE_FILES)
aux_source_directory(./src/gc_implementation/parallelScavenge SOURCE_FILES)
aux_source_directory(./src/gc_implementation/parNew SOURCE_FILES)
aux_source_directory(./src/gc_implementation/shared SOURCE_FILES)
aux_source_directory(./src/gc_interface SOURCE_FILES)
aux_source_directory(./src/interpreter SOURCE_FILES)
aux_source_directory(./src/libadt SOURCE_FILES)
aux_source_directory(./src/linux SOURCE_FILES)
aux_source_directory(./src/linux_x86 SOURCE_FILES)
aux_source_directory(./src/memory SOURCE_FILES)
aux_source_directory(./src/oops SOURCE_FILES)
aux_source_directory(./src/opto SOURCE_FILES)
aux_source_directory(./src/posix SOURCE_FILES)
aux_source_directory(./src/precompiled SOURCE_FILES)
aux_source_directory(./src/prims SOURCE_FILES)
aux_source_directory(./src/prims/wbtestmethods SOURCE_FILES)
aux_source_directory(./src/runtime SOURCE_FILES)
aux_source_directory(./src/services SOURCE_FILES)
aux_source_directory(./src/trace SOURCE_FILES)
aux_source_directory(./src/utilities SOURCE_FILES)
aux_source_directory(./src/x86 SOURCE_FILES)
aux_source_directory(./src/tracefiles SOURCE_FILES)
aux_source_directory(./src/adfiles SOURCE_FILES)

add_library(${PROJECT_NAME} SHARED  ${SOURCE_FILES} ./src/linux_x86/linux_x86_64.s)

将以.s结尾的汇编文件和.cpp源代码一起编译,最终会将编译出的libjvm.so动态链接库放到指定的目录下,替换之前编译出的libjvm.so文件。  

4、编写虚拟机启动逻辑

HotSpot VM的启动逻辑在之前也有介绍过,如下:

第1.4篇-HotSpot VM的启动过程(配视频进行源码分析)

不过因为要考虑跨平台兼容以及用户输入等一系列因素,所以这个启动逻辑太繁琐,我们直接在CMakeLists项目中创建一个main.cpp文件,简化这个启动逻辑,如下:

#include <iostream>

#include "src/prims/jni.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include "dlfcn.h"

#include "src/include/jni.h"

typedef jint (JNICALL *CreateJavaVM_t)(JavaVM **pvm, void **env, void *args);
typedef jint (JNICALL *GetDefaultJavaVMInitArgs_t)(void *args);
typedef jint (JNICALL *GetCreatedJavaVMs_t)(JavaVM **vmBuf, jsize bufLen, jsize *nVMs);

typedef struct {
    CreateJavaVM_t CreateJavaVM;
    GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs;
    GetCreatedJavaVMs_t GetCreatedJavaVMs;
} InvocationFunctions;

typedef jclass (JNICALL FindClassFromBootLoader_t(JNIEnv *env,
                                                  const char *name));
static FindClassFromBootLoader_t *findBootClass = NULL;

jclass FindBootStrapClass(JNIEnv *env, const char* classname){
    if (findBootClass == NULL) {
        findBootClass = (FindClassFromBootLoader_t *)dlsym(RTLD_DEFAULT,"JVM_FindClassFromBootLoader");
        if (findBootClass == NULL) {
            return NULL;
        }
    }
    return findBootClass(env, classname);
}


jboolean
LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn){
    void *libjvm;

    // dlopen() 函数以指定模式打开指定的动态链接库文件
    libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL);
    if (libjvm == NULL) {
        std::cout << ::dlerror() << std::endl;
        return JNI_FALSE;
    }

    // dlsym() 函数在动态链接库中查找指定的符号,并返回符号对应的地址
    ifn->CreateJavaVM = (CreateJavaVM_t)
            dlsym(libjvm, "JNI_CreateJavaVM");
    if (ifn->CreateJavaVM == NULL) {
        return JNI_FALSE;
    }

    ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)
            dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs");
    if (ifn->GetDefaultJavaVMInitArgs == NULL) {
        return JNI_FALSE;
    }

    ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t)
            dlsym(libjvm, "JNI_GetCreatedJavaVMs");
    if (ifn->GetCreatedJavaVMs == NULL) {
        return JNI_FALSE;
    }

}
static jclass helperClass = NULL;

jclass GetLauncherHelperClass(JNIEnv *env){
    if (helperClass == NULL) {
        helperClass = FindBootStrapClass(env,"sun/launcher/LauncherHelper");
    }
    return helperClass;
}

static jclass GetApplicationClass(JNIEnv *env){
    jmethodID mid;
    jobject result;
    jclass cls = GetLauncherHelperClass(env);
    mid = env->GetStaticMethodID(cls,"getApplicationClass","()Ljava/lang/Class;");

    return static_cast<jclass>(env->CallStaticObjectMethod(cls, mid));
}

static jmethodID makePlatformStringMID = NULL;
static jstring NewPlatformString(JNIEnv *env, char *s)
{
    int len = (int)strlen(s);
    jbyteArray ary;
    jclass cls = GetLauncherHelperClass(env);
    if (s == NULL){
        return 0;
    }

    ary = (env)->NewByteArray(len);
    if (ary != 0) {
        jstring str = 0;
        (env)->SetByteArrayRegion(ary, 0, len, (jbyte *)s);
        if (!(env)->ExceptionOccurred()) {
            if (makePlatformStringMID == NULL) {
                makePlatformStringMID = (env)->GetStaticMethodID(cls, "makePlatformString", "(Z[B)Ljava/lang/String;");
            }
            str = static_cast<jstring>((env)->CallStaticObjectMethod(cls, makePlatformStringMID, JNI_TRUE, ary));
            (env)->DeleteLocalRef(ary);
            return str;
        }
    }
    return 0;
}

static jclass LoadMainClass(JNIEnv *env, int mode, char *name){
    jmethodID  mid;
    jstring    str;
    jobject    result;
    jlong      start, end;
    jclass     cls ;
    cls = GetLauncherHelperClass(env);
    mid = (env)->GetStaticMethodID(cls,"checkAndLoadMain","(ZILjava/lang/String;)Ljava/lang/Class;");

    str = NewPlatformString(env, name); // 这里的name为主类的名称,如com.test/Test
    result = env->CallStaticObjectMethod(cls, mid, JNI_TRUE, mode, str);

    return (jclass)result;
}

jobjectArray
NewPlatformStringArray(JNIEnv *env, char **strv, int strc)
{
    jclass cls;
    jobjectArray ary;
    int i;

    cls = FindBootStrapClass(env, "java/lang/String");
    ary = (env)->NewObjectArray( strc, cls, 0);
    for (i = 0; i < strc; i++) {
        jstring str = NewPlatformString(env, *strv++);
        (env)->SetObjectArrayElement(ary, i, str);
        (env)->DeleteLocalRef(str);
    }
    return ary;
}

int main() {
    int count = 5;
    JavaVMOption *options = (JavaVMOption *)malloc( count * sizeof(JavaVMOption));

    int numOptions = 0;
    options[numOptions].optionString =  "-Djava.class.path=.";
    options[numOptions++].extraInfo = NULL;   

    options[numOptions].optionString =  "-Djava.class.path=.:/media/mazhi/sourcecode/workspace/projectjava/projectjava01/target/mazhimazh-0.0.1-SNAPSHOT-jar-with-dependencies.jar";
    options[numOptions++].extraInfo = NULL;

    options[numOptions].optionString = "-Dsun.java.command=com.test/TestInlineMethod";
    options[numOptions++].extraInfo = NULL;

    options[numOptions].optionString =  "-Dsun.java.launcher=SUN_STANDARD";
    options[numOptions++].extraInfo = NULL;

    char *substr = "-Dsun.java.launcher.pid=";
    char *pid_prop_str = (char *)malloc(strlen(substr) + 10 + 1);
    sprintf(pid_prop_str, "%s%d", substr, getpid());
    options[numOptions].optionString = substr;
    options[numOptions++].extraInfo = NULL;

    // 为启动虚拟机传递的参数
    JavaVMInitArgs  args = {
            65538,
            count,
            options,
            true
    };
    JavaVM *vm = 0;
    JNIEnv *env = 0;

    InvocationFunctions ifn;
    ifn.CreateJavaVM = 0;
    ifn.GetDefaultJavaVMInitArgs = 0;

    // 加载动态链接库并查找相关的符号
    char *jvmpath = "/media/mazhi/system2-ssd/openjdks/updated/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so";
    LoadJavaVM(jvmpath,&ifn);

    // 创建一个虚拟机实例,目录不能以直接调用的方式启动虚拟机HotSpot
//    jint r = JNI_CreateJavaVM(&vm, (void **)&env, &args);
    jint r = ifn.CreateJavaVM(&vm, (void **)&env, &args);
    free(options);
    if(r == JNI_OK){
        printf("success");
    }

    // 查找Java主类
    char* what = "com.test/TestInlineMethod";
    jclass mainClass = LoadMainClass(env, 1, what);

    // 找到Java主类main()方法对应的唯一ID
    jmethodID mainID = env->GetStaticMethodID(mainClass, "main", "([Ljava/lang/String;)V");

    // 为应用程序传递的参数
    jobjectArray mainArgs = NewPlatformStringArray(env, 0, NULL);

    // 调用Java的main()方法
    env->CallStaticVoidMethod(mainClass, mainID, mainArgs);

    return 0;
}

由于我们现在还不能在main()中直接调用HotSpot VM源代码函数的方式启动,所以在编译好了libjvm.so库后,在CMakeLists.txt文件中注释掉编译动态链接库的逻辑(注释掉aux_source_directory和add_library即可),加上编译可执行程序的逻辑即可,如下:

add_executable(${PROJECT_NAME} main.cpp)
target_link_libraries(${PROJECT_NAME} dl pthread)

运行main()函数即可开启断点调试。  

如有对虚拟机感兴趣的,可扫码群,加过虚拟机群的就不要再加入了。

本人最近准备出一个手写Hotspot VM的课程,超级硬核,从0开始写HotSpot VM,将HotSpot VM所有核心的实现全部走一遍,如感兴趣,速速入群。

 

 

  

 

与如何随心所欲调试HotSpot VM源代码?(改造为CMakeLists项目)相似的内容:

如何随心所欲调试HotSpot VM源代码?(改造为CMakeLists项目)

常有小伙伴问我是怎么调试HotSpot VM源代码的,我之前通过视频和文章介绍过一种大家都用的调试方法,如下: 文章地址:第1.2篇-调试HotSpot VM源代码(配视频) 视频地址:https://space.bilibili.com/27533329 网上所有的文章都介绍的是这种方式,先将Ho

如何在现实场景中随心放置AR虚拟对象?

随着AR的发展和电子设备的普及,人们在生活中使用AR技术的门槛降低,比如对于不方便测量的物体使用AR测量,方便又准确;遇到陌生的路段使用AR导航,清楚又便捷;网购时拿不准的物品使用AR购物,体验更逼真。 想要让虚拟物体和现实世界相融合,重要的一步就是将虚拟对象准确放置在现实场景中,当用户触摸电子屏幕

如何优雅地写注释:找到代码注释的黄金平衡点

优雅的注释是一种平衡艺术,它要求我们在不牺牲代码清晰度的前提下,避免过度注释。通过遵循上述原则和技巧,我们可以写出既有助于自己,也有助于他人的注释,从而提升代码的整体质量和可维护性。

如何解决 CentOS 7 官方 yum 仓库无法使用的问题

一、背景介绍 2024 年 7 月 1 日,在编译基于 CentOS 7.6.1810 镜像的 Dockerfile 过程中,执行 yum install 指令时,遇到了错误:Could not resolve host: mirrorlist.centos.org; Unknown error。

js需要同时发起百条接口请求怎么办?--通过Promise实现分批处理接口请求

如何通过 Promise 实现百条接口请求? 实际项目中遇到需要发起上百条Promise接口请求怎么办? 前言 不知你项目中有没有遇到过这样的情况,反正我的实际工作项目中真的遇到了这种玩意,一个接口获取一份列表,列表中的每一项都有一个属性需要通过另一个请求来逐一赋值,然后就有了这份封装 真的是很多功

如何将文本转换为向量?(方法三)

​ 文本转换为向量有多种方式: 方法一:通过模型服务灵积DashScope将文本转换为向量(推荐) 方法二:通过ModelScope魔搭社区中的文本向量开源模型将文本转换为向量 方法三:通过Jina Embeddings v2模型将文本转换为向量 方法四:通过百川智能向量化模型将文本转换为向量 本文

深度解读昇腾CANN模型下沉技术,提升模型调度性能

如何减少Host Bound模型的Device空闲时间,从而优化模型执行性能显得尤其重要,GE(Graph Engine)图引擎通过图模式的Host调度和模型下沉调度的方式,可提升模型调度性能,缩短模型E2E执行时间。

如何实现元素的曝光监测

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。 本文作者:霁明 一些名词解释 曝光 页面上某一个元素、组件或模块被用户浏览了,则称这个元素、组件或模块被曝光了。 视图元素 将页面上展示的元素、组件或模块统称为视图元素

如何实现一个分布式锁

如何实现一个分布式锁 本篇内容主要介绍如何使用 Java 语言实现一个注解式的分布式锁,主要是通过注解+AOP 环绕通知来实现。 1. 锁注解 我们首先写一个锁的注解 /** * 分布式锁注解 */ @Retention(RetentionPolicy.RUNTIME) @Target({Eleme

如何设计一套单点登录系统 ?

一、介绍 在企业发展初期,使用的后台管理系统还比较少,一个或者两个。 以电商系统为例,在起步阶段,可能只有一个商城下单系统和一个后端管理产品和库存的系统。 随着业务量越来越大,此时的业务系统会越来越复杂,项目会划分成多个组,每个组负责各自的领域,例如:A组负责商城系统的开发,B组负责支付系统的开发,