Java tutorial
/* * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.tdk.jcov.instrument; import com.sun.tdk.jcov.data.FileFormatException; import com.sun.tdk.jcov.io.Reader; import com.sun.tdk.jcov.runtime.Collect; import com.sun.tdk.jcov.runtime.FileSaver; import com.sun.tdk.jcov.tools.OptionDescr; import com.sun.tdk.jcov.util.DebugUtils; import com.sun.tdk.jcov.util.Utils; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.util.HashMap; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.Adler32; import org.objectweb.asm.Attribute; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import org.objectweb.asm.util.TraceClassVisitor; /** * @author Dmitry Fazunenko * @author Alexey Fedorchenko */ public class ClassMorph { private DataRoot root; private final String outputFile; // private List<String> instrumented = new ArrayList<String>(); // private HashSet<String> instrumented = new HashSet<String>(); // Set should work better private HashMap<String, Long> instrumented = new HashMap<String, Long>(); // Set should work better private HashMap<String, byte[]> instrumentedValues = new HashMap<String, byte[]>(); // Set should work better private static final boolean IS_SELFTEST = System.getProperty("jcov.selftest") != null; private final InstrumentationParams params; private boolean rtClassesInstrumented = false; private static final Logger logger; static { Utils.initLogger(); logger = Logger.getLogger(ClassMorph.class.getName()); } //For Agent public ClassMorph(String filename, DataRoot root, final InstrumentationParams params) { this.outputFile = filename; this.root = root; this.params = params; // can't use params from root - they can be different from instrumenting ones (? - but not at the moment) rtClassesInstrumented = params.isDataSaveSpecified(); findAlreadyInstrumentedAndSetID(); } /** * Default constructor for Instr and TemplGen. Uses specified template as * output file. */ public ClassMorph(final InstrumentationParams params, String template) { this(template, new DataRoot(params), params); this.root.getXMLHeadProperties().put("os.name", System.getProperty("os.name")); this.root.getXMLHeadProperties().put("os.arch", System.getProperty("os.arch")); this.root.getXMLHeadProperties().put("os.version", System.getProperty("os.version")); this.root.getXMLHeadProperties().put("user.name", System.getProperty("user.name")); this.root.getXMLHeadProperties().put("java.version", System.getProperty("java.version")); this.root.getXMLHeadProperties().put("java.runtime.version", System.getProperty("java.runtime.version")); } public boolean isTransformable(String className) { if (className.equals("java/lang/Object") && !params.isDynamicCollect() && params.isIncluded(className)) { logger.log(Level.WARNING, "java.lang.Object can't be statically instrumented and was excluded"); return false; } if (className.startsWith("com/sun/tdk/jcov") && !IS_SELFTEST || className.startsWith("org/objectweb/asm")) { logger.log(Level.INFO, "{0} - skipped (should not perform self-instrument)", className); return false; } if (className.startsWith("sun/reflect/Generated")) { logger.log(Level.WARNING, "{0} - skipped (should not instrument generated classes)"); return false; } return true; } public boolean isAlreadyTransformed(String className) { return instrumented.containsKey(className); } public boolean shouldTransform(String className) { return isTransformable(className) && !isAlreadyTransformed(className) && params.isIncluded(className); } /** * <p> Instrument loaded class data. </p> * * @param classfileBuffer Class data * @param loader ClassLoader containing this class (used in agent) * @param flushPath * @return * @throws IOException */ public byte[] morph(byte[] classfileBuffer, ClassLoader loader, String flushPath) throws IOException { if (loader == null) { loader = ClassLoader.getSystemClassLoader(); } if (classfileBuffer[0] != -54 || classfileBuffer[1] != -2 || classfileBuffer[2] != -70 || classfileBuffer[3] != -66) { throw new IOException("Not a java classfile (0xCAFEBABE header not found)"); } OffsetLabelingClassReader cr = new OffsetLabelingClassReader(classfileBuffer); String fullname = cr.getClassName(); if (!isTransformable(fullname)) { return null; } boolean shouldFlush = (flushPath != null); if (isAlreadyTransformed(fullname)) { long cs = computeCheckSum(classfileBuffer); Long oldCs = instrumented.get(fullname); if (oldCs > 0) { if (cs == oldCs && flushPath != null) { // the same class - restore from flushed logger.log(Level.FINE, "{0} - instrumented copy used", fullname); return DebugUtils.readClass(fullname, flushPath); } else { logger.log(Level.WARNING, "application has different classes with the same name: {0}", fullname); } } } if (params.isClassesReload() && !shouldTransform(fullname) && isAlreadyTransformed(fullname) && params.isDynamicCollect()) { return instrumentedValues.get(fullname); } if (!shouldTransform(fullname)) { if (!params.isDynamicCollect() || // isStatic !params.isCallerFilterOn() && !params.isInstrumentFields()) { if (isAlreadyTransformed(fullname)) { logger.log(Level.INFO, "{0} - skipped (already instrumented)", fullname); } if (!params.isIncluded(fullname)) { logger.log(Level.INFO, "{0} - skipped (is not included or is excluded explicitly)", fullname); } //null tells to AbstractUniversalInstrumenter not to overwrite existing data return null; } // support of caller_include feature // even those classes which are out of scope require some minor // transformation ClassWriter cw = new OverriddenClassWriter(cr, ClassWriter.COMPUTE_MAXS, loader); ClassVisitor cv = shouldFlush ? new TraceClassVisitor(cw, DebugUtils.getPrintWriter(fullname, flushPath)) : cw; cv = new InvokeClassAdapter(cv, params); cr.accept(cv, 0); byte[] res = cw.toByteArray(); if (shouldFlush) { DebugUtils.flushInstrumentedClass(flushPath, fullname, res); } if (!params.isDynamicCollect() && !rtClassesInstrumented && isPreVMLoadClass(fullname)) { rtClassesInstrumented = true; logger.log(Level.WARNING, "It's possible that you are instrumenting classes which are loaded before VM is loaded. It's recomended to add saveatend at java/lang/Shutdown.runHooks method. Data could be lost otherwise."); } return res; } // Checksum should be counted before changing content long checksum = params.isDynamicCollect() ? -1 : computeCheckSum(classfileBuffer); // The stackmap should be calculated only when static instrumentation // is used and class version 50. In this case the classfiles should // be downgraded to v 49. int opt = ClassWriter.COMPUTE_MAXS; if (params.isStackMapShouldBeUpdated()) { if (params.isDynamicCollect() && classfileBuffer[7] == 50) { classfileBuffer[7] = 49; } } if (classfileBuffer[7] > 49) { opt = ClassWriter.COMPUTE_FRAMES; } ClassWriter cw = new OverriddenClassWriter(cr, opt, loader); DataClass k = new DataClass(root.rootId(), fullname, checksum, false); // ClassVisitor cv = shouldFlush ? new TraceClassVisitor // (cw, DebugUtils.getPrintWriter(fullname, Options.getFlushPath())) : // cw; ClassVisitor cv = cw; cv = new DeferringMethodClassAdapter(cv, k, params); cr.accept(cv, new Attribute[] { new CharacterRangeTableAttribute(root.rootId()) }, 0); if (k.hasModifier(Opcodes.ACC_SYNTHETIC) && !params.isInstrumentSynthetic()) { return null; } else { root.addClass(k); instrumented.put(fullname, checksum); instrumentedValues.put(fullname, cw.toByteArray()); byte[] res = cw.toByteArray(); if (shouldFlush) { DebugUtils.flushInstrumentedClass(flushPath, fullname, res); } if (!params.isDynamicCollect() && !rtClassesInstrumented && isPreVMLoadClass(fullname)) { rtClassesInstrumented = true; logger.log(Level.WARNING, "It's possible that you are instrumenting classes which are loaded before VM is loaded. It's recomended to add saveatend at java/lang/Shutdown.runHooks method. Data could be lost otherwise."); } return res; } } public static long computeCheckSum(byte[] classfileBuffer) { // Adler32 adler = new Adler32(); // adler.update(classfileBuffer, 0, classfileBuffer.length); // long checksum = adler.getValue(); // return checksum; int i = 0; i += 4;//skip magic i += 4;//skip minor/major version int cp_count = ((classfileBuffer[i] & 0xFF) << 8) | (classfileBuffer[i + 1] & 0xFF); i += 2;//skip constant pool count // Need to cache UTF8 values and their indexes to be able to resolve // method and attribute names // TreeMap<Integer, String> cp_utf8_cache = new TreeMap(); HashMap<Integer, String> cp_utf8_cache = new HashMap(); // faster get //Process constant pool for (int j = 0; j < cp_count - 1; j++) { int cp_type = classfileBuffer[i++]; switch (cp_type) { case 1://utf8 int utf8_length = ((classfileBuffer[i] & 0xFF) << 8) | (classfileBuffer[i + 1] & 0xFF); i += 2; // byte[] value = Arrays.copyOfRange(classfileBuffer, i, i + utf8_length); // byte[] value = new byte[utf8_length]; // System.arraycopy(classfileBuffer, i, value, 0, utf8_length); // jdk1.5 support // String is immutable. No need to copy arrays String sval = new String(classfileBuffer, i, utf8_length, Charset.forName("UTF-8")); cp_utf8_cache.put(j + 1, sval); i += utf8_length; break; case 3://integer case 4://float i += 4; break; case 5://long case 6://double j++; i += 8; break; case 7://class i += 2; break; case 8://string i += 2; break; case 9://fieldref case 10://methodref case 11://interfacemethodref i += 4; break; case 12: i += 4;//name and type break; case 15: // methodhandle i += 3; break; case 16: // methodtype i += 2; break; case 18: // invokedynamic i += 4; break; case 19: // moduleId case 20: // moduleQuery i += 4; break; default: logger.log(Level.SEVERE, "SHOULD NOT OCCUR: unknown cp_type: {0}", cp_type); break; } } i += 2;//skip access flags i += 2;//skip this class i += 2;//skip super class int i_count = ((classfileBuffer[i] & 0xFF) << 8) | (classfileBuffer[i + 1] & 0xFF); i += 2;//skip interfaces count i += 2 * i_count;//skip interfaces table // Process fields int fld_count = ((classfileBuffer[i] & 0xFF) << 8) | (classfileBuffer[i + 1] & 0xFF); i += 2; byte[] clone = new byte[classfileBuffer.length]; int clone_ptr = i; // Copy all processed data to the clone System.arraycopy(classfileBuffer, 0, clone, 0, clone_ptr); for (int j = 0; j < fld_count; j++) { int f_start = i; i += 2 * 3; int attr_count = ((classfileBuffer[i] & 0xFF) << 8) | (classfileBuffer[i + 1] & 0xFF); i += 2; int[][] attr_ptrs = new int[attr_count][2]; int clone_attr_count = 0; for (int k = 0; k < attr_count; k++) { int name_index = ((classfileBuffer[i] & 0xFF) << 8) | (classfileBuffer[i + 1] & 0xFF); i += 2;//skip attr name long fld_attr_length = ((classfileBuffer[i] & 0xFF) << 24) | ((classfileBuffer[i + 1] & 0xFF) << 16) | ((classfileBuffer[i + 2] & 0xFF) << 8) | (classfileBuffer[i + 3] & 0xFF); i += 4;//skip attr length i += fld_attr_length; if (!cp_utf8_cache.get(name_index) .contains("Deprecated") /*&& !cp_utf8_cache.get(name_index).contains("ConstantValue")*/) {// skip deprecated attribute attr_ptrs[clone_attr_count][1] = 2 + 4 + (int) fld_attr_length; attr_ptrs[clone_attr_count][0] = i - attr_ptrs[clone_attr_count][1]; clone_attr_count++; } } System.arraycopy(classfileBuffer, f_start, clone, clone_ptr, 6); clone_ptr += 6; clone[clone_ptr++] = (byte) (clone_attr_count >> 8); clone[clone_ptr++] = (byte) clone_attr_count; // copy all required field attributes for (int l = 0; l < clone_attr_count; l++) { System.arraycopy(classfileBuffer, attr_ptrs[l][0], clone, clone_ptr, attr_ptrs[l][1]); clone_ptr += attr_ptrs[l][1]; } } // Process methods. Methods are sorted by their name + description befor // being written to the clone int mth_count = ((classfileBuffer[i] & 0xFF) << 8) | (classfileBuffer[i + 1] & 0xFF); clone[clone_ptr] = classfileBuffer[i]; clone_ptr++; i++; clone[clone_ptr] = classfileBuffer[i]; clone_ptr++; i++; TreeMap<String, byte[]> methods = new TreeMap(); for (int j = 0; j < mth_count; j++) { int m_start = i; i += 2; int name_index = ((classfileBuffer[i] & 0xFF) << 8) | (classfileBuffer[i + 1] & 0xFF); i += 2; int descriptor_index = ((classfileBuffer[i] & 0xFF) << 8) | (classfileBuffer[i + 1] & 0xFF); i += 2; int attr_count = ((classfileBuffer[i] & 0xFF) << 8) | (classfileBuffer[i + 1] & 0xFF); i += 2;//skip count; int[][] attr_ptrs = new int[attr_count][2]; int clone_attr_count = 0; int whole_attr_length = 0; for (int k = 0; k < attr_count; k++) { int attr_name_index = ((classfileBuffer[i] & 0xFF) << 8) | (classfileBuffer[i + 1] & 0xFF); i += 2;//skip attr name long mth_attr_length = ((classfileBuffer[i] & 0xFF) << 24) | ((classfileBuffer[i + 1] & 0xFF) << 16) | ((classfileBuffer[i + 2] & 0xFF) << 8) | (classfileBuffer[i + 3] & 0xFF); i += 4;//skip attr length i += mth_attr_length; if (!cp_utf8_cache.get(attr_name_index).contains("Deprecated")) {// skip deprecated attribute attr_ptrs[clone_attr_count][1] = 2 + 4 + (int) mth_attr_length; whole_attr_length += attr_ptrs[clone_attr_count][1]; attr_ptrs[clone_attr_count][0] = i - attr_ptrs[clone_attr_count][1]; clone_attr_count++; } } byte[] data = new byte[2 * 4 + whole_attr_length]; System.arraycopy(classfileBuffer, m_start, data, 0, 6); data[6] = (byte) (clone_attr_count >> 8); data[7] = (byte) clone_attr_count; int data_ptr = 0; for (int l = 0; l < clone_attr_count; l++) { System.arraycopy(classfileBuffer, attr_ptrs[l][0], data, data_ptr + 8, attr_ptrs[l][1]); data_ptr += attr_ptrs[l][1]; } methods.put(cp_utf8_cache.get(name_index) + cp_utf8_cache.get(descriptor_index), data); } // sorting methods for (byte data[] : methods.values()) { System.arraycopy(data, 0, clone, clone_ptr, data.length); clone_ptr += data.length; } // for (String key : methods.keySet()) { // int length = methods.get(key).length; // System.arraycopy(methods.get(key), 0, clone, clone_ptr, length); // clone_ptr += length; // } // Process class attributes. They are sorted by their names. Some are skipped. TreeMap<String, byte[]> attributes = new TreeMap(); int attr_count = ((classfileBuffer[i] & 0xFF) << 8) | (classfileBuffer[i + 1] & 0xFF); i += 2; int clone_attr_count = 0; for (int k = 0; k < attr_count; k++) { int name_index = ((classfileBuffer[i] & 0xFF) << 8) | (classfileBuffer[i + 1] & 0xFF); i += 2;//skip attr name long class_attr_length = ((classfileBuffer[i] & 0xFF) << 24) | ((classfileBuffer[i + 1] & 0xFF) << 16) | ((classfileBuffer[i + 2] & 0xFF) << 8) | (classfileBuffer[i + 3] & 0xFF); i += 4;//skip attr length i += class_attr_length; if (!cp_utf8_cache.get(name_index).contains("Deprecated") && !cp_utf8_cache.get(name_index).contains("EnclosingMethod")) { clone_attr_count++; byte[] data = new byte[2 + 4 + (int) class_attr_length]; System.arraycopy(classfileBuffer, i - data.length, data, 0, data.length); attributes.put(cp_utf8_cache.get(name_index), data); } } clone[clone_ptr++] = (byte) (clone_attr_count >> 8); clone[clone_ptr++] = (byte) clone_attr_count; // Sorting attributes for (byte data[] : attributes.values()) { System.arraycopy(data, 0, clone, clone_ptr, data.length); clone_ptr += data.length; } // for (String key : attributes.keySet()) { // int length = attributes.get(key).length; // System.arraycopy(attributes.get(key), 0, clone, clone_ptr, length); // clone_ptr += length; // } Adler32 adler = new Adler32(); adler.update(clone, 0, clone_ptr); long checksum = adler.getValue(); return checksum; } private void findAlreadyInstrumentedAndSetID() { try { if (outputFile == null) { return; } File file = new File(outputFile); if (!file.exists()) { return; } DataRoot r; if (params.isDynamicCollect()) { r = Reader.readXMLHeader(outputFile); } else { r = Reader.readXML(outputFile, true, null); for (DataPackage pack : r.getPackages()) { for (DataClass clazz : pack.getClasses()) { instrumented.put(clazz.getFullname(), clazz.getChecksum()); } } } Collect.setSlot(r.getCount()); r.destroy(); } catch (FileFormatException ffe) { System.err.println("Wrong format of the output file: " + outputFile + ". " + "Delete output file to receive coverage data"); } catch (Exception e) { throw new Error(e); } } public void saveData(InstrumentationOptions.MERGE merge) { FileSaver fileSaver = FileSaver.getFileSaver(root, outputFile, outputFile, merge, true, false); fileSaver.saveResults(); } public void saveData(String outputFile, InstrumentationOptions.MERGE merge) { FileSaver fileSaver = FileSaver.getFileSaver(root, outputFile, outputFile, merge, true, false); fileSaver.saveResults(); } /** * <p> Saves collected DataRoot (template) </p> * * @param outputTemplateFile File to save the template to * @param initialTemplatePath Template to use for saving the new template * (would be merged). Can be null if no merging with old template needed * @param merge Merging type used in case <code>outputTemplateFile</code> * file already exists */ public void saveData(String outputTemplateFile, String initialTemplatePath, InstrumentationOptions.MERGE merge) { FileSaver fileSaver = FileSaver.getFileSaver(root, outputTemplateFile, initialTemplatePath, merge, true, false); fileSaver.saveResults(); } public final static OptionDescr DSC_FLUSH_CLASSES = new OptionDescr("flush", null, "flush instrumented classes", OptionDescr.VAL_SINGLE, null, "Specify path to directory, where to store instrumented classes.\n" + "Directory should exist. Classes will be saved in respect to their package hierarchy.\n" + "Default value is \"none\". Pushing it means you don't want to flush classes.", "none"); private boolean isPreVMLoadClass(String fullname) { // classes (actually only certain packages needed) which are loaded before // VM is loaded - add classname (eg through new Exception().getStackTrace()[0]) // saving to Collect.hit return fullname.startsWith("java/lang") || fullname.startsWith("sun") || fullname.startsWith("java/util"); } }