1

我正在使用 JDK 15。(我正在使用 ByteBuddy 1.10.16 生成一些类,但我认为它在这里几乎无关紧要,除了作为背景信息。)

在其中一个生成的类中,我调用invokeExact()了一个我设法存储在生成的类中的MethodHandle 常量。它是通过 获得的“字段设置器” MethodHandles.Lookup#findSetter

(在下文中,我知道该MethodHandles.privateLookupIn()方法。)

我注意到MethodHandle当它代表一个字段时,有问题的“字段设置器”会失败private。在大多数层面上,这并不让我感到惊讶:直接MethodHandle是,嗯,直接:虽然我不假装对所有这些东西的内部了解很多,但在我看来,它肯定只是包装了一些没有的低级字节码访问检查。

但是鉴于privateLookupIn()which 的存在表明在某些情况下可以绕过访问检查,是否有一条路径可以让我MethodHandle从 A 类中“收获”一个可以读取private字段的“字段设置器”,然后将其作为常量存储在另一个B类这样invokeExact()就可以成功?

我相信我过去做过类似的事情(必须检查)涉及private方法,但在那些情况下,我没有使用MethodHandle 常量,即我在使用和存储结果时获取MethodHandle类初始化时间并将结果存储在一个字段中,然后调用该字段的内容。如果我必须继续走这条路,我会的,但这里的常量似乎很有吸引力,如果可以的话,使用它们会很好。<clinit>privateLookupIn()MethodHandleprivate static finalinvokeExact()MethodHandle

因此,我的问题的另一种表述方式是:表示 a 的常量形式MethodHandle是否能够存储其特权?或者是否有一些一次性的方法来“提高”给定MethodHandle存储为常量的特权?或者一个给定的值被存储为一个常量这一事实是否MethodHandle会阻止它在任何时候访问除传统可访问的 Java 构造之外的任何东西? 我在相关部分的 JVM 规范中没有看到任何非常明显的内容。

4

1 回答 1

3

您链接的规范指出:

要解析,使用以下四个步骤来解析 字节码行为中MH对类、接口、字段和方法的所有符号引用:MH

R已解决。当的字节码行为是种类 1、2、3 或 4时,这就像通过字段解析(§5.4.3.2)发生,并且当的字节码行为是种类 5时,好像通过方法解析( §5.4.3.3)发生, 6、7 或 8,并且好像通过接口方法解析(§5.4.3.4)当的字节码行为是种类 9。MHMHMH

链接的章节,即针对字段的§5.4.3.2,描述了普通的解析过程,包括访问控制。即使没有该显式声明,您也可以从前面的描述中推导出访问控制的存在,即这些符号方法句柄引用应该等同于列出的特定字节码行为。

因此,通过类文件常量池的条目获取的直接方法句柄CONSTANT_MethodHandle_info无法访问字节码指令也无法直接访问的类或成员。

但是从 JDK 11 开始,您可以使用动态常量来加载由任意引导过程定义的任意类型的常量。因此,当您可以用 Java 代码表达如何获取常量时,例如使用privateLookupIn,您还可以将其定义为动态常量的引导,并将该常量加载到您将加载直接方法句柄的位置。

考虑以下起点:

public class DynConstant {
    private static void inacessibleMethod() {
        new Exception("inacessibleMethod() called").printStackTrace();
    }

    public static void main(String[] args) throws Throwable {
        // express the constant
        Handle theHandle = new Handle(H_INVOKESTATIC,
            Type.getInternalName(DynConstant.class), "inacessibleMethod",
            Type.getMethodDescriptor(Type.VOID_TYPE), false);

        String generatedClassName
                = DynConstant.class.getPackageName().replace('.', '/')+"/Test";

        ClassWriter cw = new ClassWriter(0);
        cw.visit(55, ACC_INTERFACE|ACC_ABSTRACT,
                generatedClassName, null, "java/lang/Object", null);

        MethodVisitor mv = cw.visitMethod(
                ACC_PUBLIC|ACC_STATIC, "test", "()V", null, null);
        mv.visitCode();
        mv.visitLdcInsn(theHandle);
        mv.visitMethodInsn(INVOKEVIRTUAL,
                "java/lang/invoke/MethodHandle", "invokeExact", "()V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(1, 0);
        mv.visitEnd();
        cw.visitEnd();
        byte[] code = cw.toByteArray();

        ToolProvider.findFirst("javap").ifPresentOrElse(javap -> {
            String fName = generatedClassName+".class";
            try {
                Path dir = Files.createTempDirectory("javapTmp");
                Path classFile = dir.resolve(fName);
                Files.createDirectories(classFile.getParent());
                Files.write(classFile, code);
                javap.run(System.out, System.err, "-c", "-cp",
                    dir.toAbsolutePath().toString(), generatedClassName);
                for(Path p = classFile;;p=p.getParent()) {
                    Files.delete(p);
                    if(p.equals(dir)) break;
                }
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            }
        }, () -> System.out.println("javap not found in current environment"));

        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            lookup.findStatic(lookup.defineClass(code),
                "test", MethodType.methodType(void.class)).invokeExact();
        }
        catch(Throwable t) {
            t.printStackTrace();
        }
    }
}

它尝试定义一个新的运行时类,该类MethodHandle尝试inacessibleMethod()通过CONSTANT_MethodHandle_info. 程序打印

interface instexamples.Test {
  public static void test();
    Code:
       0: ldc           #12                 // MethodHandle REF_invokeStatic instexamples/DynConstant.inacessibleMethod:()V
       2: invokevirtual #17                 // Method java/lang/invoke/MethodHandle.invokeExact:()V
       5: return
}
java.lang.IllegalAccessError: class instexamples.Test tried to access private method 'void instexamples.DynConstant.inacessibleMethod()' (instexamples.Test and instexamples.DynConstant are in unnamed module of loader 'app')
    at instexamples.Test.test(Unknown Source)
    at instexamples.DynConstant.main(DynConstant.java:100)

现在,让我们将常量更改为动态常量,该常量将执行等价于

MethodHandles.Lookup l = MethodHandles.lookup();
l = MethodHandles.privateLookupIn(DynConstant.class, l);
MethodHandle mh = l.findStatic(
        DynConstant.class, "inacessibleMethod", MethodType.methodType(void.class));

当常数第一次解决时。常数的定义“有点”复杂。由于代码包含三个方法调用,因此该定义需要三个方法句柄,此外,还需要一个现有引导方法的句柄ConstantBootstraps.invoke(…),允许使用任意方法调用进行引导。这些句柄可用于定义动态常量,而动态常量允许作为另一个动态常量的常量输入。

所以我们将// express the constant注释后的定义替换为:

Type string = Type.getType(String.class), clazz = Type.getType(Class.class);
Type oArray = Type.getType(Object[].class), object = oArray.getElementType();
Type mhLookup = Type.getType(MethodHandles.Lookup.class);
Type mHandle = Type.getType(MethodHandle.class), mType = Type.getType(MethodType.class);
Type targetType = Type.getType(DynConstant.class);

String methodHandles = Type.getInternalName(MethodHandles.class);

Handle methodHandlesLookup = new Handle(H_INVOKESTATIC, methodHandles,
    "lookup", Type.getMethodDescriptor(mhLookup), false);
Handle privateLookupIn = new Handle(H_INVOKESTATIC, methodHandles,
    "privateLookupIn", Type.getMethodDescriptor(mhLookup, clazz, mhLookup), false);
Handle findStatic = new Handle(H_INVOKEVIRTUAL, mhLookup.getInternalName(),
    "findStatic", Type.getMethodDescriptor(mHandle, clazz, string, mType), false);
Handle invoke = new Handle(H_INVOKESTATIC,
    Type.getInternalName(ConstantBootstraps.class), "invoke",
    Type.getMethodDescriptor(object, mhLookup, string, clazz, mHandle, oArray), false);

ConstantDynamic methodHandlesLookupC = new ConstantDynamic("lookup",
    mhLookup.getDescriptor(), invoke, methodHandlesLookup);
ConstantDynamic privateLookupInC = new ConstantDynamic("privateLookupIn",
    mhLookup.getDescriptor(), invoke, privateLookupIn, targetType, methodHandlesLookupC);
ConstantDynamic theHandle = new ConstantDynamic("findStatic",
    mHandle.getDescriptor(), invoke, findStatic,
    privateLookupInC, targetType, "inacessibleMethod", Type.getMethodType("()V"));

为了避免重复很长的常量方法描述符字符串,我使用了 ASM 的Type抽象。原则上,我们可以为所有类型名称和签名使用常量字符串。

该程序打印:

interface instexamples.Test {
  public static void test();
    Code:
       0: ldc           #45                 // Dynamic #2:findStatic:Ljava/lang/invoke/MethodHandle;
       2: invokevirtual #50                 // Method java/lang/invoke/MethodHandle.invokeExact:()V
       5: return
}
java.lang.Exception: inacessibleMethod() called
    at instexamples.DynConstant.inacessibleMethod(DynConstant.java:23)
    at instexamples.Test.test(Unknown Source)
    at instexamples.DynConstant.main(DynConstant.java:89)

由方法调用创建的三个常量组成的动态常量的复杂性将导致相当大的常量池。尽管我们有一个额外的方法,但我们可能会生成一个自定义引导方法并获得一个小得多的类文件:

public class DynConstant {
    private static void inacessibleMethod() {
        new Exception("inacessibleMethod() called").printStackTrace();
    }

    public static void main(String[] args) throws Throwable {
        Type string = Type.getType(String.class), clazz = Type.getType(Class.class);
        Type mhLookup = Type.getType(MethodHandles.Lookup.class);
        Type mHandle = Type.getType(MethodHandle.class), mType = Type.getType(MethodType.class);

        Type targetType = Type.getType(DynConstant.class);

        String myBootstrapName = "privateLookup";
        String myBootstrapDesc = Type.getMethodDescriptor(mHandle, mhLookup, string, clazz, clazz, mType);

        String generatedClassName = DynConstant.class.getPackageName().replace('.', '/')+"/Test";

        Handle myBootStrap = new Handle(H_INVOKESTATIC, generatedClassName,
            myBootstrapName, myBootstrapDesc, true);
        ConstantDynamic theHandle = new ConstantDynamic("inacessibleMethod",
            mHandle.getDescriptor(), myBootStrap, targetType, Type.getMethodType("()V"));

        ClassWriter cw = new ClassWriter(0);
        cw.visit(55, ACC_INTERFACE|ACC_ABSTRACT, generatedClassName, null, "java/lang/Object", null);

        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC|ACC_STATIC, "test", "()V", null, null);
        mv.visitCode();
        mv.visitLdcInsn(theHandle);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandle", "invokeExact", "()V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(1, 0);
        mv.visitEnd();
        mv = cw.visitMethod(ACC_PRIVATE|ACC_STATIC, myBootstrapName, myBootstrapDesc, null, null);
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 3); // bootstrap argument, i.e. DynConstant.class
        mv.visitVarInsn(ALOAD, 0); // MethodHandles.lookup() generated as JVM arg
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "privateLookupIn",
            Type.getMethodDescriptor(mhLookup, clazz, mhLookup), false);
        mv.visitVarInsn(ALOAD, 3); // bootstrap argument, i.e. DynConstant.class
        mv.visitVarInsn(ALOAD, 1); // invoked name, i.e. "inacessibleMethod"
        mv.visitVarInsn(ALOAD, 4); // bootstrap argument, i.e. MethodType ()V
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandles$Lookup", "findStatic",
            Type.getMethodDescriptor(mHandle, clazz, string, mType), false);
        mv.visitInsn(ARETURN);
        mv.visitMaxs(4, 5);
        mv.visitEnd();
        cw.visitEnd();
        byte[] code = cw.toByteArray();

        ToolProvider.findFirst("javap").ifPresentOrElse(javap -> {
            String fName = generatedClassName+".class";
            try {
                Path dir = Files.createTempDirectory("javapTmp");
                Path classFile = dir.resolve(fName);
                Files.createDirectories(classFile.getParent());
                Files.write(classFile, code);
                javap.run(System.out, System.err, "-p", "-c", "-cp",
                    dir.toAbsolutePath().toString(), generatedClassName);
                for(Path p = classFile;;p=p.getParent()) {
                    Files.delete(p);
                    if(p.equals(dir)) break;
                }
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            }
        }, () -> System.out.println("javap not found in current environment"));

        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            lookup.findStatic(lookup.defineClass(code),
                "test", MethodType.methodType(void.class)).invokeExact();
        }
        catch(Throwable t) {
            t.printStackTrace();
        }
    }
}
interface instexamples.custombootstrap.Test {
  public static void test();
    Code:
       0: ldc           #18                 // Dynamic #0:inacessibleMethod:Ljava/lang/invoke/MethodHandle;
       2: invokevirtual #23                 // Method java/lang/invoke/MethodHandle.invokeExact:()V
       5: return

  private static java.lang.invoke.MethodHandle privateLookup(java.lang.invoke.MethodHandles$Lookup, java.lang.String, java.lang.Class, java.lang.Class, java.lang.invoke.MethodType);
    Code:
       0: aload_3
       1: aload_0
       2: invokestatic  #29                 // Method java/lang/invoke/MethodHandles.privateLookupIn:(Ljava/lang/Class;Ljava/lang/invoke/MethodHandles$Lookup;)Ljava/lang/invoke/MethodHandles$Lookup;
       5: aload_3
       6: aload_1
       7: aload         4
       9: invokevirtual #35                 // Method java/lang/invoke/MethodHandles$Lookup.findStatic:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
      12: areturn
}
java.lang.Exception: inacessibleMethod() called
    at instexamples.custombootstrap.DynConstant.inacessibleMethod(DynConstant.java:22)
    at instexamples.custombootstrap.Test.test(Unknown Source)
    at instexamples.custombootstrap.DynConstant.main(DynConstant.java:91)

bootstrap 方法被设计为可重用。它接收所有必要的信息作为常量参数,因此不同的ldc指令可以使用它来获取不同成员的句柄。JVM 确实已经将调用者的查找上下文作为第一个参数传递了,所以我们可以使用它而不需要调用MethodHandles.lookup(). 搜索成员的类是第一个附加参数,它用作两者的第一个参数,privateLookupInfindStatic。由于每个动态常量都有一个标准名称参数,我们可以使用它来表示成员的名称。最后一个参数表示MethodType要查找的方法。当我们为字段查找进行改造时,我们可以删除该参数,作为第三个标准参数,预期的常量类型可以与预期的字段类型匹配。

基本上,自定义引导方法会执行privateLookupIn您在问题中描述的基于查找,但使用它ldc允许进行延迟初始化(而不是字段的类初始化时间),同时在链接指令后static final仍像字段一样进行优化。static final此外,这些动态常量允许作为其他动态常量或指令的其他引导方法的常量输入(不过,您也可以使用此引导方法invokedynamic将现有static final字段调整为动态常量)。

于 2020-11-05T08:19:25.467 回答