3

我在字節碼注入方面很新穎。直到現在,我能夠通過詳盡的研究和痛苦的試驗和錯誤獲得我想要的一切:-)但是我似乎已經達到了我目前所追求的目標。所以,這裏是:我的第一個stackoverflow問題!帶有ASM的Java字節碼檢測:INVOKESPECIAL指令處的代碼注入驗證錯誤

我的目標是通過一個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消失。一切似乎都完美地工作,我得到所需的輸出。我可以把它這樣,但實際上整個問題是導致我的痛苦,讓我睡得很不好;-)

如果我的理解是正確的,有些像聲明中「新的Thread()」原來是

NEW java/lang/Thread 
DUP 
INVOKESPECIAL <init> 

in bytecode。在構造函數被調用之前,新創建的對象是否仍然未初始化會是一個問題嗎?

我不明白爲什麼代碼工作,但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。

最後,我希望這裏有人能幫助我獲得必要的洞察力。提前致謝!

+1

據我所知,關於傳遞單元化實例有一些限制。 (也就是用「new」創建的對象,但尚未調用構造函數的對象),我想這會導致您的驗證錯誤。有關更詳細的驗證輸出,請嘗試使用內置於ASM中的驗證程序。它提供了有關錯誤的更多信息 – ruediste 2013-05-06 07:34:25

+0

感謝您的回覆以及使用ASM驗證程序的提示。雖然我沒有得到任何關於使用ASM驗證器的問題的任何輸出,但我會考慮它可能面臨的任何即將出現的問題。 – 2013-05-07 14:48:43

回答

2

再次感謝Mike和ruediste的評論。

邁克是對的: 我的問題是,我嘗試傳遞一個引用作爲一個方法調用參數剛剛創建之後,但在它的構造函數被調用之前。 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); 
     } 
    } 
} 

可能這有助於有人試圖做類似的東西:)

4

有兩個原因,我知道可能產生這個錯誤時INVOKESPECIAL是concerend:

  1. 您正試圖調用上不能被驗證爲未初始化的參考構造函數(第一個參數INVOKESPECIAL必須是未初始化的參考)。

  2. 您正在嘗試將某個未初始化的引用傳遞給需要初始化引用的地方(作爲方法調用的參數,AASTORE操作等)。

代碼的粗略地看一眼意味着你可以存儲方法參數陣列的未初始化的參考。

+0

感謝您的回答。重新考慮我的代碼,我相信VerifyError的原因確實是我嘗試將未初始化的引用存儲到本地數組中。 – 2013-05-07 14:55:48