使用ASM的Java字节码检测:在INVOKESPECIAL指令处对代码注入进行VerifyError

时间:2013-05-03 12:01:18

标签: java instrumentation java-bytecode-asm javaagents verifyerror

我是字节码注入的新手。直到现在,我能够通过详尽的研究和痛苦的试验和错误得到我想要的一切:-) 但我似乎已经达到了目前所追求的目标。所以,这是:我的第一个stackoverflow问题!

我的目标是通过java代理跟踪方法调用的对象引用。我正在使用ASM 4.0库并实现了AdviceAdapter。 我的重写visitMethodInsn() - 方法如下所示:

/**
 * Visits a method instruction. A method instruction is an instruction that invokes a method.
 * The stack before INVOKEINTERFACE, INVOKESPECIAL and INVOKEVIRTUAL instructions is:
 * "objectref, [arg1, arg2, ...]"
 *
 * @param opcode the opcode of the type instruction to be visited. This opcode is either INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC or INVOKEINTERFACE.
 * @param owner  the internal name of the method's owner class.
 * @param name   the method's name.
 * @param desc   the method's descriptor.
 */
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc) {
    if (isExcluded()) {
        super.visitMethodInsn(opcode, owner, name, desc);
        return;
    }

    int arraySlot = -1;
    boolean isStatic = false;
    if (opcode == INVOKEVIRTUAL || opcode == INVOKEINTERFACE) {
        arraySlot = saveMethodParameters(owner, desc);
        super.visitMethodInsn(opcode, owner, name, desc);
    } else if (opcode == INVOKESTATIC) {
        isStatic = true;
        super.visitMethodInsn(opcode, owner, name, desc);
    } else if (opcode == INVOKESPECIAL && !owner.equals("java/lang/Object")) {
        //TODO: Causes VerifyError
        arraySlot = saveMethodParameters(owner, desc);
        super.visitMethodInsn(opcode, owner, name, desc);
    } else {
        super.visitMethodInsn(opcode, owner, name, desc);
    }

    if (arraySlot > 0) {
        loadLocal(arraySlot);
        push(0);
        arrayLoad(Type.getType(Object.class));
    } else {
        super.visitInsn(ACONST_NULL);
    }
    super.visitMethodInsn(INVOKESTATIC, "net/myjavaagent/MethodLogger",
            "writeToLoggerTest", "(Ljava/lang/Object;)V");
}

 /**
 * Pops the method invocation' arguments and objectref off the stack, saves them into a local array variable and
 * then puts them back on the stack again.
 *
 * @param owner owner class of the method
 * @param desc  method descriptor
 * @return the identifier of the local variable containing the parameters.
 */
private int saveMethodParameters(String owner, String desc) {
    JavaTracerAgent.agentErrorLogger.info("Save method parameters: " + owner + " " + desc);
    // Preparing the array construction
    Type objectType = Type.getType(Object.class);
    Type objectArrayType = Type.getType("[Ljava/lang/Object;");
    Type[] invokeParamTypes = getMethodParamTypes(owner, desc);
    int invokeParamCount = invokeParamTypes.length;

    // allocate a slot for the method parameters array
    int arrayLocal = newLocal(objectArrayType);
    // construct the object array
    push(invokeParamCount);
    newArray(objectType);
    // store array in the local variable
    storeLocal(arrayLocal);

    // pop the arguments off the stack into the array
    // note: the top one is the last parameter !
    for (int i = invokeParamCount - 1; i >= 0; i--) {
        Type type = invokeParamTypes[i];
        JavaTracerAgent.agentErrorLogger.info("Get from stack [" + i + "]:" + type.toString());

        if (type != null) {
            // convert value to object if needed
            box(type);
            // load array and  swap under value
            loadLocal(arrayLocal);
            swap(objectArrayType, objectType);
            // load index and swap under value
            push(i);
            swap(Type.INT_TYPE, objectType);
        } else {
            // this is a static method and index is 0 so we put null into the array
            // load array index and then null
            loadLocal(arrayLocal);
            push(i);
            push((Type) null);
        }
        // store the value in the array as an object
        arrayStore(objectType);
    }

    // now restore the stack and put back the arguments from the array in increasing order
    for (int i = 0; i < invokeParamCount; i++) {
        Type type = invokeParamTypes[i];
        JavaTracerAgent.agentErrorLogger.info("Put to stack [" + i + "]:" + type.toString());

        if (type != null) {
            // load the array
            loadLocal(arrayLocal);
            //retrieve the object at index i
            push(i);
            arrayLoad(objectType);
            //unbox if needed
            unbox(type);
        } else {
            // this is a static method so no target instance has to be put on stack
        }
    }

    return arrayLocal;
}

/**
 * Returns a type array containing the parameters of a method invocation:
 * <ul><li>owner type</li><li>arg1 type</li><li>arg2 type</li><li>...</li><li>argN type</li></ul>
 *
 * @param owner owner class
 * @param desc  method descriptor
 * @return method parameter types
 */
public Type[] getMethodParamTypes(String owner, String desc) {
    Type ownerType = Type.getObjectType(owner);
    Type[] argTypes = Type.getArgumentTypes(desc);
    int numArgs = argTypes.length;
    Type[] result = new Type[numArgs + 1];
    result[0] = ownerType;
    System.arraycopy(argTypes, 0, result, 1, numArgs);

    return result;
}

简而言之,我试图在将INVOKESOMETHING操作执行到局部变量之前保存堆栈中的所有内容。 为了能够执行方法操作,我必须将整个东西放回堆栈。之后我假设被调用对象的引用是我本地数组中的第一个条目。

以下是我的一个测试类。这个很简单:它只是开始另一个线程:

/**
* My test class.
*/
public class ThreadStarter {

    public static void main(String args[]) {

        Thread thread = new Thread("Hugo") {

            @Override
            public void run() {
                System.out.println("Hello World");
            }
        };

        thread.start();
    }
}

关于INVOKEVIRTUAL,INVOKEINTERFACE和INVOKESTATIC,我没有遇到任何问题。一切似乎都很好,日志输出正是我所期望的。 但是,INVOKESPECIAL指令似乎存在问题。我在这里面对一个丑陋的VerifyError,所以我想我对待堆栈的方式肯定有问题。

Exception in thread "main" java.lang.VerifyError: (class: net/petafuel/qualicore/examples/ThreadStarter, method: main signature: ([Ljava/lang/String;)V) Expecting to find object/array on stack
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:171)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:113)

使用“-noverify”启动测试类会使VerifyError消失。一切似乎都很完美,我得到了所需的输出。我可以这样离开,但实际上整个问题都让我痛苦,让我睡得很糟糕; - )

如果我的理解是正确的,那么像“new Thread()”这样的陈述将变为

NEW java/lang/Thread
DUP
INVOKESPECIAL <init> 
字节码中的

。在调用构造函数之前,新创建的对象仍然未初始化,这是一个问题吗?

我不明白为什么代码工作但JVM在验证期间抱怨。

甚至在检测后查看反编译代码对我没有帮助:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   ThreadStarter.java
public class ThreadStarter
{

    public ThreadStarter()
    {
        MethodLogger.writeToLoggerTest(null);
    }

    public static void main(String args[])
    {
        JVM INSTR new #2   <Class ThreadStarter$1>;
        JVM INSTR dup ;
        "Hugo";
        Object aobj[] = new Object[2];
        aobj;
        JVM INSTR swap ;
        1;
        JVM INSTR swap ;
        JVM INSTR aastore ;
        aobj;
        JVM INSTR swap ;
        0;
        JVM INSTR swap ;
        JVM INSTR aastore ;
        ((_cls1)aobj[0])._cls1((String)aobj[1]);
        MethodLogger.writeToLoggerTest(aobj[0]);
        Thread thread;
        thread;
        thread;
        Object aobj1[] = new Object[1];
        aobj1;
        JVM INSTR swap ;
        0;
        JVM INSTR swap ;
        JVM INSTR aastore ;
        ((Thread)aobj1[0]).start();
        MethodLogger.writeToLoggerTest(aobj1[0]);
        return;
    }
}

其他一些信息: 我正在使用IntelliJ IDEA 10.5.4开发并使用jdk1.6.0_39。

最后,我希望有人能帮助我获得必要的见解。提前谢谢!

2 个答案:

答案 0 :(得分:4)

我知道有两个原因可能会在INVOKESPECIAL出现错误时产生此错误:

  1. 您正在尝试在无法验证为未初始化的引用上调用构造函数(INVOKESPECIAL 的第一个参数必须是未初始化的引用。)

    < / LI>
  2. 您正在尝试在需要初始化引用的地方传递未初始化的引用(作为方法调用的参数,AASTORE操作等)。

  3. 粗略地看一下代码,可能会在方法参数数组中存储未初始化的引用。

答案 1 :(得分:2)

再次感谢Mike和ruediste的评论。

迈克是对的: 我的问题是我尝试将引用作为方法调用参数传递,当它由NEW创建之后但在其构造函数被调用之前。 JVM规范明确指出禁止这样的行为:“验证者在初始化之前拒绝使用新对象的代码[...]”(参见http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.10.2.4

但是,创建和初始化新对象的指令序列将我想要的对象留在操作数堆栈的顶部,可以很容易地获得它:)

最后,我为INVOKESPECIAL处理炸毁了我的代码:

if (opcode == INVOKESPECIAL) {
    // Invoke constructors and private methods

    // Ignore initialization of java/lang/Object
    if (name.equals("<init>") && owner.equals("java/lang/Object")) {
        super.visitMethodInsn(opcode, owner, name, desc);
        return;
    }

    if (methodName.equals("<clinit>")) {

        if (name.equals("<clinit>")) {
            // call to a static initializer from within a static initializer
            // there is no object reference!
            super.visitMethodInsn(opcode, owner, name, desc);
        } else if (name.equals("<init>")) {
            // call to a constructor from within a static initializer
            super.visitMethodInsn(opcode, owner, name, desc);
            // object reference is initialized and on stack now -> obtain it via DUP
        } else {
            // call to a private method from within a static initializer
            // no this-reference in static initializer!
            super.visitMethodInsn(opcode, owner, name, desc);
        }

    } else if (methodName.equals("<init>")) {

        if (name.equals("<clinit>")) {
            // call to a static initializer from within a constructor
            // there is no object reference!
            super.visitMethodInsn(opcode, owner, name, desc);
        } else if (name.equals("<init>")) {
            // call to a constructor from within a constructor
            super.visitMethodInsn(opcode, owner, name, desc);
            // if this constructor call is not an invocation of the super constructor: obtain object reference via DUP
        } else {
            // call to a private method from within a constructor
            // object reference is the this-reference (at local variable 0)
            super.visitMethodInsn(opcode, owner, name, desc);
        }

    } else {

        if (name.equals("<clinit>")) {
            // call to a static initializer from within some method
            // there is no object reference!
            super.visitMethodInsn(opcode, owner, name, desc);
        } else if (name.equals("<init>")) {
            // call to a constructor from within some method
            super.visitMethodInsn(opcode, owner, name, desc);
            // obtain object reference via DUP
        } else {
            // call to a private method from within some method
            // if the private method is called within some NON-STATIC method: object reference is the this-reference (at local variable 0)
            // if the private method is called within some NON-STATIC method: there is no object reference!
            super.visitMethodInsn(opcode, owner, name, desc);
        }
    }
}

这可能会帮助那些尝试做类似事情的人:)