package org.intrace.agent; import static org.objectweb.asm.Opcodes.INVOKESTATIC; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.intrace.output.AgentHelper; import org.intrace.output.trace.TraceHandler; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.MethodAdapter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; /** * ASM2 ClassWriter used to transform class files to instrument methods to add * calls into {@link AgentHelper}. */ public class InstrumentedClassWriter extends ClassWriter { private final String className; private final ClassAnalysis analysis; private final boolean threadClass; private final boolean shouldInstrument; private AgentSettings settings = null; /** * cTor * * @param xiClassName * @param xiReader * @param xiShouldInstrument * @param analysis */ public InstrumentedClassWriter(String xiClassName, ClassReader xiReader, ClassAnalysis xiAnalysis, boolean xiShouldInstrument, AgentSettings settings) { super(xiReader, COMPUTE_MAXS); className = xiClassName; analysis = xiAnalysis; shouldInstrument = xiShouldInstrument; threadClass = xiClassName.equals("java.lang.Thread"); this.settings = settings; } /** * Instrument a particular method. */ @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { if (settings.isVerboseMode()) { /** The following code provides invaluable debug for conjuring right syntax for method parameters. * http://hsqldb.org/doc/src/org/hsqldb/jdbc/JDBCConnection.html#isWrapperFor(java.lang.Class) [14:44:45.065]:[19]:access:1 [14:44:45.065]:[19]:name:isWrapperFor [14:44:45.065]:[19]:desc:(Ljava/lang/Class;)Z <<<<<==== complicate syntax for method specification [14:44:45.065]:[19]:signature:(Ljava/lang/Class<*>;)Z http://hsqldb.org/doc/src/org/hsqldb/jdbc/JDBCConnection.html#prepareStatement(java.lang.String, java.lang.String[]) [14:44:45.068]:[19]:access:33 [14:44:45.068]:[19]:name:prepareStatement [14:44:45.069]:[19]:desc:(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement; <<<<<==== complicated syntax for method specification [14:44:45.069]:[19]:signature:null */ StringBuilder sb = new StringBuilder(); sb.append(this.className); sb.append(InstrCriteria.CLASS_METHOD_DELIMITER); sb.append(name); sb.append(desc); TraceHandler.INSTANCE.writeTraceOutput("DEBUG: method signature: " + sb.toString()); } MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); // If this class/method is not excluded, but then consider including it. // If all methods are allowed (by not specifying any methods) or this specific method is defined, then instrument it. if (this.settings.getClassesToExclude()==null ||!this.settings.getClassesToExclude().thisMethodSpecified(this.className, name, desc)) { if (this.settings.getClassesToInclude().thisMethodSpecified(this.className, name, desc) ) { // Extract analysis results for this method Set<Integer> branchTraceLines = analysis.methodReverseGOTOLines.get(name + desc); Integer entryLine = analysis.methodEntryLines.get(name + desc); if (!threadClass || !name.equals("getUncaughtExceptionHandler")) { //System.out.println("Instrumenting class [" + this.className + "] method [" + name + "] args [" + desc + "]"); mv = new InstrumentedMethodWriter(mv, access, name, desc, branchTraceLines, entryLine); } } } // Transform the method return mv; } /** * ASM2 MethodVisitor used to instrument methods. */ private class InstrumentedMethodWriter extends MethodAdapter { private static final String HELPER_CLASS = "org/intrace/output/AgentHelper"; private static final String CRITICAL_BLOCK = "INSTRU_CRITICAL_BLOCK"; // Final method fields private final String methodName; private final String methodDescriptor; private final int methodAccess; // Analysis data private final Set<Integer> reverseGOTOLines; private final Integer entryLine; private final Map<Label, Integer> labelLineNos = new HashMap<Label, Integer>(); private final Set<Label> traceLabels = new HashSet<Label>(); private final Set<Label> exceptionHandlerLabels = new HashSet<Label>(); // State private boolean writeTraceLine = false; private CTorEntryState ctorEntryState = CTorEntryState.NORMALMETHOD; private int lineNumber = -1; private int numBranchesOnLine = 0; private TernaryState ternState = TernaryState.BASE; private Label threadLabel; /** * cTor * * @param xiMethodVisitor * @param access * @param xiClassName * @param xiMethodName * @param xiDesc * @param xiBranchTraceLines * @param entryLine */ public InstrumentedMethodWriter(MethodVisitor xiMethodVisitor, int access, String xiMethodName, String xiDesc, Set<Integer> xiBranchTraceLines, Integer xiEntryLine) { super(xiMethodVisitor); methodAccess = access; methodName = xiMethodName; methodDescriptor = xiDesc; reverseGOTOLines = xiBranchTraceLines; entryLine = xiEntryLine; if (methodName.equals("<init>")) { ctorEntryState = CTorEntryState.ISCTOR; } } /** * Add the special preable for the Thread.setUncaughtExceptionHandler(...) */ private void addThreadSetUCEHPreamble() { Label l0 = new Label(); mv.visitLabel(l0); mv.visitVarInsn(Opcodes.ALOAD, 1); mv.visitFieldInsn(Opcodes.GETSTATIC, HELPER_CLASS, CRITICAL_BLOCK, "L" + HELPER_CLASS + "$CriticalBlock;"); Label l1 = new Label(); Label l2 = new Label(); Label l3 = new Label(); mv.visitJumpInsn(Opcodes.IF_ACMPEQ, l3); mv.visitLabel(l2); mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitFieldInsn(Opcodes.GETFIELD, "java/lang/Thread", "uncaughtExceptionHandler", "Ljava/lang/Thread$UncaughtExceptionHandler;"); mv.visitFieldInsn(Opcodes.GETSTATIC, HELPER_CLASS, CRITICAL_BLOCK, "L" + HELPER_CLASS + "$CriticalBlock;"); mv.visitJumpInsn(Opcodes.IF_ACMPNE, l1); mv.visitLabel(l3); mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitVarInsn(Opcodes.ALOAD, 1); mv.visitFieldInsn(Opcodes.PUTFIELD, "java/lang/Thread", "uncaughtExceptionHandler", "Ljava/lang/Thread$UncaughtExceptionHandler;"); threadLabel = new Label(); mv.visitJumpInsn(Opcodes.GOTO, threadLabel); mv.visitLabel(l1); mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); } /** * Initial entry point - generate ENTRY call. */ @Override public void visitCode() { if (threadClass && methodName.equals("setUncaughtExceptionHandler")) { addThreadSetUCEHPreamble(); } if (ctorEntryState != CTorEntryState.ISCTOR) { addEntryCalls(); } // For Constructors we add the entry calls after the first invokeSpecial // which calls into the superclass constructor. super.visitCode(); } private void addEntryCalls() { if (!shouldInstrument) return; generateCallToAgentHelper(InstrumentationType.ENTER, ((entryLine != null ? entryLine : -1))); traceMethodArgs(); } /** * Pass the args of this method out to the {@link AgentHelper} */ private void traceMethodArgs() { Type[] argTypes = Type.getArgumentTypes(methodDescriptor); boolean isStaticAccess = ((methodAccess & Opcodes.ACC_STATIC) > 0); int offset = (isStaticAccess ? 0 : 1); List<String> argNames = analysis.methodArgNames.get(methodName + methodDescriptor); for (int ii = 0; ii < argTypes.length; ii++) { String typeDescriptor = argTypes[ii].getDescriptor(); int varslot = ii + offset; int opcode = Opcodes.ILOAD; if (argTypes[ii].getSort() == Type.OBJECT) { typeDescriptor = "Ljava/lang/Object;"; opcode = Opcodes.ALOAD; } else if ((argTypes[ii].getSort() == Type.ARRAY) && (argTypes[ii].getDescriptor().startsWith("[["))) { // All multidimensional arrays are handled by the object array // function typeDescriptor = "[Ljava/lang/Object;"; } else if ((argTypes[ii].getSort() == Type.ARRAY) && (argTypes[ii].getDescriptor().startsWith("[L"))) { // All object arrays are cast to the object supertype typeDescriptor = "[Ljava/lang/Object;"; } else if (argTypes[ii].getSort() == Type.LONG) { opcode = Opcodes.LLOAD; offset++; } else if (argTypes[ii].getSort() == Type.FLOAT) { opcode = Opcodes.FLOAD; } else if (argTypes[ii].getSort() == Type.DOUBLE) { opcode = Opcodes.DLOAD; offset++; } if (argTypes[ii].getSort() == Type.ARRAY) { opcode = Opcodes.ALOAD; } if ((argNames != null) && (argNames.size() > ii)) { mv.visitLdcInsn("Arg (" + argNames.get(ii) + ")"); } else { mv.visitLdcInsn("Arg"); } mv.visitLdcInsn(className); mv.visitLdcInsn(methodName); mv.visitVarInsn(opcode, varslot); mv.visitMethodInsn(INVOKESTATIC, HELPER_CLASS, "val", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;" + typeDescriptor + ")V"); } } /** * Guard against a label which hasn't been resolved. This happens sometimes, * I assume due to a bug in ASM. */ @Override public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) { try { end.getOffset(); } catch (IllegalStateException ex) { mv.visitLabel(end); } super.visitLocalVariable(name, desc, signature, start, end, index); } /** * Write branch trace. */ @Override public void visitLineNumber(int xiLineNumber, Label label) { lineNumber = xiLineNumber; labelLineNos.put(label, xiLineNumber); if (writeTraceLine || reverseGOTOLines.contains(xiLineNumber) || traceLabels.contains(label)) { // This check excludes ternary statements where we can't add trace if (numBranchesOnLine < 2) { if (ctorEntryState == CTorEntryState.SEEN_SPECIAL) { addEntryCalls(); ctorEntryState = CTorEntryState.ENTRY_WRITTEN; } else { if (shouldInstrument && exceptionHandlerLabels.contains(label)) { // Top of the stack contains an exception - generate code to trace // it // Duplicate the exception mv.visitInsn(Opcodes.DUP); // Load args mv.visitLdcInsn("Caught"); mv.visitInsn(Opcodes.SWAP); mv.visitLdcInsn(className); mv.visitInsn(Opcodes.SWAP); mv.visitLdcInsn(methodName); mv.visitInsn(Opcodes.SWAP); mv.visitLdcInsn(lineNumber); mv.visitInsn(Opcodes.SWAP); // Generate call to trace exception mv .visitMethodInsn( INVOKESTATIC, HELPER_CLASS, "val", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Throwable;)V"); } else if (shouldInstrument) { generateCallToAgentHelper(InstrumentationType.BRANCH, lineNumber); } } writeTraceLine = false; } else { writeTraceLine = false; } } numBranchesOnLine = 0; super.visitLineNumber(xiLineNumber, label); } /** * Handle return instructions by writing exit trace. */ @Override public void visitInsn(int xiOpCode) { if ((ternState == TernaryState.BASE) && (xiOpCode == Opcodes.DUP)) { ternState = TernaryState.SEEN_DUP; } else if ((ternState == TernaryState.SEEN_BRANCH) && (xiOpCode == Opcodes.POP)) { ternState = TernaryState.BASE; writeTraceLine = false; } else { ternState = TernaryState.BASE; } if (!shouldInstrument) { // Don't instrument - ignore } else if (xiOpCode == Opcodes.RETURN) { // Ensure that cTor entry call gets written even if the cTor is // implicit and therefore has only a single line number. if ((ctorEntryState == CTorEntryState.SEEN_SPECIAL) || ("java.lang.Object".equals(className) && (ctorEntryState == CTorEntryState.ISCTOR))) { addEntryCalls(); ctorEntryState = CTorEntryState.ENTRY_WRITTEN; } generateCallToAgentHelper(InstrumentationType.EXIT, lineNumber); } else if ((xiOpCode == Opcodes.IRETURN) || (xiOpCode == Opcodes.FRETURN) || (xiOpCode == Opcodes.ARETURN)) { // Duplicate the return value mv.visitInsn(Opcodes.DUP); // Push the callname and methodname while keeping the return value# // at the top of the stack mv.visitLdcInsn("Return"); mv.visitInsn(Opcodes.SWAP); mv.visitLdcInsn(className); mv.visitInsn(Opcodes.SWAP); mv.visitLdcInsn(methodName); mv.visitInsn(Opcodes.SWAP); if (xiOpCode == Opcodes.IRETURN) { mv .visitMethodInsn(INVOKESTATIC, HELPER_CLASS, "val", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V"); } else if (xiOpCode == Opcodes.FRETURN) { mv .visitMethodInsn(INVOKESTATIC, HELPER_CLASS, "val", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;F)V"); } else if (xiOpCode == Opcodes.ARETURN) { mv .visitMethodInsn(INVOKESTATIC, HELPER_CLASS, "val", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V"); } generateCallToAgentHelper(InstrumentationType.EXIT, lineNumber); } else if ((xiOpCode == Opcodes.LRETURN) || (xiOpCode == Opcodes.DRETURN)) { // Duplicate the return value mv.visitInsn(Opcodes.DUP2); // Push the callname and methodname while keeping the return value // at the top of the stack mv.visitLdcInsn("Return"); mv.visitInsn(Opcodes.DUP_X2); mv.visitInsn(Opcodes.POP); mv.visitLdcInsn(className); mv.visitInsn(Opcodes.DUP_X2); mv.visitInsn(Opcodes.POP); mv.visitLdcInsn(methodName); mv.visitInsn(Opcodes.DUP_X2); mv.visitInsn(Opcodes.POP); if (xiOpCode == Opcodes.LRETURN) { mv .visitMethodInsn(INVOKESTATIC, HELPER_CLASS, "val", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;J)V"); } else if (xiOpCode == Opcodes.DRETURN) { mv .visitMethodInsn(INVOKESTATIC, HELPER_CLASS, "val", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;D)V"); } generateCallToAgentHelper(InstrumentationType.EXIT, lineNumber); } else if (xiOpCode == Opcodes.ATHROW) { // Top of the stack contains an exception - generate code to trace // it - Duplicate the exception mv.visitInsn(Opcodes.DUP); // Load args mv.visitLdcInsn("Throw"); mv.visitInsn(Opcodes.SWAP); mv.visitLdcInsn(className); mv.visitInsn(Opcodes.SWAP); mv.visitLdcInsn(methodName); mv.visitInsn(Opcodes.SWAP); mv.visitLdcInsn(lineNumber); mv.visitInsn(Opcodes.SWAP); // Generate call to trace exception mv .visitMethodInsn( INVOKESTATIC, HELPER_CLASS, "val", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Throwable;)V"); // Also write exit trace generateCallToAgentHelper(InstrumentationType.EXIT, lineNumber); } if (xiOpCode == Opcodes.RETURN) { if (threadClass && methodName.equals("setUncaughtExceptionHandler")) { mv.visitLabel(threadLabel); mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); } } super.visitInsn(xiOpCode); } /** * Handle cTor case where we write entry trace later. */ @Override public void visitMethodInsn(int xiOpCode, String owner, String name, String desc) { if ((ctorEntryState == CTorEntryState.ISCTOR) && (xiOpCode == Opcodes.INVOKESPECIAL)) { ctorEntryState = CTorEntryState.SEEN_SPECIAL; writeTraceLine = true; } super.visitMethodInsn(xiOpCode, owner, name, desc); } /** * For all forward branch instructions we trace the next line we see as we * know it is optional. * <p> * We don't mark the target label as we don't know whether it is optional * code. */ @Override public void visitJumpInsn(int xiOpCode, Label xiBranchLabel) { if (ternState == TernaryState.SEEN_DUP) { ternState = TernaryState.SEEN_BRANCH; } else { ternState = TernaryState.BASE; } numBranchesOnLine++; Integer lineNo = labelLineNos.get(xiBranchLabel); if (lineNo == null) { // This is a forward jump writeTraceLine = true; } super.visitJumpInsn(xiOpCode, xiBranchLabel); } /** * Try catch block - mark all labels for tracing */ @Override public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { // Null type means that handler is a finally block which will always be // executed if (type != null) { traceLabels.add(handler); exceptionHandlerLabels.add(handler); } super.visitTryCatchBlock(start, end, handler, type); } /** * Table switch block - mark all labels for tracing */ @Override public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { traceLabels.add(dflt); for (Label label : labels) { traceLabels.add(label); } super.visitTableSwitchInsn(min, max, dflt, labels); } /** * Lookup switch block - mark all labels for tracing */ @Override public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { traceLabels.add(dflt); for (Label label : labels) { traceLabels.add(label); } super.visitLookupSwitchInsn(dflt, keys, labels); } /** * Generate an ENTER/BRANCH/EXIT instrumentation call. * * @param traceType * @param lineNumber */ private void generateCallToAgentHelper(InstrumentationType traceType, int lineNumber) { mv.visitLdcInsn(className); mv.visitLdcInsn(methodName); mv.visitLdcInsn(lineNumber); switch (traceType) { case ENTER: { mv.visitMethodInsn(INVOKESTATIC, HELPER_CLASS, "enter", "(Ljava/lang/String;Ljava/lang/String;I)V"); break; } case BRANCH: { mv.visitMethodInsn(INVOKESTATIC, HELPER_CLASS, "branch", "(Ljava/lang/String;Ljava/lang/String;I)V"); break; } case EXIT: { mv.visitMethodInsn(INVOKESTATIC, HELPER_CLASS, "exit", "(Ljava/lang/String;Ljava/lang/String;I)V"); break; } } } } /** * Three way enum to signal the difference between ENTER/BRANCH/EXIT trace. */ private enum InstrumentationType { ENTER, BRANCH, EXIT; } /** * Type used to suppress trace in the case where the following instruction * sequence is seen. * <ul> * <li>... * <li>DUP * <li>BRANCH * <li>POP * <li>... * </ul> * This sequence is used in ternary if statements of the form (cond(x) ? x : * Y). This agent currently does not support adding trace into these * constructs. * <p> * MCHR: This should either be fixed or strengthened to ensure it covers all * ternary statements. Does numBranchesOnLine already strengthen this? */ private enum TernaryState { BASE, SEEN_DUP, SEEN_BRANCH /* SEEN_POP */ } /** * cTor related state. * <p> * Normal methods are assigned NORMALMETHOD. * <p> * cTors are assigned ISCTOR and then SEEN_SPECIAL once the superclass * constructor call is issued. This allows us to avoid writing entry trace * before the superclass constructor call. * <p> * ENTRY_WRITTEN is set if a line number is processed such that we write entry * trace. Otherwise we know to write entry trace before exit trace when we * visit a return instruction. This is mostly only useful for processing * implicit constructors. * <p> * MCHR: Ensure this all works in the case of a constructor calling through to * another constructor in the same class. */ private enum CTorEntryState { NORMALMETHOD, ISCTOR, SEEN_SPECIAL, ENTRY_WRITTEN } }