I'm quite new in bytecode injection. Until now, I was able to get everything what I wanted by exhaustive research and painful trial and error :-) But I seem to have reached my limits with the currently pursued objective. So, here it is: my very first stackoverflow question!
My aim is to trace the object references of method invocations via a java agent. I am using the ASM 4.0 library and have implemented an AdviceAdapter. My overriden visitMethodInsn()-method looks like this:
/**
* 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;
}
In short, I am trying to save everything which is on the stack before the INVOKESOMETHING operation is executed into a local variable. In order to enable the execution of the method operation I have to put the whole stuff back to the stack. Afterwards I assume that the reference of the called object is the first entry in my local array.
In the following is one of my test classes. This one is pretty simple: it is just starting another thread:
/**
* 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();
}
}
Concerning INVOKEVIRTUAL, INVOKEINTERFACE and INVOKESTATIC I did not face any issues. Everything seems fine and the logging output is exactly what I expect. However, there seems to be a problem with the INVOKESPECIAL instruction. I'm facing an ugly VerifyError here so I guess there must be something wrong in the way that I treat the stack.
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)
Starting the test class with the "-noverify" makes the VerifyError disappear. Everything seems to work just perfectly and I get the desired output. I could just leave it like that but actually the whole issue is causing me pain and lets me sleep very bad ;-)
If my understanding is correct, some statement like "new Thread()" turns to be
NEW java/lang/Thread
DUP
INVOKESPECIAL <init>
in bytecode. Could it be a problem that the newly created object is still uninitialzed before the constructor is invoked?
I do not understand why the code is working but the JVM is complaining during verification.
Even looking at the decompiled code after instrumentation does not help me:
// 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;
}
}
Some additional information: I am developping with IntelliJ IDEA 10.5.4 and using jdk1.6.0_39.
Finally, I hope that somebody here can help me to get the necessary insight. Thanks in advance!
Thanks again to Mike and ruediste for their comments.
Mike was right: My problem was that I tried passing a reference as a method call argument right after it was created by NEW but before its constructor has been called. The JVM specification clearly states that such a behavior is forbidden: "The verifier rejects code that uses the new object before it has been initialized [...]" (see http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.10.2.4)
However, the instruction sequence of creating and initializing a new object leaves my desired object on top of the operand stack where it can easily be obtained :)
In the end, I blew up my code bit for the INVOKESPECIAL handling:
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);
}
}
}
Possibly this helps someone who's trying to do similar stuff :)