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}