首页投稿(暂停使用,暂停投稿)程序员

ASM简介(四)

2016-07-31  本文已影响1776人  千里山南

函数

我们在使用ASM相关API对函数进行操作之前,我们需要了解函数在字节码的存储格式及其执行模型。

执行模型

我们需要简单了解Java虚拟机的执行模型。Java代码是在线程中执行的,每个Java程序可能包含多个执行线程,而每个线程都有一个执行栈。这些执行栈是由许多frame(帧)组成的,每一个frame都代表一个函数调用。每当一个函数被调用的时候,一个frame就被推到执行线程的栈顶。当函数执行完毕(无论是正常还是异常返回)这个frame就会弹栈,然后执行下一个frame.
每一个frame都包含两个部分,一个本地的符号表和一个操作栈。符号表存储了可以被自由取值的变量,操作栈主要用于在执行时存储字节码指令。本地符号表大小及操作栈大小是由函数本身大小决定的。因此二者是在编译期由编译器计算并存储在字节码中的。
当每个frame创建的时候,其操作栈是空的,符号表由this对象和函数参数组成。符号表和操作栈中每一个item都可以防止任意的除long和double以外的Java对象。这是因为long和double需要两个item才能存下。

字节码指令

字节码指令是由一个opcode和几个固定的参数组成
opcode 是无符号byte类型。因此一般用助记符代替,例如一般使用NOP代替0.参数一般是静态的值,用于说明opcode的操作对象。参数是静态存储在class文件里的。opcode只有运行时才知道。
总体来说opcode主要分为两大类,其中一小部分主要用于将数据由符号表推送到操作栈或者相反。其余的指令只是操作操作栈上的对象。他们可以从操作栈弹出几个对象,根据这些对象计算出一些值,然后将结果再送回操作栈。
ILOAD LLOAD FLOAD DLOAD ALOAD 指令用于将符号表中的变量推动到操作栈上。他们接收变量在符号表的索引值。ILOAD用于操作boolean byte char short int. LOAD ,FLOAD DLOAD用于long,float,double的操作。ALOD主要用于非基本数据类型的操作。对应 ISTORE,LSTORE,FSTORE,DSTORE ASTORE 从操作站弹出并将其存储在符号表。字节码指令基本都是类型相关的。字节码指令一般可以分为:

对于以下简单的函数,其对应的指令如下:

package pkg;
public class Bean {
    private int f;
    public int getF() {
        return this.f;
    }
    public void setF(int f) {
        this.f = f;
    }
}


 // getF函数对应的指令如下
 ALOAD 0   //将this入栈 
 GETFIELD pkg/Bean f I //this弹出,将this.f入栈
         IRETURN // 返回 this.f
 // setF函数对应的指令      
 ALOAD 0  // this 入栈
 ILOAD 1  // 将索引为1(类型为int)的变量入栈
 PUTFIELD pkg/Bean f I // 弹出两个值,并且将栈顶的元素及this弹出,并将其值赋给this.f
         RETURN  // RETURN

Bean对象会有默认的构造函数,其对应的指令为:ALOAD 0 INVOKESPECIAL java/lang/Object <init> ()V RETURN
构造函数在字节码中的名称为<init>
一个复杂的例子如下:

public void checkAndSetF(int f) {
    if (f >= 0) {
        this.f = f;
    } else {
        throw new IllegalArgumentException();
    }
}
// 其对应的字节码指令
ILOAD 1 // f入栈
IFLT label // f弹栈,如果小于0,跳转到label
ALOAD 0 // this 入栈
ILOAD 1 // f入栈
PUTFIELD pkg/Bean f I // this.f = f
GOTO end //跳转到end
label:
NEW java/lang/IllegalArgumentException //生成IllegalArgumentException对象
        DUP // 赋值对象
INVOKESPECIAL java/lang/IllegalArgumentException <init> ()V // 弹栈并调用初始化方法
        ATHROW // 抛出栈底异常,指令执行结束
end:
RETURN

异常处理

并没有catch对应的字节码指令,不过函数会和一系列exception handler(异常处理代码)相关联。当抛出指定的异常时对应的handler代码就会执行。因此exception handler就和try catch代码块类似。

public static void sleep(long d) {
    try {
        Thread.sleep(d);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

//对应的指令如下

TRYCATCHBLOCK try catch catch java/lang/InterruptedException // 声明handler,如果try: 和 catch: 之间代码发生异常,则生成InterruptedException实例,跳转到catch:
try:
LLOAD 0
INVOKESTATIC java/lang/Thread sleep (J)V
RETURN
catch:
INVOKEVIRTUAL java/lang/InterruptedException printStackTrace ()V
RETURN

Frames(帧)
Java6及以上版本编译的class文件,包含一系列stack map frames来加速jvm对class的校验。它们甚至在运行前就能告知jvm某个frame的符号表及操作栈的详细信息。为此,我们可以为frame中的每一个指令创建一个frame来查看其运行时的状态

//运行前的state frame     对应的指令
[pkg/Bean] []           ALOAD 0
[pkg/Bean] [pkg/Bean]   GETFIELD
[pkg/Bean] [I]          IRETURN

//对于 throw new IllegalArgumentException的代码:
[pkg/Bean I] []                                             NEW
[pkg/Bean I] [Uninitialized(label)]                         DUP
[pkg/Bean I] [Uninitialized(label) Uninitialized(label)]    INVOKESPECIAL
[pkg/Bean I] [java/lang/IllegalArgumentException]           ATHROW

上述的Uninitialized只存在于stack map frame中,代表内存分配完毕但是构造函数还没调用。UNINITIALIZED_THIS 代表被初始化为0 TOP代表未定义类型 NULL代表null
对于编译后的class为了节省空间,实际上并不是每一个指令都对应一个state frame,而是只有跳转指令和异常处理handler 和无条件跳转后面的第一个指令包含state frame. 而其他指令可以从已有的state frames推断出来。为了进一步节省空间,每一个frame只有在和上一个frame不同的时候才会被存储。初始帧由于可以很容易从函数参数中推断,因此不会存储,而后续的帧如果和初始帧相同只需存储 F_SAME即可

ILOAD 1
IFLT label
ALOAD 0
ILOAD 1
PUTFIELD pkg/Bean f I
GOTO end
label:
F_SAME
NEW java/lang/IllegalArgumentException
        DUP
INVOKESPECIAL java/lang/IllegalArgumentException <init> ()V
        ATHROW
end:
F_SAME
        RETURN

接口及组件

函数的生成改写需要使用MethodVisitor(在ClassVisitor的visitMethod函数中返回),生成函数时各个部分的调用顺序也是固定的:

visitAnnotationDefault?
        ( visitAnnotation | visitParameterAnnotation | visitAttribute )*
        ( visitCode
        ( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |
                visitLocalVariable | visitLineNumber )*
visitMaxs )?
visitEnd

注解和参数需要首先被生成,然后是方法体,最后需要调用visitMax。 visitCode和visitMax可以看做函数体的开始和结束。最后需要调用visitEnd代表事件结束。

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();

于我们而言计算某个方法的stack frame绝非易事。幸好ASM可以帮我们计算,当你声明一个ClassWriter的时候你可以声明让ASM自动计算这些值。例如:new ClassWriter(ClassWriter.COMPUTE_MAX) 符号表及操作栈的大小会自动帮你计算。但你仍然需要调用visitMax,此时你传什么值都可以(它们会被忽略),但你仍要自己计算frames。而new ClassWriter(ClassWriter.COMPUTE_FRAMES) 所有的都会自动被计算,但仍要调用visitMax.但是COMPUTE_MAX会使得ClassWriter慢10%左右,而COMPUTE_FRAMES会慢一倍。

//生成getF的代码如下
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "pkg/Bean", "f", "I");
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();

//
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(); // 这里又创建一个label,虽然不创建直接用前一个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();

函数也可以像类那样被修改,我们可以直接使用MethodVisitor即可。而MethodVisitor可以包含多分枝:

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);
}  

测量类中所有方法的执行时间

public class C {
    public static long timer;
    public void m() throws Exception {
        timer -= System.currentTimeMillis();
        Thread.sleep(100);
        timer += System.currentTimeMillis();
    }
}

我们可以使用TraceClassVisitor来查看该类生成的字节码:使用Textifier或者使用ASMifier

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行代码结束的时候插入另外4行代码。我们还需要更新最大操作栈的大小,因此我们在visitCode中添加如下代码

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");
}

我们需要添加另外4条指令,在return或者xRETURN、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);
}

最后我们需要更新最大操作栈的大小。由于我们向操作栈添加了两个long型变量,因此最坏情况maxsize+4

public void visitMaxs(int maxStack, int maxLocals) {
    mv.visitMaxs(maxStack + 4, maxLocals);
}

当然我们也可以依赖COMPUTE_MAX来计算。
我们的ClassVisitor可以这样来写:

public class AddTimerAdapter extends ClassVisitor {
    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();
    }
}

对于label和frames而言,我们知道visitLabel的调用是在其关联代码前的。如果代码跳转到ICONST_0 IADD ,当我们把这两个指令删除后跳转后直接执行后续的代码,这正是我们期望的。而如果是跳转到IADD 我们就不能直接删除相关指令了,不过这样的话ICONST_0和IADD之前肯定有一个label.如果在两个指令之间我们访问了stack map frame, 我们也不能删除这些指令。这些case都可以通过把label和frame当做需要match的指令(我们要记录指令出现的位置及后续指令的状态)来处理。编译后的class文件同时包含了其对应源代码中的行号信息用于异常栈的处理。
很多时候,我们需要记录某一指令调用时所处的状态才能识别固定的pattern,例如如果我们想找出class中自赋值语句(f=f this.f = this.f)等就需要记录ALOAD 0的状态

class RemoveGetFieldPutFieldAdapter extends PatternMethodAdapter {
    private final static int SEEN_ALOAD_0 = 1;
    private final static int SEEN_ALOAD_0ALOAD_0 = 2;
    private final static int SEEN_ALOAD_0ALOAD_0GETFIELD = 3;
    private String fieldOwner;
    private String fieldName;
    private String fieldDesc;

    public RemoveGetFieldPutFieldAdapter(MethodVisitor mv) {
        super(mv);
    }

    @Override
    public void visitVarInsn(int opcode, int var) {
        switch (state) {
            case SEEN_NOTHING: // S0 -> S1
                if (opcode == ALOAD && var == 0) {
                    state = SEEN_ALOAD_0;
                    return;
                }
                break;
            case SEEN_ALOAD_0: // S1 -> S2
                if (opcode == ALOAD && var == 0) {
                    state = SEEN_ALOAD_0ALOAD_0;
                    return;
                }
                break;
            case SEEN_ALOAD_0ALOAD_0: // S2 -> S2
                if (opcode == ALOAD && var == 0) {
                    mv.visitVarInsn(ALOAD, 0);
                    return;
                }
                break;
        }
        visitInsn();
        mv.visitVarInsn(opcode, var);
    }

    @Override
    public void visitFieldInsn(int opcode, String owner, String name,
                               String desc) {
        switch (state) {
            case SEEN_ALOAD_0ALOAD_0: // S2 -> S3
                if (opcode == GETFIELD) {
                    state = SEEN_ALOAD_0ALOAD_0GETFIELD;
                    fieldOwner = owner;
                    fieldName = name;
                    fieldDesc = desc;
                    return;
                }
                break;
            case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 -> S0
                if (opcode == PUTFIELD && name.equals(fieldName)) {
                    state = SEEN_NOTHING;
                    return;
                }
                break;
        }
        visitInsn();
        mv.visitFieldInsn(opcode, owner, name, desc);
    }

    @Override
    protected void visitInsn() {
        switch (state) {
            case SEEN_ALOAD_0: // S1 -> S0
                mv.visitVarInsn(ALOAD, 0);
                break;
            case SEEN_ALOAD_0ALOAD_0: // S2 -> S0
                mv.visitVarInsn(ALOAD, 0);
                mv.visitVarInsn(ALOAD, 0);
                break;
            case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 -> S0
                mv.visitVarInsn(ALOAD, 0);
                mv.visitVarInsn(ALOAD, 0);
                mv.visitFieldInsn(GETFIELD, fieldOwner, fieldName, fieldDesc);
                break;
        }
        state = SEEN_NOTHING;
    }
}

工具类

ClassVisitor中介绍的工具类在这里仍然是适用的。

上一篇下一篇

猜你喜欢

热点阅读