Presentation
生成和转换编译方法的ASM API是基于MethodVisitor接口,它通过ClassVisitor的vistMethod方法返回。除了annotation和debug信息外,基于指令参数类型个数,该接口为每个字节码指令类别定义了一个方法。这些方法必须以以下顺序调用:
visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )*
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |
visitLocalVariable | visitLineNumber )*
visitMaxs )?
visitEnd
如果有annotation和attribute,必须首先访问。接着就是方法的指令。对这些方法来说,在visitCode和visitMaxs之间,代码必须顺序访问。
public interface MethodVisitor {
AnnotationVisitor visitAnnotationDefault();
AnnotationVisitor visitAnnotation(String desc, boolean visible);
AnnotationVisitor visitParameterAnnotation(int parameter,
String desc, boolean visible);
void visitAttribute(Attribute attr);
void visitCode();
void visitFrame(int type, int nLocal, Object[] local, int nStack,
Object[] stack);
void visitInsn(int opcode);
void visitIntInsn(int opcode, int operand);
void visitVarInsn(int opcode, int var);
void visitTypeInsn(int opcode, String desc);
void visitFieldInsn(int opc, String owner, String name, String desc);
void visitMethodInsn(int opc, String owner, String name, String desc);
void visitJumpInsn(int opcode, Label label);
void visitLabel(Label label);
void visitLdcInsn(Object cst);
void visitIincInsn(int var, int increment);
void visitTableSwitchInsn(int min, int max, Label dflt,
Label labels[]);
void visitLookupSwitchInsn(Label dflt, int keys[], Label labels[]);
void visitMultiANewArrayInsn(String desc, int dims);
void visitTryCatchBlock(Label start, Label end, Label handler,
String type);
void visitLocalVariable(String name, String desc, String signature,
Label start, Label end, int index);
void visitLineNumber(int line, Label start);
void visitMaxs(int maxStack, int maxLocals);
void visitEnd();
}
visitCode和visitMaxs方法可以用来探测方法指令代码的开始和结束。
ClassVisitor和MethodVisitor接口结合使用来生成完整的类:
ClassVisitor cv = ...;
cv.visit(...);
MethodVisitor mv1 = cv.visitMethod(..., "m1", ...);
mv1.visitCode();
mv1.visitInsn(...);
...
mv1.visitMaxs(...);
mv1.visitEnd();
MethodVisitor mv2 = cv.visitMethod(..., "m2", ...);
mv2.visitCode();
mv2.visitInsn(...);
...
mv2.visitMaxs(...);
mv2.visitEnd();
cv.visitEnd();
记住,没有必要来完成一个method来结束访问另外一个方法。实际上MethodVisitor实例是完全独立的,可以以任何顺序访问。
ClassVisitor cv = ...;
cv.visit(...);
MethodVisitor mv1 = cv.visitMethod(..., "m1", ...);
mv1.visitCode();
mv1.visitInsn(...);
...
MethodVisitor mv2 = cv.visitMethod(..., "m2", ...);
mv2.visitCode();
mv2.visitInsn(...);
...
mv1.visitMaxs(...);
mv1.visitEnd();
...
mv2.visitMaxs(...);
mv2.visitEnd();
cv.visitEnd();
ASM基于MethodVisotr接口提供了三个核心组件来生成和转换方法:ClassReader类转换编译方法的内容,且调用MethodVisitor对象的相关方法。
1) ClassReader类转换编译方法的内容,且调用MethodVisitor对象的相关方法。
2) ClassWriter的visitMethod方法返回MethodVisitor接口的实现,它直接以二进制方式构建编译方法。
3) MethodAdapter是MethodVisitor的一个实现,它代理另一个MethodVisitor实例的所有方法。
ClassWriter可选项
计算一个方法的stack map frame并不容易,你必须计算所有的frame,且找到对应跳转到目标对象的frame,或无条件跳转,最后压缩剩余的frames。同样,计算本地变量和操作栈的大小并不容易。幸运的是ASM可以为你计算,通过指定什么被自动计算的方式来创建ClassWriter。
1) new ClassWriter(0),不会计算任何东西,你必须计算自己的frames和本地变量和操作栈大小。
2) new ClassWriter(ClassWriter.COMPUTER_MAXS),本地变量和操作栈的大小被计算。你必须调用visitMaxs,但是可是使用任何参数,它们会被忽略,重计算。使用这个选项,你不得不计算frames。
3) new ClassWriter(ClassWriter.COMPUTER_FRAMES),一切都被计算好了。没有必要调用visitFrame,但是必须调用visitMaxs。
使用这些选项很便利,但有很大的代价:COMPUTE_MAXS选项使得ClassWriter慢10%,使用COMPUTE_FRAMES慢2倍。
如果你选择自己计算frames,你可使得ClassWriter类为你压缩。如果这样的话,你必须访问未压缩frames,visitFrame(F_NEW,nLocals,locals,nStack, stack),这里的nLocals和nStacks是本地变量和操作栈大小,locals和stack是包含对应类型的数组。
Generating methods
在上面提及的getF方法的字节码可以通过以下方法来生成:mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "pkg/Bean", "f", "I");
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
第一个调用开始字节码生成,后面的三个调用生成方法的三条指令。visitMaxs必须在所有指令被访问过后才被调用。它定义执行帧的本地变量和操作栈大小。最后一个方法调用结束方法的生成。
setF方法的字节码和构造器的字节码生成的方式类似。更加复杂的是checkAndSetF方法:
mv.visitCode();
mv.visitVarInsn(ILOAD, 1);
Label label = new Label();
mv.visitJumpInsn(IFLT, label);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitFieldInsn(PUTFIELD, "pkg/Bean", "f", "I");
Label end = new Label();
mv.visitJumpInsn(GOTO, end);
mv.visitLabel(label);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitTypeInsn(NEW, "java/lang/IllegalArgumentException");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL,
"java/lang/IllegalArgumentException", "<init>", "()V");
mv.visitInsn(ATHROW);
mv.visitLabel(end);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
visitCode和visitEnd之间的调用和checkAndSetF方法做了精确的映射。一个Lable对象指明了visitLable后面的指令是属于这个lable范畴的。可能有多个lable指向同一个指令,但是一个label必须指向一个指令。换句话说,可能使用不同的lable多次调用visitLable,但是使用在一个指令中的lable必须使用visitLable一次。最后一个约束,lable常量是不能共享的,每个方法必须有它自己的lable。
Transforming methods
方法可以像类一样被转换,例如通过方法适配器(传递它接受的方法调用):更改参数来改变个别指令。MethodAdapter类提供了诸如method visitor的基本实现,它除了传递接它接受的方法调用外,不做任何事情。
为了理解method adapter怎么使用,看下以下的简单例子:移除方法内部的NOP指令。
public class RemoveNopAdapter extends MethodAdapter {
public RemoveNopAdapter(MethodVisitor mv) {
super(mv);
}
@Override
public void visitInsn(int opcode) {
if (opcode != NOP) {
mv.visitInsn(opcode);
}
}
}
public class RemoveNopClassAdapter extends ClassAdapter {
public RemoveNopClassAdapter(ClassVisitor cv) {
super(cv);
}
@Override
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
MethodVisitor mv;
mv = cv.visitMethod(access, name, desc, signature, exceptions);
if (mv != null) {
mv = new RemoveNopAdapter(mv);
}
return mv;
}
}
class adapter仅仅构建了一个method adapter(它封装了在chain中被下一个class visitor返回的method visitor),并且返回这个adapter。method adapter连的构建与class adapter chain相似。
然而,这不是强制的,能以不同于class adapter chain的构建方式来构建method adapter chain。甚至,每个方法能有不同的method adapter chain。例如,class adapter可以仅在方法中选择移除NOPs,不在构造器。
mv = cv.visitMethod(access, name, desc, signature, exceptions);
if (mv != null && !name.equals("<init>")) {
mv = new RemoveNopAdapter(mv);
}
method adapter chian可以拥有比class adapter chain多的拓扑图。例如class adapter chain是线性的,method adapter可以有分支。
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
MethodVisitor mv1, mv2;
mv1 = cv.visitMethod(access, name, desc, signature, exceptions);
mv2 = cv.visitMethod(access, "_" + name, desc, signature, exceptions);
return new MultiMethodAdapter(mv1, mv2);
}
Stateless transformations
假如我们想测量程序中每个类的花费时间,我们需要在每个类中加入一个静态timer域,且需要加入每个方法的执行时间到这个timer域。我们需要在类中加入一下代码:
public static long timer;
public void m() throws Exception {
timer -= System.currentTimeMillis();
Thread.sleep(100);
timer += System.currentTimeMillis();
}
为了对ASM怎么实现这个有大体的理解,我们编译两个类且比较这两个版本的输出。
GETSTATIC C.timer : J
INVOKESTATIC java/lang/System.currentTimeMillis()J
LSUB
PUTSTATIC C.timer : J
LDC 100
INVOKESTATIC java/lang/Thread.sleep(J)V
GETSTATIC C.timer : J
INVOKESTATIC java/lang/System.currentTimeMillis()J
LADD
PUTSTATIC C.timer : J
RETURN
MAXSTACK = 4
MAXLOCALS = 1
我们必须在方法的开头添加4个指令,且在返回指令浅四个其他指令。我们需要更新最大操作栈大小。方法开头的代码使用visitCode方法访问。我们通过覆写该方法添加前4个指令:
public void visitCode() {
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LSUB);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
代码中的owner必须设置为被转换的class的名称。我们必须在return之前添加后四个指令(在return或athrow之前)。这些指令没有任何参数,可以在visitInsn方法中访问。
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
mv.visitInsn(opcode);
}
最后我们必须更新操作栈的大小。我们添加了push long值的指令,因此需要4个slot。在方法开始的时候,操作站初始化所以我们知道在方法开头添加的4个指令需要4个slot。我们插入代码离开栈时,状态为改变(入栈多少,出栈多少)。结果是,如果原始代码需要栈的大小为s,栈大小为max(s,4)。不幸的是,我们在返回之前添加了指令。我们并不知道在此之前操作栈的大小。我们仅知道小于或等于s。因此我们仅能说添加代码后,在返回指令之前需要的操作栈最大值s+4。我们必须覆盖visitMaxs方法:
public void visitMaxs(int maxStack, int maxLocals) {
mv.visitMaxs(maxStack + 4, maxLocals);
}
依赖于COMPUTE_MAX选项,不必为操作栈大小的事烦心。
package com.fanshadoop.example;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class AddTimerAdapter extends ClassVisitor implements Opcodes {
private String owner;
private boolean isInterface;
public AddTimerAdapter(ClassVisitor cv) {
super(ASM4, cv);
}
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
cv.visit(version, access, name, signature, superName, interfaces);
owner = name;
isInterface = (access & ACC_INTERFACE) != 0;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
exceptions);
if (!isInterface && mv != null && !name.equals("<init>")) {
mv = new AddTimerMethodAdapter(mv);
}
return mv;
}
@Override
public void visitEnd() {
if (!isInterface) {
FieldVisitor fv = cv.visitField(ACC_PUBLIC + ACC_STATIC, "timer",
"J", null, null);
if (fv != null) {
fv.visitEnd();
}
}
cv.visitEnd();
}
class AddTimerMethodAdapter extends MethodVisitor {
public AddTimerMethodAdapter(MethodVisitor mv) {
super(ASM4, mv);
}
@Override
public void visitCode() {
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LSUB);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
mv.visitInsn(opcode);
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
mv.visitMaxs(maxStack + 4, maxLocals);
}
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
}
}
Statefull transformations
上章节看的transformation是本地的,不依赖我们访问的当前的代码的指令:在开始添加的代码总是相同,总是被添加。这些transformation被成为stateless transformation。
更复杂的transformation需要记住当前代码访问的一些指令状态。例如,移除所有ICONST_0_IADD。很明确的是,当IADD指令访问时,仅当最后一个访问指令是ICONST_0时被移除。这需要在method adapter内部存储状态,类似这些transformation被称为statefull transformation。
当ICONST_0被加载时,仅当下一个指令为IADD时,它应该被删除。但下一指令到目前为止未知。解决的方法是将这个决定延期到下一个指令:如果是IADD移除两个指令,否则忽略ICONST_0和当前指令。
为了实现移除或替换指令序列,引入MethodVisitor子类,它的vistXxxInsn方法调用普通的visitInsn()方法:
public abstract class PatternMethodAdapter extends MethodVisitor {
protected final static int SEEN_NOTHING = 0;
protected int state;
public PatternMethodAdapter(int api, MethodVisitor mv) {
super(api, mv);
}
@Overrid public void visitInsn(int opcode) {
visitInsn();
mv.visitInsn(opcode);
}
@Override public void visitIntInsn(int opcode, int operand) {
visitInsn();
mv.visitIntInsn(opcode, operand);
}
protected abstract void visitInsn();
}
public class RemoveAddZeroAdapter extends PatternMethodAdapter {
private static int SEEN_ICONST_0 = 1;
public RemoveAddZeroAdapter(MethodVisitor mv) {
super(ASM4, mv);
}
@Override public void visitInsn(int opcode) {
if (state == SEEN_ICONST_0) {
if (opcode == IADD) {
state = SEEN_NOTHING;
return;
}
}
visitInsn();
if (opcode == ICONST_0) {
state = SEEN_ICONST_0;
return;
}
mv.visitInsn(opcode);
}
@Override protected void visitInsn() {
if (state == SEEN_ICONST_0) {
mv.visitInsn(ICONST_0);
}
state = SEEN_NOTHING;
}
}
Labels and frames
labels和frames仅在它们相关的指令之前被访问。换句话说,他们在与指令的同时被访问,尽管他们不是指令本身。这对探测指令序列有影响,但事实上,这也是优点所在。如果某些指令会跳转到ICONST_0,这意味着这儿有lable指向该指令。移除这两个指令后,该label会指向被移除的指令的后继者。这种情况下,在ICONST_0和IADD之间必须存在一个容易被探测的label。
对于stack map frames来说也是如此:如果stack map frame在两个指令之间被访问,我么不能移除他们。这两种情况都可以以模式匹配逻辑的方式考虑将lable和frames做为指令的方式来处理。
public abstract class PatternMethodAdapter extends MethodVisitor {
@Override public void visitFrame(int type, int nLocal, Object[] local,
int nStack, Object[] stack) {
visitInsn();
mv.visitFrame(type, nLocal, local, nStack, stack);
}
@Override public void visitLabel(Label label) {
visitInsn();
mv.visitLabel(label);
}
@Override public void visitMaxs(int maxStack, int maxLocals) {
visitInsn();
mv.visitMaxs(maxStack, maxLocals);
}
}
注意:visitMaxs也调用visitInsn方法,用来处理方法的尾部是序列的前缀,且必须被探测的情况。