pku.sei.checkedcoverage.tracer.instrumentation.Transformer.java Source code

Java tutorial

Introduction

Here is the source code for pku.sei.checkedcoverage.tracer.instrumentation.Transformer.java

Source

/** License information:
 *    Component: checkedcoverage-tracer
 *    Package:   pku.sei.checkedcoverage.tracer.instrumentation
 *    Class:     Transformer
 *    Filename:  checkedcoverage-tracer/src/main/java/de/unisb/cs/st/checkedcoverage/tracer/instrumentation/Transformer.java
 *
 * This file is part of the checkedcoverage tool, developed by Clemens Hammacher at Saarland University.
 * See http://www.st.cs.uni-saarland.de/checkedcoverage/ for more information.
 *
 * This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License.
 * To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/ or send a
 * letter to Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA.
 */
package pku.sei.checkedcoverage.tracer.instrumentation;

import java.io.PrintStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TryCatchBlockNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.BasicVerifier;
import org.objectweb.asm.tree.analysis.Frame;
import org.objectweb.asm.util.CheckClassAdapter;
import org.objectweb.asm.util.TraceMethodVisitor;

import pku.sei.checkedcoverage.common.classRepresentation.Field;
import pku.sei.checkedcoverage.common.classRepresentation.ReadClass;
import pku.sei.checkedcoverage.common.classRepresentation.instructions.AbstractInstruction;
import pku.sei.checkedcoverage.common.exceptions.TracerException;
import pku.sei.checkedcoverage.tracer.ThreadTracer;
import pku.sei.checkedcoverage.tracer.Tracer;

public class Transformer implements ClassFileTransformer {

    /**
     * The asm {@link ClassWriter} has an "error" (maybe feature) in the
     * method {@link #getCommonSuperClass(String, String)}, because it
     * only uses the classloader of the current class, not the system
     * class loader.
     *
     * @author Clemens Hammacher
     */
    public static final class FixedClassWriter extends ClassWriter {
        protected FixedClassWriter(final int flags) {
            super(flags);
        }

        @Override
        protected String getCommonSuperClass(final String type1, final String type2) {
            Class<?> c, d;
            try {
                c = Class.forName(type1.replace('/', '.'));
            } catch (final ClassNotFoundException e) {
                try {
                    c = ClassLoader.getSystemClassLoader().loadClass(type1.replace('/', '.'));
                } catch (final ClassNotFoundException e1) {
                    throw new RuntimeException(e1);
                }
            }
            try {
                d = Class.forName(type2.replace('/', '.'));
            } catch (final ClassNotFoundException e) {
                try {
                    d = ClassLoader.getSystemClassLoader().loadClass(type2.replace('/', '.'));
                } catch (final ClassNotFoundException e1) {
                    throw new RuntimeException(e1);
                }
            }
            if (c.isAssignableFrom(d)) {
                return type1;
            }
            if (d.isAssignableFrom(c)) {
                return type2;
            }
            if (c.isInterface() || d.isInterface()) {
                return "java/lang/Object";
            }
            do {
                c = c.getSuperclass();
            } while (!c.isAssignableFrom(d));
            return c.getName().replace('.', '/');
        }
    }

    private final String[] pauseTracingClasses = new String[] { "java.lang.ClassLoader",
            "sun.instrument.InstrumentationImpl" };

    private final Set<String> notRedefinedClasses;

    private static final boolean COMPUTE_FRAMES = false;
    private final Object transformationLock = new Object();

    private final AtomicLong totalTransformationTime = new AtomicLong(0);
    private final AtomicInteger totalTransformedClasses = new AtomicInteger(0);

    private final Instrumentation instrumentation;
    private final Tracer tracer;
    private final ConcurrentLinkedQueue<ReadClass> readClasses;

    public Transformer(final Tracer tracer, final Instrumentation instrumentation,
            final ConcurrentLinkedQueue<ReadClass> readClasses, final Set<String> notRedefinedClasses) {
        this.tracer = tracer;
        this.instrumentation = instrumentation;
        this.readClasses = readClasses;
        this.notRedefinedClasses = notRedefinedClasses;
    }

    public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined,
            final ProtectionDomain protectionDomain, final byte[] classfileBuffer) {

        final long startTime = System.nanoTime();

        ThreadTracer tt = null;
        boolean paused = false;
        try {
            if (this.tracer.tracingReady)
                return null;

            // disable tracing for the thread tracer of this thread
            tt = this.tracer.getThreadTracer();
            tt.pauseTracing();
            paused = true;

            final String javaClassName = Type.getObjectType(className).getClassName();
            if (isExcluded(javaClassName))
                return null;
            return transform0(className, javaClassName, classfileBuffer);
        } catch (TracerException e) {
            System.err.println("Error transforming class " + className + ": " + e.getMessage());
            return null;
        } catch (RuntimeException e) {
            System.err.println("Uncatched error while transforming class " + className + ":");
            e.printStackTrace(System.err);
            return null;
        } finally {
            if (this.tracer.debug) {
                // first build the string, then print it. otherwise the output may be interrupted
                // when new classes need to be loaded to format the output
                final long nanoSecs = System.nanoTime() - startTime;
                this.totalTransformationTime.addAndGet(nanoSecs);
                this.totalTransformedClasses.incrementAndGet();
                final String text = String.format((Locale) null, "Transforming %s took %.2f msec.%n", className,
                        1e-6 * nanoSecs);
                System.out.print(text);
            }
            if (paused && tt != null)
                tt.resumeTracing();
        }
    }

    private boolean isExcluded(final String javaClassName) {
        if (javaClassName.startsWith("pku.sei.checkedcoverage.tracer."))
            return true;
        if (javaClassName.startsWith("pku.sei.checkedcoverage.common."))
            return true;
        if (javaClassName.startsWith("de.hammacher.util."))
            return true;
        if (javaClassName.startsWith("pku.sei.sequitur"))
            return true;

        //////////////////////////////////////////////////////////////////
        // NOTE: these will be cleaned up when the system runs stable
        //////////////////////////////////////////////////////////////////

        if (javaClassName.equals("java.lang.System"))
            return true;
        /*
        if (javaClassName.equals("java.lang.VerifyError")
            || javaClassName.equals("java.lang.ClassCircularityError")
            || javaClassName.equals("java.lang.LinkageError")
            || javaClassName.equals("java.lang.Error")
            || javaClassName.equals("java.lang.Throwable"))
        return null;
        */

        if (javaClassName.startsWith("java.util.Collections"))
            return true;

        if (javaClassName.startsWith("java.lang.Thread") && !"java.lang.Thread".equals(javaClassName))
            return true;
        // because of Thread.getName()
        if (javaClassName.equals("java.lang.String"))
            return true;
        if (javaClassName.equals("java.util.Arrays"))
            return true;
        if (javaClassName.equals("java.lang.Math"))
            return true;

        // Object
        if (javaClassName.equals("java.lang.Object"))
            return true;
        // references
        if (javaClassName.startsWith("java.lang.ref."))
            return true;

        return false;
    }

    private byte[] transform0(final String className, final String javaClassName, final byte[] classfileBuffer) {
        final ClassReader reader = new ClassReader(classfileBuffer);

        final ClassNode classNode = new ClassNode();
        reader.accept(classNode, 0);
        final ClassWriter writer;

        // we have to synchronize on System.out first.
        // otherwise it may lead to a deadlock if a thread calls removeStale() on ConcurrentReferenceHashMap
        // while he holds the lock for System.out, but another thread is inside the transformation step and
        // waits for the lock of System.out
        synchronized (System.out) {
            synchronized (this.transformationLock) {

                // register that class for later reconstruction of the trace
                List<Field> fields;
                if (classNode.fields.isEmpty())
                    fields = Collections.emptyList();
                else
                    fields = new ArrayList<Field>(classNode.fields.size());

                final String javaSuperName = Type.getObjectType(classNode.superName).getClassName();
                final ReadClass readClass = new ReadClass(className, AbstractInstruction.getNextIndex(),
                        classNode.access, classNode.sourceFile, fields, javaSuperName);
                for (final Object fieldObj : classNode.fields) {
                    final FieldNode f = (FieldNode) fieldObj;
                    fields.add(new Field(f.name, f.desc, f.access, readClass));
                }

                writer = new FixedClassWriter(
                        COMPUTE_FRAMES ? ClassWriter.COMPUTE_FRAMES : ClassWriter.COMPUTE_MAXS);

                final ClassVisitor output = this.tracer.check ? new CheckClassAdapter(writer) : writer;

                if (Arrays.asList(this.pauseTracingClasses).contains(javaClassName)
                        || className.startsWith("java/security/")) {
                    new PauseTracingInstrumenter(readClass, this.tracer).transform(classNode);
                } else {
                    if ("java/lang/Thread".equals(className))
                        new ThreadInstrumenter(readClass, this.tracer).transform(classNode);
                    else
                        new TracingClassInstrumenter(readClass, this.tracer).transform(classNode);
                }

                new IdentifiableInstrumenter(readClass, this.tracer).transform(classNode);

                classNode.accept(COMPUTE_FRAMES ? new JSRInliner(output) : output);

                readClass.setInstructionNumberEnd(AbstractInstruction.getNextIndex());

                // now we can write the class out
                // NOTE: we do not write it out immediately, because this sometimes leads
                // to circular dependencies!
                //readClass.writeOut(this.readClassesOutputStream, this.readClassesStringCache);
                this.readClasses.add(readClass);

            }
        }

        final byte[] newClassfileBuffer = writer.toByteArray();

        if (this.tracer.check) {
            checkClass(newClassfileBuffer, className);
        }

        //printClass(newClassfileBuffer, Type.getObjectType(className).getClassName());
        /*
        if (className.endsWith("line/Main"))
        printClass(newClassfileBuffer, Type.getObjectType(className).getClassName());
        */

        return newClassfileBuffer;
    }

    private boolean checkClass(final byte[] newClassfileBuffer, final String classname) {
        final ClassNode cn = new ClassNode();
        final ClassReader cr = new ClassReader(newClassfileBuffer);
        //cr.accept(new CheckClassAdapter(cn), ClassReader.SKIP_DEBUG);
        cr.accept(new CheckClassAdapter(cn), 0);

        for (final Object methodObj : cn.methods) {
            final MethodNode method = (MethodNode) methodObj;
            final Analyzer a = new Analyzer(new BasicVerifier());
            // SimpleVerifier has problems with sub-classes, e.g. you cannot use PrintStream for Appendable and so on...
            //final Analyzer a = new Analyzer(new SimpleVerifier(
            //    Type.getObjectType(cn.name), Type.getObjectType(cn.superName),
            //    (cn.access & Opcodes.ACC_INTERFACE) != 0));
            try {
                a.analyze(cn.name, method);
            } catch (final AnalyzerException e) {
                System.err.println("Error in method " + classname + "." + method.name + method.desc + ":");
                e.printStackTrace(System.err);
                printMethod(a, System.err, method);
                return false;
            }
        }
        return true;
    }

    public static void printMethod(PrintStream out, MethodNode method) {
        final TraceMethodVisitor mv = new TraceMethodVisitor();

        out.println(method.name + method.desc);
        for (int j = 0; j < method.instructions.size(); ++j) {
            method.instructions.get(j).accept(mv);

            final StringBuffer s = new StringBuffer();
            while (s.length() < method.maxStack + method.maxLocals + 1) {
                s.append(' ');
            }
            out.print(Integer.toString(j + 100000).substring(1));
            out.print(" " + s + " : " + mv.text.get(j));
        }
        for (int j = 0; j < method.tryCatchBlocks.size(); ++j) {
            ((TryCatchBlockNode) method.tryCatchBlocks.get(j)).accept(mv);
            out.print(" " + mv.text.get(method.instructions.size() + j));
        }
        out.println(" MAXSTACK " + method.maxStack);
        out.println(" MAXLOCALS " + method.maxLocals);
        out.println();
    }

    private static void printMethod(final Analyzer a, final PrintStream out, final MethodNode method) {
        final Frame[] frames = a.getFrames();

        final TraceMethodVisitor mv = new TraceMethodVisitor();

        out.println(method.name + method.desc);
        for (int j = 0; j < method.instructions.size(); ++j) {
            method.instructions.get(j).accept(mv);

            final StringBuffer s = new StringBuffer();
            final Frame f = frames[j];
            if (f == null) {
                s.append('?');
            } else {
                for (int k = 0; k < f.getLocals(); ++k) {
                    s.append(getShortName(f.getLocal(k).toString())).append(' ');
                }
                s.append(" : ");
                for (int k = 0; k < f.getStackSize(); ++k) {
                    s.append(getShortName(f.getStack(k).toString())).append(' ');
                }
            }
            while (s.length() < method.maxStack + method.maxLocals + 1) {
                s.append(' ');
            }
            out.print(Integer.toString(j + 100000).substring(1));
            out.print(" " + s + " : " + mv.text.get(j));
        }
        for (int j = 0; j < method.tryCatchBlocks.size(); ++j) {
            ((TryCatchBlockNode) method.tryCatchBlocks.get(j)).accept(mv);
            out.print(" " + mv.text.get(method.instructions.size() + j));
        }
        out.println(" MAXSTACK " + method.maxStack);
        out.println(" MAXLOCALS " + method.maxLocals);
        out.println();
    }

    @SuppressWarnings("unused")
    private static void printClass(final byte[] classfileBuffer, final String classname) {
        /*
        final TraceClassVisitor v = new TraceClassVisitor(new PrintWriter(System.out));
        new ClassReader(classfileBuffer).accept(v, ClassReader.SKIP_DEBUG);
        */
        final ClassNode cn = new ClassNode();
        final ClassReader cr = new ClassReader(classfileBuffer);
        //cr.accept(new CheckClassAdapter(cn), ClassReader.SKIP_DEBUG);
        cr.accept(new CheckClassAdapter(cn), 0);

        for (final Object methodObj : cn.methods) {
            final MethodNode method = (MethodNode) methodObj;
            final Analyzer a = new Analyzer(new BasicVerifier());
            //final Analyzer a = new Analyzer(new SimpleVerifier());
            try {
                a.analyze(cn.name, method);
            } catch (final AnalyzerException e) {
                System.err.println("// error in method " + classname + "." + method.name + method.desc + ":" + e);
            }
            printMethod(a, System.err, method);
        }
    }

    private static String getShortName(final String name) {
        final int n = name.lastIndexOf('/');
        int k = name.length();
        if (name.charAt(k - 1) == ';') {
            k--;
        }
        return n == -1 ? name : name.substring(n + 1, k);
    }

    public void finish() {
        this.instrumentation.removeTransformer(this);
        if (this.tracer.debug) {
            TracingMethodInstrumenter.printStats(System.out);
            System.out.format((Locale) null, "Transforming %d classes took %.3f seconds in total.%n",
                    this.totalTransformedClasses.get(), 1e-9 * this.totalTransformationTime.get());
        }
    }

    /**
     * Checks whether the class given by the fully qualified java class name has been
     * redefined by the instrumenter or not.
     * The classes that couldn't get redefined are those already loaded by the vm when
     * the agent's premain method was executed.
     *
     * @param className the fully qualified classname to check
     * @return true if the class was redefined, false if not
     */
    // hmm, redefined is the wrong word here...
    public boolean wasRedefined(final String className) {
        return !this.notRedefinedClasses.contains(className);
    }

}