Инструментарий байт-кода Java с ASM: 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. Я столкнулся с уродливым проверяющим здесь, поэтому я думаю, что должно быть что-то неправильное в том, как я отношусь к стеку.
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 10.5.4 и используя jdk1.6.0_39.

Наконец, я надеюсь, что кто-нибудь из присутствующих здесь сможет помочь мне получить необходимое понимание. Заранее спасибо!
2 3

2 ответа:

Я знаю две причины, которые могут привести к этой ошибке, когда INVOKESPECIAL является concerend:

  1. Вы пытаетесь вызвать конструктор по ссылке, которая не может быть проверена как неинициализированная (первый аргумент для INVOKESPECIAL должен быть неинициализированной ссылкой).

  2. Вы пытаетесь передать неинициализированную ссылку туда, где ожидается инициализированная ссылка (в качестве аргумента для вызова метода, операции AASTORE, и т.д.).

Беглый взгляд на ваш код предполагает, что вы, возможно, храните неинициализированную ссылку в массиве параметров метода.

Еще раз спасибо майку и руэдисту за их комментарии.

Майк был прав.: Моя проблема заключалась в том, что я попытался передать ссылку в качестве аргумента вызова метода сразу после его создания NEW, но до того, как был вызван его конструктор. В спецификации JVM четко указано, что такое поведение запрещено: "верификатор отклоняет код, который использует новый объект до его инициализации [...]" (видеть http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.10.2.4 )

Однако последовательность команд создания и инициализации нового объекта оставляет мой искомый объект на вершине стека операндов, где его можно легко получить:)

В конце концов, я взорвал свой бит кода для специальной обработки вызова:

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

Возможно, это поможет кому-то, кто пытается делать подобные вещи:)