001package daikon.dcomp;
002
003import daikon.DynComp;
004import daikon.plumelib.bcelutil.BcelUtil;
005import daikon.plumelib.bcelutil.SimpleLog;
006import daikon.plumelib.reflection.Signatures;
007import java.io.ByteArrayInputStream;
008import java.io.File;
009import java.lang.instrument.ClassFileTransformer;
010import java.lang.instrument.IllegalClassFormatException;
011import java.security.ProtectionDomain;
012import org.apache.bcel.classfile.ClassParser;
013import org.apache.bcel.classfile.JavaClass;
014import org.checkerframework.checker.nullness.qual.Nullable;
015import org.checkerframework.checker.signature.qual.BinaryName;
016import org.checkerframework.checker.signature.qual.InternalForm;
017import org.checkerframework.dataflow.qual.Pure;
018
019/**
020 * This class is responsible for modifying another class's bytecodes. Specifically, its main task is
021 * to add calls into the DynComp Runtime for instrumentation purposes. These added calls are
022 * sometimes referred to as "hooks".
023 *
024 * <p>This class is loaded by Premain at startup. It is a ClassFileTransformer which means that its
025 * {@code transform} method gets called each time the JVM loads a class.
026 */
027public class Instrument implements ClassFileTransformer {
028
029  /** Directory for debug output. */
030  final File debug_dir;
031
032  /** Directory into which to dump debug-instrumented classes. */
033  final File debug_instrumented_dir;
034
035  /** Directory into which to dump original classes. */
036  final File debug_uninstrumented_dir;
037
038  /** Have we seen a class member of a known transformer? */
039  private static boolean transformer_seen = false;
040
041  /**
042   * Debug information about which classes and/or methods are transformed and why. Use
043   * debugInstrument for actual instrumentation details.
044   */
045  protected static final SimpleLog debug_transform = new SimpleLog(false);
046
047  /** Create an instrumenter. Setup debug directories, if needed. */
048  public Instrument() {
049    debug_transform.enabled =
050        DynComp.debug || DynComp.debug_transform || Premain.debug_dcinstrument || DynComp.verbose;
051    daikon.chicory.Instrument.debug_ppt_omit.enabled = DynComp.debug;
052
053    debug_dir = DynComp.debug_dir;
054    debug_instrumented_dir = new File(debug_dir, "instrumented");
055    debug_uninstrumented_dir = new File(debug_dir, "uninstrumented");
056
057    if (DynComp.dump) {
058      debug_instrumented_dir.mkdirs();
059      debug_uninstrumented_dir.mkdirs();
060    }
061  }
062
063  /** Debug code for printing the current run-time call stack. */
064  public static void print_call_stack() {
065    StackTraceElement[] stack_trace;
066    stack_trace = Thread.currentThread().getStackTrace();
067    // [0] is getStackTrace
068    // [1] is print_call_stack
069    for (int i = 2; i < stack_trace.length; i++) {
070      System.out.printf("call stack: %s%n", stack_trace[i]);
071    }
072    System.out.println();
073  }
074
075  /*
076   * Output a .class file and a .bcel version of the class file.
077   *
078   * @param c the Java class to output
079   * @param directory output location for the files
080   * @param className the current class
081   */
082  private void outputDebugFiles(JavaClass c, File directory, @BinaryName String className) {
083    try {
084      debug_transform.log("Dumping .class and .bcel for %s to %s%n", className, directory);
085      // Write the byte array to a .class file.
086      File outputFile = new File(directory, className + ".class");
087      c.dump(outputFile);
088      // Write a BCEL-like file.
089      BcelUtil.dump(c, directory);
090    } catch (Throwable t) {
091      System.err.printf("Unexpected error %s writing debug files for: %s%n", t, className);
092      t.printStackTrace();
093      // ignore the error, it shouldn't affect the instrumentation
094    }
095  }
096
097  /**
098   * Given a class, return a transformed version of the class that contains instrumentation code.
099   * Because DynComp is invoked as a javaagent, the transform method is called by the Java runtime
100   * each time a new class is loaded. A return value of null leaves the byte codes unchanged.
101   *
102   * <p>{@inheritDoc}
103   */
104  @Override
105  public byte @Nullable [] transform(
106      @Nullable ClassLoader loader,
107      @InternalForm String className,
108      @Nullable Class<?> classBeingRedefined,
109      ProtectionDomain protectionDomain,
110      byte[] classfileBuffer)
111      throws IllegalClassFormatException {
112
113    // for debugging
114    // new Throwable().printStackTrace();
115
116    debug_transform.log("Entering dcomp.Instrument.transform(): class = %s%n", className);
117
118    @BinaryName String binaryClassName = Signatures.internalFormToBinaryName(className);
119
120    if (className == null) {
121      /*
122      // debug code to display unnamed class
123      try {
124        // Parse the bytes of the classfile, die on any errors
125        ClassParser parser = new ClassParser(new ByteArrayInputStream(classfileBuffer), className);
126        JavaClass c = parser.parse();
127        System.out.println(c.toString());
128      } catch (Throwable e) {
129        System.out.printf("Unexpected Error: %s%n", e);
130        e.printStackTrace();
131        throw new RuntimeException("Unexpected error", e);
132      }
133      */
134      // most likely a lambda related class
135      return null;
136    }
137
138    // See comments in Premain.java about meaning and use of in_shutdown.
139    if (Premain.in_shutdown) {
140      debug_transform.log("Skipping in_shutdown class %s%n", binaryClassName);
141      return null;
142    }
143
144    // If already instrumented, nothing to do
145    // (This set will be empty if Premain.jdk_instrumented is false)
146    if (Premain.pre_instrumented.contains(className)) {
147      debug_transform.log("Skipping pre_instrumented JDK class %s%n", binaryClassName);
148      return null;
149    }
150
151    boolean in_jdk = false;
152
153    // Check if class is in JDK
154    if (BcelUtil.inJdkInternalform(className)) {
155      // If we are not using an instrumented JDK, then skip this class.
156      if (!Premain.jdk_instrumented) {
157        debug_transform.log("Skipping JDK class %s%n", binaryClassName);
158        return null;
159      }
160
161      int lastSlashPos = className.lastIndexOf('/');
162      if (lastSlashPos > 0) {
163        String packageName = className.substring(0, lastSlashPos).replace('/', '.');
164        if (Premain.problem_packages.contains(packageName)) {
165          debug_transform.log("Skipping problem package %s%n", packageName);
166          return null;
167        }
168      }
169
170      if (BcelUtil.javaVersion > 8) {
171        if (Premain.problem_classes.contains(binaryClassName)) {
172          debug_transform.log("Skipping problem class %s%n", binaryClassName);
173          return null;
174        }
175      }
176
177      if (className.contains("/$Proxy")) {
178        debug_transform.log("Skipping proxy class %s%n", binaryClassName);
179        return null;
180      }
181
182      if (className.equals("java/lang/DCRuntime")) {
183        debug_transform.log("Skipping special DynComp runtime class %s%n", binaryClassName);
184        return null;
185      }
186
187      in_jdk = true;
188      debug_transform.log("Instrumenting JDK class %s%n", binaryClassName);
189    } else {
190
191      // We're not in a JDK class
192      // Don't instrument our own classes
193      if (is_dcomp(className)) {
194        debug_transform.log("Skipping is_dcomp class %s%n", binaryClassName);
195        return null;
196      }
197
198      // Don't instrument other byte code transformers
199      if (is_transformer(className)) {
200        debug_transform.log("Skipping is_transformer class %s%n", binaryClassName);
201        if (!transformer_seen) {
202          transformer_seen = true;
203          System.err.printf(
204              "DynComp warning: This program uses a Java byte code transformer: %s%n",
205              binaryClassName);
206          System.err.printf(
207              "This may interfere with the DynComp transformer and cause DynComp to fail.%n");
208        }
209        return null;
210      }
211    }
212
213    ClassLoader cfLoader;
214    if (loader == null) {
215      cfLoader = ClassLoader.getSystemClassLoader();
216      debug_transform.log("Transforming class %s, loader %s - %s%n", className, loader, cfLoader);
217    } else {
218      cfLoader = loader;
219      debug_transform.log(
220          "Transforming class %s, loader %s - %s%n", className, loader, loader.getParent());
221    }
222
223    // Parse the bytes of the classfile, die on any errors.
224    JavaClass c;
225    try (ByteArrayInputStream bais = new ByteArrayInputStream(classfileBuffer)) {
226      ClassParser parser = new ClassParser(bais, className);
227      c = parser.parse();
228    } catch (Throwable t) {
229      System.err.printf("Unexpected error %s while reading %s%n", t, binaryClassName);
230      t.printStackTrace();
231      // No changes to the bytecodes
232      return null;
233    }
234
235    if (DynComp.dump) {
236      outputDebugFiles(c, debug_uninstrumented_dir, binaryClassName);
237    }
238
239    // Instrument the classfile, die on any errors
240    JavaClass njc;
241    try {
242      DCInstrument dci = new DCInstrument(c, in_jdk, loader);
243      njc = dci.instrument();
244    } catch (Throwable t) {
245      RuntimeException re =
246          new RuntimeException(
247              String.format("Unexpected error %s in transform of %s", t, binaryClassName), t);
248      re.printStackTrace();
249      throw re;
250    }
251
252    if (njc != null) {
253      if (DynComp.dump) {
254        outputDebugFiles(njc, debug_instrumented_dir, binaryClassName);
255      }
256      return njc.getBytes();
257    } else {
258      debug_transform.log("Didn't instrument %s%n", binaryClassName);
259      // No changes to the bytecodes
260      return null;
261    }
262  }
263
264  /**
265   * Returns whether or not the specified class is part of dcomp itself (and thus should not be
266   * instrumented). Some Daikon classes that are used by DynComp are included here as well.
267   *
268   * @param classname class to be checked
269   * @return true if classname is a part of DynComp
270   */
271  @Pure
272  private static boolean is_dcomp(String classname) {
273
274    if (classname.startsWith("daikon/dcomp/") && !classname.startsWith("daikon/dcomp/DcompTest")) {
275      return true;
276    }
277    if (classname.startsWith("daikon/chicory/")
278        && !classname.equals("daikon/chicory/ChicoryTest")) {
279      return true;
280    }
281    if (classname.equals("daikon/PptTopLevel$PptType")) {
282      return true;
283    }
284    if (classname.startsWith("daikon/plumelib")) {
285      return true;
286    }
287    return false;
288  }
289
290  /**
291   * Returns whether or not the specified class is part of a tool known to do Java byte code
292   * transformation. We need to warn the user that this may not work correctly.
293   *
294   * @param classname class to be checked
295   * @return true if classname is a known transformer
296   */
297  @Pure
298  protected static boolean is_transformer(String classname) {
299
300    if (classname.startsWith("org/codehaus/groovy")) {
301      return true;
302    }
303    if (classname.startsWith("groovy/lang")) {
304      return true;
305    }
306    if (classname.startsWith("org/mockito")) {
307      return true;
308    }
309    if (classname.startsWith("org/objenesis")) {
310      return true;
311    }
312    if (classname.contains("ByMockito")) {
313      return true;
314    }
315    return false;
316  }
317}