[备份] 如何实现Java的内联汇编

介绍

Java是否可以在代码中嵌入ASM代码,类似C++的__asm__呢。通过一些摸索,在老外的代码中学习到了一种方式。下方这段代码,仅初始化了RuntimeProcess对象,当执行完内联汇编之后,将会执行Runtime.exec方法并使Process p指向返回值,或者说得到返回值。本文就是解释如何实现下方这一段代码内容,虽然暂时想不到合适的使用场景

动态调用

将Java代码编译为字节码之后,会遇到一个问题,我们编写的__asm__方法和其中的表达式是无法直接变成我们希望的字节码的,而是这样的字节码:

在当前类中会产生一个新的方法

这个方法中是我们表达式的字节码,可以看到我们希望的ALOAD 1实际上是ICONST_1压栈常量1后INVOKE某个类的ALOAD方法。我们需要做的事情是将这一段替换为实际上期望的ALOAD 1指令,然后写入到原来的类中

收集

第一步要做的是Collect收集所有的__asm__方法,在我源码的CollectVisitor中核心代码如下,可以大致浏览下,我将逐步介绍

这里的BLOCK_TYPE_DESC代码如下:某一个返回值为void且入参类型是ASMOpcodes的方法。注释中的方法正好符合这个条件,这也是JVM根据Lambda表达式生成的新方法:返回值为空且参数是ASMOpcodes接口(自定义的核心接口)

参数类型是ASMOpcodes因为__asm__方法泛型中已经指定

回到上文,接下来我创建了一个根据当前类名和方法名生成的唯一类名,然后将当前表达式生成的新方法(当前visit的方法)写入了这个唯一类中,交给后续处理

写好的ClassWriter对象保存到某一个Map中,注意这个Map记录了每一个__asm__内联汇编方法对应的一个唯一类ClassWriter对象,包含了一个新生成的方法,后续我们将会对这个对象做操作

最后我们return proxy没有特殊意义,这里没有对字节码进行修改。如果当前方法不是新生成方法而是普通方法,返回null表示不感兴趣不是目标

核心

第一处收集完毕接下来是核心部分,我们重新对字节码内容进行分析,修改字节码。之前保存的Map对象有用处,在Map中保存的Key包含了所有内联汇编生成的新方法,如果分析到达了新生成的方法处直接返回,我们不需要对新生成方法内的具体代码做操作

进入到CoreAdapter方法,处理其他所有的方法。注意如果普通方法中遇到了__asm__INVOKE指令则直接返回,不处理这种情况,换句话来说是删除这条指令。而这个指令对比文章开头,是与INVOKEDYNAMIC配合的指令。遇到其他的方法调用正常继续分析即可

核心是处理INVOKEDYNAMIC指令的问题,这里省略一大堆判断,判断的意义是找到我们的__asm__精确对应的INVOKEDYNAMIC指令以及参数,通过handle.getName拿到的是自动生成新方法的名称,然后实例化一个新的类,反射调用该类的某个方法,这里稍后分析。注意最后直接返回表示删除当前的INVOKEDYNAMIC指令,配合上文删除下一条指令,使得原有的__asm__块代码完全被删除

实例化新类的方法如下,通过之前保存的Map拿到对应的唯一类ClassWriter对象,其中包含了写入的__asm__生成的新方法,得到字节码后通过UnsafedefineClass方法得到这个凭空捏造的类型。接下来我们会反射调用这个类包含的方法

反射调用这个凭空捏造类中包含的__asm__块自动生成的新方法

ASMCoreVisitor中没有什么魔法,是简单的使用MethodVisitor构造字节码的方法,选取了其中两处很简单的指令代码,这也是ASM生成字节码的代码。

注意上文传入的初始化参数是this所以这里的visitXxx等方法通过这个MethodVisitor (this)构造出我们希望的ASM代码,反射调用后this对象中保存了调用这一套visit方法后的内容(可以理解为希望的指令已经写入了this中,等待ClassWriter.toBytesArray即可导出包含了希望指令的字节码)

可能会有一个疑问,对这里的this进行一系列的调用之后,是否会影响到后续正常的代码?不会,因为当前的操作全都在visitInvokeDynamicInsn方法中,且一系列调用后返回的是null以删除原来的__asm__块,后续的指令调用后续指令对应的visit方法,与这里将没有关联

处理畸形字节码

这样的反射调用,执行了通过UNSAFE定义的新类的新方法中的ASM代码,使this对象包含了希望的字节码,但这里引出了下一个问题,MethodVisitor是畸形的,并不能直接转字节码使用,因为只有指令没有符号表,常量池的内容有问题。可以测试直接导出字节码,分析后发现确实进入了指令,但是由于符号表和其它一些原因,这个字节码是无法正常运行的

通过上文ASMVisitor中的魔法操作,现在rewriter中的字节码指令是我们真正需要的,但这个字节码整体来看是畸形的,还需要后续处理,代码如下

老外代码使用一些魔法来复制symbolTable对象,我分析老外的魔法其实本质是一个反射操作,于是写了以上的代码。但是我对这个操作持怀疑态度,因为新的ClassWriter会根据指令自动生成symbolTable对象,复制未必有意义。删除这一处但复制后测试代码没有问题,所以这可能是多余的操作

况且除了符号表,还有一些结构可能存在问题,例如局部变量数组的大小,是否会溢出等问题

通过一个新的ClassWriter读取畸形字节码重新生成后,会得到正确的符号表,以及正确的字节码,拿到正确的字节码之后,一切就简单了,写回到对应的文件中,提示用户再次执行即可

Patch

在JDK 8中拿到的SystemClassLoaderURLClassLoader其中包含了我们编译生成的target/classes以及测试的test-classes目录,我收集到这些路径并调用Runner的方法进行Patch

遍历目录找所有classes路径下但.class文件,进行Patch

核心Patch的过程如下,首先收集得到一个Map对象,保存了所有的__asm__表达式生成的新方法以及对应的ClassWriter对象,接下来找到每一个方法中的__asm__块,删除已有的指令,反射将希望的ASM代码加入到MethodVisitor中,得到一个畸形的ClassWriter对象,畸形的字节码需要一个新的ClassWriter重新生成符号表,得到真正的字节码,然后写回到原来的文件

关于

代码地址在:github

其中有些地方可能我的理解还不到位,如果有错误之处,还请大佬教导,我进行修正