What I want to do is to record the event that a Throwable is thrown out of a method. I wrote the following simple code and didn't use COMPUTE_FRAME and COMPUTE_MAX deliberately to get myself familiar with the concepts of stack map frame, operand stack, and locals. I only insert three stackmap frame by instrumentation: after the tryEnd
label, after the catchStart
label, and after the catchEnd
(In my class MyMethodVisitor
, the code is sho).
When I tried my javaagent in the testing process of joda-time, it crashed with the following message:
[ERROR] There was an error in the forked process
[ERROR] Stack map does not match the one at exception handler 9
[ERROR] Exception Details:
[ERROR] Location:
[ERROR] org/joda/time/TestAllPackages.<init>(Ljava/lang/String;)V @9: ldc
[ERROR] Reason:
[ERROR] Type 'org/joda/time/TestAllPackages' (current frame, locals[0]) is not assignable to uninitializedThis (stack map, locals[0])
[ERROR] Current Frame:
[ERROR] bci: @2
[ERROR] flags: { flagThisUninit }
[ERROR] locals: { 'org/joda/time/TestAllPackages', 'java/lang/String' }
[ERROR] stack: { 'java/lang/Throwable' }
[ERROR] Stackmap Frame:
[ERROR] bci: @9
[ERROR] flags: { flagThisUninit }
[ERROR] locals: { uninitializedThis, 'java/lang/String' }
[ERROR] stack: { 'java/lang/Throwable' }
[ERROR] Bytecode:
[ERROR] 0x0000000: 2a2b b700 01b1 a700 0912 57b8 005c bfb1
[ERROR] 0x0000010:
[ERROR] Exception Handler Table:
[ERROR] bci [0, 6] => handler: 9
[ERROR] Stackmap Table:
[ERROR] same_frame(@6)
[ERROR] same_locals_1_stack_item_frame(@9,Object[#85])
[ERROR] same_frame(@15)
Obviously, it must be the problem when I inserting the stackmap frame. But I got confused:
- What is the exact meaning and the difference of
Current Frame
andStackmap Frame
? - Why there is a
uninitializedThis
in the stackmap frame at @9? To my understanding, a object is alwaysuninitializedThis
until the constructor call is finished, am I right? - I think my instrumentation is correct because
org/joda/time/TestAllPackages
is the type ofthis
. How to avoid the inconsistency betweenorg/joda/time/TestAllPackages
anduninitializedThis
?
When I looked into the bytecode, it looks like:
public org.joda.time.TestAllPackages(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=7, locals=2, args_size=2
0: aload_0
1: aload_1
2: invokespecial #1 // Method junit/framework/TestCase."<init>":(Ljava/lang/String;)V
5: return
6: goto 15
9: ldc #87 // String org/joda/time/TestAllPackages#<init>#(Ljava/lang/String;)V
11: invokestatic #92 // Method MyRecorder.exception_caught:(Ljava/lang/String;)V
14: athrow
15: return
Exception table:
from to target type
0 6 9 Class java/lang/Throwable
StackMapTable: number_of_entries = 3
frame_type = 6 /* same */
frame_type = 66 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 5 /* same */
LineNumberTable:
line 31: 0
line 32: 5
BTW, my simplified instrumentation code is like:
public class PreMain {
public static void premain(String args, Instrumentation inst){
inst.addTransformer(new MyTransformer());
}
}
public class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
byte[] result = classfileBuffer;
try{
if (className == null || shouldExcludeClass(className)) return result;
ClassReader cr = new ClassReader(classfileBuffer);
// I don't use COMPUTE_FRAME and COMPUTE_MAX deliberately
ClassWriter cw = new ClassWriter(cr, 0);
ClassVisitor cv = new MyClassVistor(cw, className, loader);
cr.accept(cv, 0);
result = cw.toByteArray();
} catch (Throwable t){
t.printStackTrace();
}
return result;
}
}
public class MyClassVistor extends ClassVisitor {
...
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
if (!isNative && !isEnum && !isAbstract && !"<clinit>".equals(name)){
mv = new MyMethodVisitor(mv, name, access, desc, slashClassName, isStatic, isPublic);
}
return mv;
}
}
public class MyMethodVisitor extends MethodVisitor {
private Label tryStart = new Label();
private Label tryEnd = new Label();
private Label catchStart = new Label();
private Label catchEnd = new Label();
public void visitCode() {
mv.visitCode();
mv.visitTryCatchBlock(tryStart, tryEnd, catchStart, "java/lang/Throwable");
mv.visitLabel(tryStart);
}
@Override
public void visitEnd() {
mv.visitLabel(tryEnd);
mv.visitFrame(F_SAME, 0, null, 0, null); /* This line takes me more than 6 hours to figure out. Why this line can't be omitted? */
mv.visitJumpInsn(GOTO, catchEnd);
mv.visitLabel(catchStart);
// exception caught
mv.visitFrame(F_SAME1, 0, null, 1, new Object[] {"java/lang/Throwable"}); /* add stackmap frame after jump target */
mv.visitLdcInsn(this.selfMethodId);
mv.visitMethodInsn(INVOKESTATIC, MyRecorder.SLASH_CLASS_NAME, MyRecorder.EXCEPTION_CAUGHT,
"(Ljava/lang/String;)V", false);
mv.visitInsn(ATHROW);
mv.visitLabel(catchEnd);
mv.visitFrame(F_SAME, 0, null, 0, null); /* add stackmap frame after jump target */
// Make up a return statement
switch (Type.getReturnType(selfDesc).getSort()){
case BYTE:
case CHAR:
case SHORT:
case BOOLEAN:
case INT:
mv.visitLdcInsn(0);
mv.visitInsn(IRETURN);
break;
case LONG:
mv.visitLdcInsn(0L);
mv.visitInsn(LRETURN);
break;
case FLOAT:
mv.visitLdcInsn(0f);
mv.visitInsn(FRETURN);
break;
case DOUBLE:
mv.visitLdcInsn(0.0);
mv.visitInsn(DRETURN);
break;
case OBJECT:
mv.visitInsn(ACONST_NULL);
mv.visitInsn(ARETURN);
break;
case VOID:
mv.visitInsn(RETURN);
break;
}
super.visitEnd();
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
// +5 because other logic need more space on operand stack
super.visitMaxs(maxStack + 5, maxLocals);
}
}