Java 13+ 中的仪器本机方法

问题描述 投票:0回答:1

我想在 java 13+ 中检测本机方法(最好使用 javassist)。假设我想为特定方法添加日志记录,然后调用真正的本机方法。之前可以通过本机方法前缀(问题https://bugs.openjdk.org/browse/JDK-6263317和我的答案以及使用javassist的可能解决方案使用javassist编辑本机方法类?),但自J13以来它是如果没有显式选项,则无法添加新方法(问题https://bugs.openjdk.java.net/browse/JDK-8221528),该方法从一开始就被弃用,可以随时删除。

这个话题有很多问题,例如:

但它们都没有涵盖如何使用 javassist 在 J13+ 中包装没有

-XX:+AllowRedefinitionToAddDeleteMethods
选项的本机方法。我可以替换方法体,但看不到下一步的好动作。在纯 java 中实现每个有趣的本机方法是非常困难或不可能的,并且扩展性不好,所以我不能只是替换方法。可能是我之前尝试使用 javassist 时做错了什么。或者我唯一的选择可能是使用 JNA 以某种方式从 jdk 库调用任意函数。

java instrumentation javaagents
1个回答
0
投票

不完全是完整的答案,但也许会对某人有所帮助。我想在纯java中没有通用的解决方案,因为函数名称并不总是遵循JNI命名约定,并且可以通过

RegisterNatives
手动注册。所以你不能只在运行时为 JNA 创建接口并使用它。如果您不害怕 C/++,最重要的是管理库分发、特定平台编译和加载,您可以尝试从 Method(不是 java Method,参见
RegisterNatives
实现)对象查找指向本机函数的指针,从java签名,然后使用JNI/JNA调用它。话虽这么说,我没有找到纯java解决方案。不过,如果您知道相应的本机方法,您可以尝试一些方法。

例如,让我们看一下

Thread.sleep
方法(我最初想对其进行检测)。

第一个解决方案

等几年。现在它不再是原生的,您可以对其进行检测!问题解决了!

第二种解决方案

用纯java替换方法。您可以搜索类似的方法(如

parkNanos
)并围绕它构建解决方案(也处理“无原因”部分)或使用实用程序线程中的等待和调度任务来阻止当前线程,以通过通知唤醒当前线程。凌乱的。并且在中断期间你可以得到不同的异常。当然有一个问题 - 如何找到替代品以及如果您也想检测替代品该怎么办。

我使用了这个方法并有 2 个分支 - 是否启用了AllowRedefinitionToAddDeleteMethods。现在我需要考虑第一个解决方案并添加第三个分支。

第三种解决方案

假设您找到了本机方法名称,它是

JVM_Sleep
。现在你可以使用JNA来调用它了。只需添加依赖项,然后声明接口,如下所示:

public interface ThreadWrapper extends com.sun.jna.Library {
    void JVM_Sleep(JNIEnv env, Class thisObj, long nanos) throws InterruptedException;
}

然后这样称呼它:

ThreadWrapper lib = com.sun.jna.Native.load("jvm", ThreadWrapper.class, Collections.singletonMap(Library.OPTION_ALLOW_OBJECTS, Boolean.TRUE));
long nanos = MILLISECONDS.toNanos(millis);
lib.JVM_Sleep(JNIEnv.CURRENT, Thread.class, nanos);

简单的解决方案,但需要了解方法名称、新的相当重的依赖项,并引入 JNA 的额外开销。 哦,还有一件事 -

JVM_Sleep
更改了版本之间的合同,因此它在旧版本中接收 millis,在新版本中接收 nanos。当然,您应该意识到这一点并为此拥有
if

第四种解决方案

Java现在有了外部函数API,我们可以用它来调用JNI方法。 主要优点 - 新的 API 不需要任何依赖项。 主要缺点 - 新的 API 并没有真正与旧的 JNI 函数建立良好而干净的桥梁。 您仍然可以这样做(错误处理超出了示例的范围):

public void invokeJvmSleepFull(long millis) {
    System.out.println("Start " + new Date());
    try (Arena arena = Arena.ofConfined()) {
        Linker linker = Linker.nativeLinker();
        SymbolLookup jvmLib = SymbolLookup.libraryLookup("jvm", arena);

        // use JNI_GetCreatedJavaVMs to get address of running VM
        MemorySegment jniGetCreatedVmsAddr = jvmLib.find("JNI_GetCreatedJavaVMs").get();
        FunctionDescriptor jniGetCreatedVmsSig =
                FunctionDescriptor.of(ValueLayout.JAVA_INT,
                        ValueLayout.ADDRESS,
                        ValueLayout.JAVA_INT,
                        ValueLayout.ADDRESS
                );
        MethodHandle jniGetCreatedVms = linker.downcallHandle(jniGetCreatedVmsAddr, jniGetCreatedVmsSig);
        MemorySegment writtenVmsCount = arena.allocate(ValueLayout.OfInt.JAVA_INT, 0);
        MemorySegment writtenVms = arena.allocateArray(ValueLayout.ADDRESS, 2);
        int code = (int) jniGetCreatedVms.invokeExact(writtenVms, 2, writtenVmsCount);
        if (code != JNI_OK) {
            throw new RuntimeException("Unexpected code " + code);
        }
        MemorySegment jvmAddress = writtenVms.get(ValueLayout.ADDRESS, 0);

        // navigate through JavaVM_ -> functions (aka JNIInvokeInterface_) -> GetEnv to get Env address
        StructLayout JavaVM_Layout = MemoryLayout.structLayout(
                POINTER.withName("functions")
        ).withName("JavaVM_");
        long functionsOffset = JavaVM_Layout.byteOffset(MemoryLayout.PathElement.groupElement("functions"));
        MemorySegment functionsAddr = jvmAddress.reinterpret(JavaVM_Layout.byteSize()).get(ValueLayout.ADDRESS, functionsOffset);

        StructLayout JNIInvokeInterface_Layout = MemoryLayout.structLayout(
                POINTER.withName("reserved0"),
                POINTER.withName("reserved1"),
                POINTER.withName("reserved2"),
                POINTER.withName("DestroyJavaVM"),
                POINTER.withName("AttachCurrentThread"),
                POINTER.withName("DetachCurrentThread"),
                POINTER.withName("GetEnv"),
                POINTER.withName("AttachCurrentThreadAsDaemon")
        ).withName("JNIInvokeInterface_");
        long getEnvOffset = JNIInvokeInterface_Layout.byteOffset(MemoryLayout.PathElement.groupElement("GetEnv"));
        MemorySegment jniGetEnvAddr = functionsAddr.reinterpret(JNIInvokeInterface_Layout.byteSize()).get(ValueLayout.ADDRESS, getEnvOffset);

        FunctionDescriptor jniGetEnvSig =
                FunctionDescriptor.of(ValueLayout.JAVA_INT,
                        ValueLayout.ADDRESS,
                        ValueLayout.ADDRESS,
                        ValueLayout.JAVA_INT
                );
        MethodHandle jniGetEnv = linker.downcallHandle(jniGetEnvAddr, jniGetEnvSig);
        MemorySegment envAddress = arena.allocate(ValueLayout.ADDRESS);
        code = (int) jniGetEnv.invokeExact(jvmAddress, envAddress, JNI_VERSION_21);
        if (code != JNI_OK) {
            throw new RuntimeException("Unexpected code " + code);
        }
        envAddress = envAddress.get(ADDRESS, 0);

        // use env to call JVM_Sleep. It does not use class so just pass null instead.
        MemorySegment jvmSleepAddr = jvmLib.find("JVM_Sleep").get();
        FunctionDescriptor jvmSleepSig =
                FunctionDescriptor.ofVoid(
                        ValueLayout.ADDRESS,
                        ValueLayout.ADDRESS,
                        ValueLayout.JAVA_LONG);
        MethodHandle threadSleep = linker.downcallHandle(jvmSleepAddr, jvmSleepSig);
        threadSleep.invokeExact(envAddress, MemorySegment.NULL, MILLISECONDS.toNanos(millis));
    } catch (Throwable e) {
        throw new RuntimeException(e);
    }
    System.out.println("End " + new Date());
}

除了新的依赖之外,该解决方案还存在 JNA 解决方案的问题。它引入了新的问题 - 它是预览功能,它需要了解

JavaVM_
JNIInvokeInterface_
的结构,我不确定是否可以通过 FFI 查找
jclass
而无需原生。

© www.soinside.com 2019 - 2024. All rights reserved.