4

我想,我正在尝试做一些相对简单的事情。以 doSomething(int) 方法的以下 Java 字节码为例:

public java.lang.String doSomething(int i);
0 iload_1 [i]
1 invokestatic MyHelper.doSomething(int) : Java.lang.String
4 areturn

这个字节码几乎只将调用转发给静态助手。

我现在要做的是使用Javassist 将invokestatic 替换为invokedynamic。我知道如何用 ASM 做到这一点,因此只是假设我出于纯粹的好奇想知道它是如何工作的。以下是我的一些问题:

1) 以下是否正确:我不能使用 javassist CtMethod.instrument() 或 CtMethod.insertAt() 方法,因为这些方法需要一个包含有效 Java 表达式的字符串,而我不能用 Java 语法编写一个 invokedynamic?

2)invokestatic 的参数就像invokevirtual 或invokestatic 的参数一样处理,对吧?我的意思是,您将参数放在invokedynamic 之前的堆栈中,就像您为invokevirtual 所做的那样?

3) 是否有使用 Javassist 创建调用动态字节码的示例代码(或者你能想出一些)?

这是我目前所知道的:你可以创建一个字节码对象,它有一个 addInvokedynamic() 方法。但这需要 BootstrapMethodsAttribute 中的 BootstrapMethod 索引。BootstrapMethod 反过来期望常量池中的方法句柄信息的索引,该索引需要方法引用等。所以本质上你必须自己管理整个常量池条目。没关系,但我担心我做错了,以后会引入奇怪的问题。有没有更简单的方法来做到这一点(一个辅助方法左右)?我的代码大致看起来像这样(我并没有真正“重写”上面的调用静态但是:

void transform(CtMethod ctmethod, String originalCallDescriptor) {

    MethodInfo mInfo = ctmethod.getMethodInfo();
    ConstPool pool = ctmethod.getDeclaringClass().getClassFile().getConstPool();

    /* add info about the static bootstrap method to the constant pool*/
    int mRefIdx = /*somehow create a method reference entry*/
    int mHandleIdx = constPool.addMethodHandleInfo(ConstPool.REF_invokeStatic, mRefIdx);

    /* create bootstrap methods attribute; there can only be one per class file! */
    BootstrapMethodsAttribute.BootstrapMethod[] bms = new BootstrapMethodsAttribute.BootstrapMethod[] {
        new BootstrapMethodsAttribute.BootstrapMethod(mHandleIdx, new int[] {})
    };
    BootstrapMethodsAttribute bmsAttribute = new BootstrapMethodsAttribute(constPool, bms);
    mInfo.addAttribute(bmsAttribute);

    //... and then later, finally
    Bytecode bc = new Bytecode(constPool);
    ... push parameters ...
    bc.addInvokedynamic(0 /*index in bms array*/, mInfo.getName(), originalCallDescriptor);

    //replace the original method body with the one containing invokedynamic
    mInfo.removeCodeAttribute();
    mInfo.setCodeAttribute(bc.toCodeAttribute());

}

非常感谢您的帮助和时间!

4

1 回答 1

5

我必须说这是你在这里遇到的一个非常有趣的问题。如果我的回答有点长,我很抱歉,但我已尽力为您提供尽可能多(我认为是)有用的信息,以帮助您和其他遇到此问题的人。

问题 1

以下是否正确:我不能使用 javassist CtMethod.instrument() 或 CtMethod.insertAt() 方法,因为这些方法需要一个包含有效 Java 表达式的字符串,而我不能用 Java 语法编写一个 invokedynamic?

你是对的。

CtMethod.insertAt()只能用于 Java 代码而不是字节码操作码。 另一方面, CtMethod.instrument()允许您处理字节码,甚至可以使用ExprEditorCodeConverter以非常有限的方式对其进行修改。但是正如我所说,它们允许您更改的内容非常有限,并且对于您要实现的目标,这两个修饰符都无法帮助您。

问题2

invokestatic 的参数的处理方式与 invokevirtual 或 invokestatic 的参数一样,对吧?我的意思是,您将参数放在invokedynamic 之前的堆栈中,就像您为invokevirtual 所做的那样?

我不知道我是否完全理解您在这里真正要问的内容(您在第一句话中重复了 invokestatic )。我认为您要问的问题(如果我错了,请纠正我)是,如果invokedynamic中的参数的处理方式与它们在invokevirtualinvokestatic中的处理方式相同。使您能够简单地为invokedynamic切换invokevirtualinvokestatic。我会假设它是在回答这个问题时......

首先需要注意的是,在处理堆栈时, invokevirtualinvokestatic本身是不同的。Invokevirtual除了将所需的参数推入堆栈之外,正如invokestatic所做的那样,它还推入对象引用,以便可以链接方法调用。


边注

您可能已经知道这一点,但我添加了这些附加信息,以防其他人遇到这个问题并且想知道为什么invokestaticinvokevirtual以不同的方式处理堆栈。

  • invokestatic操作码用于调用类中的静态方法,这意味着在编译时 JVM 确切地知道如何进行方法调用链接。

  • 另一方面,invokedynamic操作码用于对象实例的方法调用。由于在编译时无法知道将方法调用链接到哪里,因此只有在 JVM 知道正确的对象引用时才能在运行时链接。


当对操作码的工作方式有疑问时,我的建议是查看JVM 规范中有关JVM 指令集的章节(链接适用于 JVM 7,即编写本文时的当前版本)。

我刚刚这样做是为了检查我们在这里谈论的 3 个操作码。

两个操作码invokestaticinvokedynamic具有相同的操作数堆栈定义:

..., [arg1, [arg2 ...]] →

...

正如我之前所说, invokevirtual具有不同的操作数堆栈定义,即:

..., objectref, [arg1, [arg2 ...]] →

...

我在这里的第一个假设(我必须警告您,我还没有深入了解invokedynamic操作码)是您无法像使用invokestatic那样简单地更改invokevirtualinvokevirtual。我这样说是因为invokedynamic不期望堆栈中有任何对象引用。

为了更好地理解这种情况,我的建议是使用java.lang.invoke包在 Java 中编写一个示例,这将允许您创建使用invokedynamic操作码的 java 字节码。编译类后,使用命令检查生成的字节码javap -l -c -v -p

问题 3

是否有使用 Javassist 创建调用动态字节码的示例代码(或者你能想出一些)?

不是我知道的。我也用谷歌搜索了一下(你可能也已经这样做了),但我什么也没找到。我认为您的帖子将给出 javassist 的第一个代码示例:)

还有一些笔记

所以本质上你必须自己管理整个常量池条目。没关系,但我担心我做的不对,以后会引入奇怪的问题

只要您使用ConstPool类来管理您的常量池,javassist 就会为您处理所有事情而不会产生问题。

此外,如果您创建了一个损坏的容器池,最常见(很可能总是)会发生的情况是,一旦您尝试加载类或调用修改后的方法,您就会遇到 ClassFormatException 错误。我会说这是它是否有效的情况之一。

我想不出这样一种场景,可以隐藏某种奇怪的错误,等待那个讨厌的时刻困扰你,而你却不那么期待(注意我说我想不到,并不真的意味着他们没有存在)。我什至敢说,只要你可以加载类并调用它的方法而不让 JVM 崩溃,你就可以了。

有没有更简单的方法来做到这一点(一个辅助方法左右)?

我不这么认为。Javassist 在字节码修改方面为您提供了很多帮助,但它是在您使用更高级别的 API 时(例如,编写 java 代码并注入该代码或移动/复制 CtMethods、Ctclasses 等)。当您使用必须处理所有字节码的低级 API 时,您几乎只能靠自己了。

我知道这可能不是您正在寻找的答案,但我希望我已经对这个主题有所了解。

于 2013-03-17T00:06:16.793 回答