ASM(六) 利用TreeApi 动态生成以及转换方法字节码

时间:2021-07-09 17:06:17

 

一、MethodNode概述  

         ASM的TreeApi 对于Method的转换、生成也提供了一系列的组件和接口。其功能主要基于前一章提到的MethodNode类。MethodNode中大多数属性和方法都和ClassNode类似,其中最主要的属性就是InsnList了。InsnList是一个双向链表对象,包含了存储方法的字节指令序。先来看下InsnList中的主要是属性和方法:

public class InsnList { // public accessors omitted
private int size;
private AbstractInsnNode first;
private AbstractInsnNode last;
AbstractInsnNode[] cache;
int size();
AbstractInsnNode getFirst();
AbstractInsnNode getLast();
AbstractInsnNode get(int index);
boolean contains(AbstractInsnNode insn);
int indexOf(AbstractInsnNode insn);
void accept(MethodVisitor mv);
ListIterator iterator();
ListIterator iterator(int index);
AbstractInsnNode[] toArray();
void set(AbstractInsnNode location, AbstractInsnNode insn);
void add(AbstractInsnNode insn);
void add(InsnList insns);
void insert(AbstractInsnNode insn);
void insert(InsnList insns);
void insert(AbstractInsnNode location, AbstractInsnNode insn);
void insert(AbstractInsnNode location, InsnList insns);
void insertBefore(AbstractInsnNode location, AbstractInsnNode insn);
void insertBefore(AbstractInsnNode location, InsnList insns);
void remove(AbstractInsnNode insn);
void clear();
}

         可以看到InsnList 中主要是对AbstractInsnNode对象的操作方法,AbstractInsnNode也就是链表中的元素。其中,AbstractInsnNode数组存储了字节码指令对象的链表连接关系。AbstractInsnNode是一个抽象父类,代表了字节指令的一个抽象类。AbstractInsnNode的主要方法如下:

public abstract class AbstractInsnNode {
public int getOpcode();
public int getType();
public AbstractInsnNode getPrevious();
public AbstractInsnNode getNext();
public void accept(MethodVisitor cv);
public AbstractInsnNode clone(Map labels);
}

         他的子类如VarInsnNode(代表局部变量表的操作指令对象,如xstore,xload)是和MethodVisitor中的visitVarInsn(int opcode, int var)关联的指令访问方法。LabelNode, FrameNode 以及 LineNumberNode也继承了AbstractInsnNode。这样就可以像CoreApi中MethodVisitor提供的visitXX 方法一样,插入在关联的指令前。在TreeApi中可以通过对象的getNext()方法方便找到跳转到的指令,并且移除指令的时候,只要label不变,也不会影响原有的跳转指令的跳转地址。同Core 不同的就是,从调用MethodVisitor各个指令对应的visitXX方法,改成对MethodNode 中InsnList对象的链表节点操作。

二、生成Method

         通过下面这个例子就会更加一目了然。当然,MethodNode生成class的效率要比MethodVisitor低,内存消耗也会大,但是我们可以更轻松得实现一段注入逻辑。

         方法内部的字节码结构样例,我们依然沿用一下在CoreApi 的Method介绍中使用的 ASM(三) 利用Method组件动态生成方法的字节码的例子。然后可以对比一下两种实现方式的不同。

package asm.tree.method;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.*;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

/**
* tree api method 生成字节码 Created by yunshen.ljy on 2015/7/20.
*/
public class GenerateClasses {

public static void main(String[] args) throws IOException {
ClassNode classNode = new ClassNode();
classNode.version = Opcodes.V1_8;
classNode.access = Opcodes.ACC_PUBLIC;
classNode.name = "bytecode/TreeMethodGenClass";
classNode.superName = "java/lang/Object";
classNode.fields.add(new FieldNode(Opcodes.ACC_PRIVATE, "espresso", "I", null, null));
// public void addEspresso(int espresso) 方法生命
MethodNode mn = new MethodNode(Opcodes.ACC_PUBLIC, "addEspresso", "(I)V", null, null);
classNode.methods.add(mn);
InsnList il = mn.instructions;
il.add(new VarInsnNode(Opcodes.ILOAD, 1));
il.add(new InsnNode(Opcodes.ICONST_1));
LabelNode label = new LabelNode();
// if (espresso > 0) 跳转通过LabelNode标记跳转地址
il.add(new JumpInsnNode(Opcodes.IF_ICMPLE, label));
il.add(new VarInsnNode(Opcodes.ALOAD, 0));
il.add(new VarInsnNode(Opcodes.ILOAD, 1));
// this.espresso = var1;
il.add(new FieldInsnNode(Opcodes.PUTFIELD, "bytecode/TreeMethodGenClass", "espresso", "I"));
LabelNode end = new LabelNode();
il.add(new JumpInsnNode(Opcodes.GOTO, end));
// label 后紧跟着下一个指令地址
il.add(label);
// java7之后对stack map frame 的处理
il.add(new FrameNode(Opcodes.F_SAME, 0, null, 0, null));
// throw new IllegalArgumentException();
il.add(new TypeInsnNode(Opcodes.NEW, "java/lang/IllegalArgumentException"));
il.add(new InsnNode(Opcodes.DUP));
il.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, "java/lang/IllegalArgumentException", "<init>", "()V", false));
il.add(new InsnNode(Opcodes.ATHROW));
il.add(end);
// stack map 的第二次偏移记录
il.add(new FrameNode(Opcodes.F_SAME, 0, null, 0, null));
il.add(new InsnNode(Opcodes.RETURN));
// 局部变量表和操作数栈大小的处理
mn.maxStack = 2;
mn.maxLocals = 2;
mn.visitEnd();
// 打印查看class的生成结果
ClassWriter cw = new ClassWriter(Opcodes.ASM5);
classNode.accept(cw);
File file = new File("TreeMethodGenClass.class");
FileOutputStream fout = new FileOutputStream(file);
try {
fout.write(cw.toByteArray());
fout.close();
} catch (IOException e) {
e.printStackTrace();
}

}
}

         InsnList il = mn.instructions;所有的方法指令都放在InsnList这样一个链表结构中。当然,这个链表结构也维系了整个字节码指令的结构。

三、转换方法的字节码

        利用Tree Api转化方法字节码,其实也就是对MethodNode对象的InsnList的操作。通过获取InsnList的迭代器,可以直接add 或者remove方法的指令。如果需要添加比较多的指令集,那么可以把指令集分开成不同的InsnList(临时的指令集对象)再将这些子集合并。具体的代码块如下:

InsnList il = new InsnList();
il.add(...);
...
il.add(...);
mn.instructions.insert(i, il);

        下面通过一个例子来看一下。这个例子是和之前CoreApi中介绍方法转换的例子(参考:http://yunshen0909.iteye.com/blog/2223935)相同。对比一下两种Api的方法转换实现方式的不同。

        这个例子中,我们还是对于一个Class的所有方法(除了构造器方法)注入一段计时的逻辑。整个Class我们需要先添加一个属性timer。这时候就可以堆ClassNode的fields属性进行add操作。代码块如下:

int acc = Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC;
cn.fields.add(new FieldNode(acc, "timer", "J", null, null));

         我们通过AddTimerTransformer类中的transform方法来实现,对ClassNode以及其MethodNode集合的操作。AddTimerTransformer  中的注入字节码逻辑实现如下:

package asm.tree.method;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.*;

import java.util.Iterator;
import java.util.List;

/**
* Created by yunshen.ljy on 2015/7/30.
*/
public class AddTimerTransformer {

public void transform(ClassNode cn) {
for (MethodNode mn : (List<MethodNode>) cn.methods) {
if ("<init>".equals(mn.name) || "<clinit>".equals(mn.name)) {
continue;
}
InsnList insns = mn.instructions;
if (insns.size() == 0) {
continue;
}
Iterator<AbstractInsnNode> j = insns.iterator();
while (j.hasNext()) {
AbstractInsnNode in = j.next();
int op = in.getOpcode();
if ((op >= Opcodes.IRETURN && op <= Opcodes.RETURN) || op == Opcodes.ATHROW) {
InsnList il = new InsnList();
il.add(new FieldInsnNode(Opcodes.GETSTATIC, cn.name, "timer", "J"));
il.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J",
false));
il.add(new InsnNode(Opcodes.LADD));
il.add(new FieldInsnNode(Opcodes.PUTSTATIC, cn.name, "timer", "J"));
insns.insert(in.getPrevious(), il);
}
}
InsnList il = new InsnList();
il.add(new FieldInsnNode(Opcodes.GETSTATIC, cn.name, "timer", "J"));
il.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false));
il.add(new InsnNode(Opcodes.LSUB));
il.add(new FieldInsnNode(Opcodes.PUTSTATIC, cn.name, "timer", "J"));
insns.insert(il);
mn.maxStack += 4;
}
int acc = Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC;
cn.fields.add(new FieldNode(acc, "timer", "J", null, null));
}
}

         简单验证如下:

package asm.tree.method;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;

import java.io.IOException;

/**
* Created by yunshen.ljy on 2015/8/12.
*/
public class AddTimerTransformTest {

public static void main(String[] args) throws IOException {
ClassReader cr = new ClassReader("asm.core.methord.Time");
ClassNode cn = new ClassNode();
cr.accept(cn, 0);
AddTimerTransformer at = new AddTimerTransformer();
at.transform(cn);
ClassWriter cw = new ClassWriter(0);
cn.accept(cw);
byte[] toByte = cw.toByteArray();
}
}

       注入了timer逻辑后的Class文件反编译后如下:

package asm.core.methord;

public class Time {
public static long timer;

public Time() {
}

public void myCount() {
timer -= System.currentTimeMillis();
byte i = 5;
byte j = 10;
System.out.println(j - i);
System.out.println(j + i);
System.out.println(j + 0);
System.out.println(0 + i);
timer += System.currentTimeMillis();
}

public static void myMethod(int a) {
timer -= System.currentTimeMillis();
System.out.println(a + 0);
timer += System.currentTimeMillis();
}
}

          对比CoreApi 示例中的AddTimerMethodAdapter的实现,TreeApi从流式的操作字节码转换成了对于字节码集合,也就是方法字节码链表元素的操作。并且这种操作是可以非按照字节码实际偏移量来编码的,因为通过遍历所有字节码list之后对于特定字节码(return等)的逻辑注入可以不受其他字节码子集的编码位置影响(例子中我们先插入了timer += System.currentTimeMillis();在遍历结束后再插入对于下面字节码指令的实现timer -= System.currentTimeMillis();)。然后通过mn.maxStack+= 4;操作maxStack属性的值,代替了像Core中需要覆盖visitMax方法(mv.visitMaxs(maxStack + 4,maxLocals);)去操作栈空间的变化。当然,整体看下来,TreeApi的操作更加便利,但代码量上来看,两种Api差距并不大。只是TreeApi更加面向对象,对开发者更加友好。  

 四、全局转换

        之前介绍的方法转换,迁移或者注入字节码指令都需要关注和知道字节码指令的位置。字节码指令位置关系如果写错了,那么生成的指令解析和验证就会出现问题,正如前面介绍的CoreApi的实现方式,实现起来也相当复杂。但是TreeApi 提供了任意位置来注入指令的实现方法。

        下面举例来看一下。还是引用之前的一个Coffee类的一段代码为例。原来的代码片段如下:

 

 int f;
public void addEspresso(int f) {
if (f >= 0) {
this.f = f;
} else {
throw new IllegalArgumentException();
}
}

      这段代码编译后,用javap分析的字节码指令集如下:

public void addEspresso(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=2
0: iload_1
1: iflt 13
4: aload_0
5: iload_1
6: i2l
7: putfield #2 // Field f:J
10: goto 21
13: new #3 // class java/lang/IllegalArgumentException
16: dup
17: invokespecial #4 // Method java/lang/IllegalArgumentException."<init>":()V
20: athrow
21: return
LineNumberTable:
line 56: 0
line 57: 4
line 59: 13
line 61: 21
StackMapTable: number_of_entries = 2
frame_type = 13 /* same */
frame_type = 7 /* same */

         字节码偏移位置10那一行,goto 21 直接跳转到return指令执行。这里我们把goto 21 直接替换成return 指令。通过TreeApi我们可以对指令的相对位置进行标记和转换,也就是可以通过操作指令对象的方式来update指令。实现代码如下:

package asm.tree.method;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.*;

import java.util.Iterator;

/**
* 将GOTO label 替换成label实际跳转到的指令-RETURN
* Created by yunshen.ljy on 2015/8/14.
*/
public class OptimizeJumpTransformer {

public void transform(MethodNode mn) {
InsnList insns = mn.instructions;
Iterator<AbstractInsnNode> i = insns.iterator();
while (i.hasNext()) {
AbstractInsnNode in = i.next();
if (in instanceof JumpInsnNode) {
// 初始化label
LabelNode label = ((JumpInsnNode) in).label;
AbstractInsnNode target;
// 循环调用,将goto XX 中的XX跳转地址记录在label变量中
while (true) {
target = label; // 跳转过滤掉FrameNode 和LabelNode
while (target != null && target.getOpcode() < 0) {
target = target.getNext();
}
if (target != null && target.getOpcode() == Opcodes.GOTO) {
label = ((JumpInsnNode) target).label;
} else {
break;
}
}
// 更新替换label的值(实际跳转地址)
((JumpInsnNode) in).label = label;
// 如果指令是goto ,并且新的跳转的目标指令是ARETURN 指令,那么就将当前的指令替换成这个return指令的一个clone对象
if (in.getOpcode() == Opcodes.GOTO && target != null) {
int op = target.getOpcode();
if ((op >= Opcodes.IRETURN && op <= Opcodes.RETURN) || op == Opcodes.ATHROW) {
// replace ’in’ with clone of ’target’
insns.set(in, target.clone(null));
}
}
}
}
}
}


        测试方法的代码片段如下:

ClassReader cr = new ClassReader("bytecode.Coffee");
ClassNode cn = new ClassNode();
cr.accept(cn, 0);
OptimizeJumpTransformer at = new OptimizeJumpTransformer();
List<MethodNode> methodNodes = cn.methods;
for(MethodNode mn :methodNodes){
if(mn.name.equals("addEspresso")){
at.transform(mn);
}
}<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>

        这时候可以对比一下CoreApi 的实现方式,我们不再需要关注字节码指令的绝对位置,也不再需要处理JVM的栈图表。转换后字节码指令如下:

 public void addEspresso(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=2
0: iload_1
1: iflt 11
4: aload_0
5: iload_1
6: i2l
7: putfield #21 // Field f:J
10: return
11: new #23 // class java/lang/IllegalArgumentException
14: dup
15: invokespecial #24 // Method java/lang/IllegalArgumentException."<init>":()V
18: athrow
19: return
LocalVariableTable:
Start Length Slot Name Signature
0 20 0 this Lbytecode/Coffee;
0 20 1 f I
LineNumberTable:
line 56: 0
line 57: 4
line 59: 11
line 61: 19
StackMapTable: number_of_entries = 2
frame_type = 11 /* same */
frame_type = 7 /* same */

五、MethodNode 源码解析

        TreeApi其实在ASM中不是独立的接口,通过和CoreApi的接口和组件结合,提供了更加友好的实现。这里以MethodNode为例。可以看到源码中MethodNode 继承于MethodVisitor。并且提供了两个accept方法,分别接受ClassVisitor以及MethodVisitor参数。accept方法处理了给予MethodNode的fileds的一组事件。MethodNode本身就成为了事件的接收方。

        Accept方法源码如下:

/**
* Makes the given method visitor visit this method.
*
* @param mv
* a method visitor.
*/
public void accept(final MethodVisitor mv) {
// visits the method parameters
int i, j, n;
n = parameters == null ? 0 : parameters.size();
for (i = 0; i < n; i++) {
ParameterNode parameter = parameters.get(i);
mv.visitParameter(parameter.name, parameter.access);
}
// visits the method attributes
if (annotationDefault != null) {
AnnotationVisitor av = mv.visitAnnotationDefault();
AnnotationNode.accept(av, null, annotationDefault);
if (av != null) {
av.visitEnd();
}
}
n = visibleAnnotations == null ? 0 : visibleAnnotations.size();
for (i = 0; i < n; ++i) {
AnnotationNode an = visibleAnnotations.get(i);
an.accept(mv.visitAnnotation(an.desc, true));
}
n = invisibleAnnotations == null ? 0 : invisibleAnnotations.size();
for (i = 0; i < n; ++i) {
AnnotationNode an = invisibleAnnotations.get(i);
an.accept(mv.visitAnnotation(an.desc, false));
}
n = visibleTypeAnnotations == null ? 0 : visibleTypeAnnotations.size();
for (i = 0; i < n; ++i) {
TypeAnnotationNode an = visibleTypeAnnotations.get(i);
an.accept(mv.visitTypeAnnotation(an.typeRef, an.typePath, an.desc,
true));
}
n = invisibleTypeAnnotations == null ? 0 : invisibleTypeAnnotations
.size();
for (i = 0; i < n; ++i) {
TypeAnnotationNode an = invisibleTypeAnnotations.get(i);
an.accept(mv.visitTypeAnnotation(an.typeRef, an.typePath, an.desc,
false));
}
n = visibleParameterAnnotations == null ? 0
: visibleParameterAnnotations.length;
for (i = 0; i < n; ++i) {
List<?> l = visibleParameterAnnotations[i];
if (l == null) {
continue;
}
for (j = 0; j < l.size(); ++j) {
AnnotationNode an = (AnnotationNode) l.get(j);
an.accept(mv.visitParameterAnnotation(i, an.desc, true));
}
}
n = invisibleParameterAnnotations == null ? 0
: invisibleParameterAnnotations.length;
for (i = 0; i < n; ++i) {
List<?> l = invisibleParameterAnnotations[i];
if (l == null) {
continue;
}
for (j = 0; j < l.size(); ++j) {
AnnotationNode an = (AnnotationNode) l.get(j);
an.accept(mv.visitParameterAnnotation(i, an.desc, false));
}
}
if (visited) {
instructions.resetLabels();
}
n = attrs == null ? 0 : attrs.size();
for (i = 0; i < n; ++i) {
mv.visitAttribute(attrs.get(i));
}
// visits the method's code
if (instructions.size() > 0) {
mv.visitCode();
// visits try catch blocks
n = tryCatchBlocks == null ? 0 : tryCatchBlocks.size();
for (i = 0; i < n; ++i) {
tryCatchBlocks.get(i).updateIndex(i);
tryCatchBlocks.get(i).accept(mv);
}
// visits instructions
instructions.accept(mv);
// visits local variables
n = localVariables == null ? 0 : localVariables.size();
for (i = 0; i < n; ++i) {
localVariables.get(i).accept(mv);
}
// visits local variable annotations
n = visibleLocalVariableAnnotations == null ? 0
: visibleLocalVariableAnnotations.size();
for (i = 0; i < n; ++i) {
visibleLocalVariableAnnotations.get(i).accept(mv, true);
}
n = invisibleLocalVariableAnnotations == null ? 0
: invisibleLocalVariableAnnotations.size();
for (i = 0; i < n; ++i) {
invisibleLocalVariableAnnotations.get(i).accept(mv, false);
}
// visits maxs
mv.visitMaxs(maxStack, maxLocals);
visited = true;
}
mv.visitEnd();
}

         这样我们就可以将CoreApi和TreeApi 结合起来,用CoreApi处理Class,TreeApi处理Method。这样在我们自己的ClassVisitorAdapter中就可以用下面的方式来处理method中的指令对象集合:

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (name.startsWith("is")) {
// System.out.println(" start with is method: " + name + desc);
}
return new MethodNode(Opcodes.ASM5, access, name, desc, signature, exceptions)
{
@Override public void visitEnd() {
accept(cv);
}
};
}