The object instantiation is separated into two instructions. This can make it difficult to work with object instantiation. It was actually the biggest problem in multiple of my projects with ASM. The simple expression new Integer(0) might compile to the following code.
new java/lang/Integer ═╤═
dup ═╪═══╤═
iconst_0 │ │ ═╤═
invokespecial java/lang/Integer.<init>(I)V │ ═╧═══╧═
↓
(The ASCII art on the right shall represent the lifetime of values on the stack. The first column represents the first item on the stack, the second column the second item on the stack, and so on.)
The reference created with new cannot be used until it is initialized by invokespecial. Therefore, you cannot put you instrumentation directly after new. In the example above, you could put the instrumentation behind invokespecial. However, you need to distinguish it from calls to the super constructor. A statement like super(0) might compile to the following code.
aload_0 ═╤═
iconst_0 │ ═╤═
invokespecial {super class}.<init>(I)V ═╧═══╧═
This two examples might cover the two most common cases where <init> is called. If you don't do anything with the value created by new Integer(0), a compiler could also decide to skip the dup instruction. I'm not sure if they actually do it. Anyway, the first example wourld become the following code.
new java/lang/Integer ═╤═
iconst_0 │ ═╤═
invokespecial java/lang/Integer.<init>(I)V ═╧═══╧═
This can still be handled by injecting dup behind new, and invokestatic behind invokespecial.
Anyway, if you want to handle even more exotic cases, the bytecode could also contain instructions like dup_x1 or dup_x2. GeneratorAdapter.box(Type) actually generates such code. Some "lazy" bytecode manipulators could also cause new instructions where the instance is never initialized before it is removed with pop. As you can see, working with object instantiations in bytecode can take a lot of work. However, depending on your situation, you might not need to suport all this cases. Unfortunately, I don't have the experiance to estimate which cases you need to support.
Here is an example that doesn't handle dup_x1 and similar instructions. Therefore, it would not work with code generated by GeneratorAdapter.box(Type). Beside that, it seems to work reasonable well in my tests. It uses AnalyzerAdapter to get some information about the operand stack.
public final class InstrumentationMethodVisitor extends AnalyzerAdapter {
public InstrumentationMethodVisitor(
String owner, int access, String name, String descriptor,
MethodVisitor methodVisitor) {
super(ASM8, owner, access, name, descriptor, methodVisitor);
}
@Override
public void visitTypeInsn(int opcode, String type) {
super.visitTypeInsn(opcode, type);
if (opcode == NEW && stack != null) {
mv.visitInsn(DUP);
}
}
@Override
public void visitInsn(int opcode) {
if (opcode == POP && stack != null && stack.get(stack.size() - 1) instanceof Label) {
mv.visitInsn(POP);
}
super.visitInsn(opcode);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
if (opcode == INVOKESPECIAL && name.equals("<init>")) {
boolean hasRelatedNew = stack != null && stack.get(stack.size() - getAmountOfArgumentSlots(descriptor) - 1) instanceof Label;
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
if (hasRelatedNew) {
mv.visitMethodInsn(
INVOKESTATIC,
"org/myekstazi/agent/PurityRecorder",
"object_new",
"(Ljava/lang/Object;)V",
false);
}
}
else {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
private static int getAmountOfArgumentSlots(String descriptor) {
int slots = 0;
for (Type type : Type.getArgumentTypes(descriptor)) {
slots += type.getSize();
}
return slots;
}
}