001package daikon.chicory; 002 003import daikon.Chicory; 004import daikon.plumelib.bcelutil.InstructionListUtils; 005import daikon.plumelib.bcelutil.SimpleLog; 006import java.io.ByteArrayInputStream; 007import java.io.IOException; 008import java.lang.instrument.ClassFileTransformer; 009import java.lang.instrument.IllegalClassFormatException; 010import java.nio.file.Files; 011import java.nio.file.Path; 012import java.security.ProtectionDomain; 013import java.util.ArrayList; 014import java.util.Iterator; 015import java.util.List; 016import java.util.regex.Matcher; 017import java.util.regex.Pattern; 018import org.apache.bcel.Const; 019import org.apache.bcel.classfile.Attribute; 020import org.apache.bcel.classfile.ClassParser; 021import org.apache.bcel.classfile.Constant; 022import org.apache.bcel.classfile.ConstantUtf8; 023import org.apache.bcel.classfile.ConstantValue; 024import org.apache.bcel.classfile.Field; 025import org.apache.bcel.classfile.JavaClass; 026import org.apache.bcel.classfile.Method; 027import org.apache.bcel.classfile.StackMapEntry; 028import org.apache.bcel.classfile.StackMapType; 029import org.apache.bcel.generic.ACONST_NULL; 030import org.apache.bcel.generic.ArrayType; 031import org.apache.bcel.generic.BasicType; 032import org.apache.bcel.generic.ClassGen; 033import org.apache.bcel.generic.ConstantPoolGen; 034import org.apache.bcel.generic.Instruction; 035import org.apache.bcel.generic.InstructionFactory; 036import org.apache.bcel.generic.InstructionHandle; 037import org.apache.bcel.generic.InstructionList; 038import org.apache.bcel.generic.InstructionTargeter; 039import org.apache.bcel.generic.LineNumberGen; 040import org.apache.bcel.generic.LocalVariableGen; 041import org.apache.bcel.generic.MethodGen; 042import org.apache.bcel.generic.ObjectType; 043import org.apache.bcel.generic.PUSH; 044import org.apache.bcel.generic.Type; 045import org.checkerframework.checker.nullness.qual.Nullable; 046import org.checkerframework.checker.signature.qual.BinaryName; 047import org.checkerframework.checker.signature.qual.ClassGetName; 048import org.checkerframework.checker.signature.qual.InternalForm; 049import org.checkerframework.dataflow.qual.Pure; 050 051/** 052 * The Instrument class is responsible for modifying another class' bytecode. Specifically, its main 053 * task is to add "hooks" into the other class at method entries and exits for instrumentation 054 * purposes. 055 */ 056@SuppressWarnings("nullness") 057public class Instrument extends InstructionListUtils implements ClassFileTransformer { 058 059 /** The index of this method in SharedData.methods. */ 060 int cur_method_info_index = 0; 061 062 /** The location of the runtime support class. */ 063 private static final String runtime_classname = "daikon.chicory.Runtime"; 064 065 /** Debug information about which classes are transformed and why. */ 066 public static SimpleLog debug_transform = new SimpleLog(false); 067 068 /** Create a new Instrument. Sets up debug logging. */ 069 public Instrument() { 070 super(); 071 debug_transform.enabled = Chicory.debug_transform; 072 debugInstrument.enabled = Chicory.debug; 073 } 074 075 /** 076 * Returns true if the given ppt should be ignored. Uses the patterns in {@link 077 * daikon.chicory.Runtime#ppt_omit_pattern} and {@link daikon.chicory.Runtime#ppt_select_pattern}. 078 * This method is used by both Chicory and Dyncomp. 079 * 080 * @param className class name to be checked 081 * @param methodName method name to be checked 082 * @param pptName ppt name to be checked 083 * @return true if the item should be filtered out 084 */ 085 public static boolean shouldIgnore(String className, String methodName, String pptName) { 086 087 // Don't instrument class if it matches an excluded regular expression 088 for (Pattern pattern : Runtime.ppt_omit_pattern) { 089 090 Matcher mPpt = pattern.matcher(pptName); 091 Matcher mClass = pattern.matcher(className); 092 Matcher mMethod = pattern.matcher(methodName); 093 094 if (mPpt.find() || mClass.find() || mMethod.find()) { 095 debug_transform.log("ignoring %s, it matches ppt_omit regex %s%n", pptName, pattern); 096 return true; 097 } 098 } 099 100 // If any include regular expressions are specified, only instrument 101 // classes that match them 102 if (Runtime.ppt_select_pattern.size() > 0) { 103 for (Pattern pattern : Runtime.ppt_select_pattern) { 104 105 Matcher mPpt = pattern.matcher(pptName); 106 Matcher mClass = pattern.matcher(className); 107 Matcher mMethod = pattern.matcher(methodName); 108 109 if (mPpt.find() || mClass.find() || mMethod.find()) { 110 debug_transform.log("including %s, it matches ppt_select regex %s%n", pptName, pattern); 111 return false; 112 } 113 } 114 } 115 116 // if we're here, this ppt not explicitly included or excluded 117 // so keep unless there were items in the "include only" list 118 if (Runtime.ppt_select_pattern.size() > 0) { 119 debug_transform.log("ignoring %s, not included in ppt_select pattern(s)%n", pptName); 120 return true; 121 } else { 122 debug_transform.log("including %s, not included in ppt_omit pattern(s)%n", pptName); 123 return false; 124 } 125 } 126 127 /** 128 * Given a class, return a transformed version of the class that contains "hooks" at method 129 * entries and exits. Because Chicory is invoked as a javaagent, the transform method is called by 130 * the Java runtime each time a new class is loaded. 131 */ 132 @Override 133 public byte @Nullable [] transform( 134 ClassLoader loader, 135 @InternalForm String className, 136 Class<?> classBeingRedefined, 137 ProtectionDomain protectionDomain, 138 byte[] classfileBuffer) 139 throws IllegalClassFormatException { 140 141 @BinaryName String fullClassName = className.replace("/", "."); 142 // String fullClassName = className; 143 144 // new Throwable().printStackTrace(); 145 146 debug_transform.log("In chicory.Instrument.transform(): class = %s%n", className); 147 148 // Don't instrument boot classes. They are uninteresting and will 149 // not be able to access daikon.chicory.Runtime (because it is not 150 // on the boot classpath). Previously this code skipped classes 151 // that started with java, com, javax, or sun, but this is not 152 // correct in many cases. Most boot classes have the null loader, 153 // but some generated classes (such as those in sun.reflect) will 154 // have a non-null loader. Some of these have a null parent loader, 155 // but some do not. The check for the sun.reflect package is a hack 156 // to catch all of these. A more consistent mechanism to determine 157 // boot classes would be preferrable. 158 if (Chicory.boot_classes != null) { 159 Matcher matcher = Chicory.boot_classes.matcher(fullClassName); 160 if (matcher.find()) { 161 debug_transform.log("ignoring boot class %s, matches boot_classes regex%n", fullClassName); 162 return null; 163 } 164 } else if (loader == null) { 165 debug_transform.log("ignoring system class %s, class loader == null%n", fullClassName); 166 return null; 167 } else if (loader.getParent() == null) { 168 debug_transform.log("ignoring system class %s, parent loader == null%n", fullClassName); 169 return null; 170 } else if (fullClassName.startsWith("sun.reflect")) { 171 debug_transform.log("ignoring system class %s, in sun.reflect package%n", fullClassName); 172 return null; 173 } else if (fullClassName.startsWith("jdk.internal.reflect")) { 174 // Starting with Java 9 sun.reflect => jdk.internal.reflect. 175 debug_transform.log( 176 "ignoring system class %s, in jdk.internal.reflect package", fullClassName); 177 return null; 178 } else if (fullClassName.startsWith("com.sun")) { 179 debug_transform.log("Class from com.sun package %s with nonnull loaders%n", fullClassName); 180 } 181 182 // Don't intrument our code 183 if (is_chicory(className)) { 184 debug_transform.log("Not considering chicory class %s%n", fullClassName); 185 return null; 186 } 187 188 debug_transform.log( 189 "transforming class %s, loader %s - %s%n", className, loader, loader.getParent()); 190 191 // Parse the bytes of the classfile, die on any errors 192 JavaClass c; 193 try (ByteArrayInputStream bais = new ByteArrayInputStream(classfileBuffer)) { 194 ClassParser parser = new ClassParser(bais, className); 195 c = parser.parse(); 196 } catch (Exception e) { 197 throw new RuntimeException("Unexpected error", e); 198 } 199 200 try { 201 // Get the class information 202 ClassGen cg = new ClassGen(c); 203 204 // Convert reach non-void method to save its result in a local 205 // before returning 206 ClassInfo c_info = instrument_all_methods(cg, fullClassName, loader); 207 208 // get constant static fields! 209 Field[] fields = cg.getFields(); 210 for (Field field : fields) { 211 if (field.isFinal() && field.isStatic() && (field.getType() instanceof BasicType)) { 212 ConstantValue value = field.getConstantValue(); 213 String valString; 214 215 if (value == null) { 216 // System.out.println("WARNING FROM " + field.getName()); 217 // valString = "WARNING!!!"; 218 valString = null; 219 } else { 220 valString = value.toString(); 221 // System.out.println("GOOD FROM " + field.getName() + 222 // " --- " + valString); 223 } 224 225 if (valString != null) { 226 c_info.staticMap.put(field.getName(), valString); 227 } 228 } 229 } 230 231 if (Chicory.checkStaticInit) { 232 // check for static initializer 233 boolean hasInit = false; 234 for (Method meth : cg.getMethods()) { 235 if (meth.getName().equals("<clinit>")) { 236 hasInit = true; 237 } 238 } 239 240 // if not found, add our own! 241 if (!hasInit) { 242 cg.addMethod(createClinit(cg, fullClassName)); 243 } 244 } 245 246 JavaClass njc = cg.getJavaClass(); 247 if (Chicory.debug) { 248 Path dir = Files.createTempDirectory("chicory-debug"); 249 Path file = dir.resolve(njc.getClassName() + ".class"); 250 debugInstrument.log("Dumping %s to %s%n", njc.getClassName(), file); 251 Files.createDirectories(dir); 252 njc.dump(file.toFile()); 253 } 254 255 if (c_info.shouldInclude) { 256 // System.out.println ("Instrumented class " + className); 257 // String filename = "/homes/gws/mernst/tmp/" + className + 258 // "Transformed.class"; 259 // System.out.println ("About to dump class " + className + 260 // " to " + filename); 261 // njc.dump(filename); 262 return njc.getBytes(); 263 } else { 264 // No changes to the bytecodes 265 return null; 266 } 267 268 } catch (Throwable e) { 269 System.out.printf("Unexpected error %s in transform of %s", e, fullClassName); 270 e.printStackTrace(); 271 // No changes to the bytecodes 272 return null; 273 } 274 } 275 276 // used to add a "hook" into the <clinit> static initializer 277 private Method addInvokeToClinit(ClassGen cg, MethodGen mg, String fullClassName) { 278 279 try { 280 InstructionList il = mg.getInstructionList(); 281 setCurrentStackMapTable(mg, cg.getMajor()); 282 MethodContext context = new MethodContext(cg, mg); 283 284 for (InstructionHandle ih = il.getStart(); ih != null; ) { 285 Instruction inst = ih.getInstruction(); 286 287 // Get the translation for this instruction (if any) 288 InstructionList new_il = xform_clinit(cg.getConstantPool(), fullClassName, inst, context); 289 290 // Remember the next instruction to process 291 InstructionHandle next_ih = ih.getNext(); 292 293 // will do nothing if new_il == null 294 insertBeforeHandle(mg, ih, new_il, false); 295 296 // Go on to the next instruction in the list 297 ih = next_ih; 298 } 299 300 remove_local_variable_type_table(mg); 301 createNewStackMapAttribute(mg); 302 303 // Update the max stack and Max Locals 304 mg.setMaxLocals(); 305 mg.setMaxStack(); 306 mg.update(); 307 } catch (Exception e) { 308 System.out.printf("Unexpected exception encountered: %s", e); 309 e.printStackTrace(); 310 } 311 312 return mg.getMethod(); 313 } 314 315 // called by addInvokeToClinit to add in a hook at return opcodes 316 private @Nullable InstructionList xform_clinit( 317 ConstantPoolGen cp, String fullClassName, Instruction inst, MethodContext context) { 318 319 switch (inst.getOpcode()) { 320 case Const.ARETURN: 321 case Const.DRETURN: 322 case Const.FRETURN: 323 case Const.IRETURN: 324 case Const.LRETURN: 325 case Const.RETURN: 326 return call_initNotify(cp, fullClassName, context.ifact); 327 328 default: 329 return null; 330 } 331 } 332 333 // create a <clinit> method, if none exists; guarantees we have this hook 334 private Method createClinit(ClassGen cg, @BinaryName String fullClassName) { 335 InstructionFactory factory = new InstructionFactory(cg); 336 337 InstructionList il = new InstructionList(); 338 il.append(call_initNotify(cg.getConstantPool(), fullClassName, factory)); 339 il.append(InstructionFactory.createReturn(Type.VOID)); // need to return! 340 341 MethodGen newMethGen = 342 new MethodGen( 343 8, 344 Type.VOID, 345 new Type[0], 346 new String[0], 347 "<clinit>", 348 fullClassName, 349 il, 350 cg.getConstantPool()); 351 newMethGen.update(); 352 353 // Update the max stack and Max Locals 354 newMethGen.setMaxLocals(); 355 newMethGen.setMaxStack(); 356 newMethGen.update(); 357 358 return newMethGen.getMethod(); 359 } 360 361 // created the InstructionList to insert for adding the <clinit> hook 362 private InstructionList call_initNotify( 363 ConstantPoolGen cp, String fullClassName, InstructionFactory factory) { 364 365 InstructionList invokeList = new InstructionList(); 366 367 invokeList.append(new PUSH(cp, fullClassName)); 368 invokeList.append( 369 factory.createInvoke( 370 runtime_classname, 371 "initNotify", 372 Type.VOID, 373 new Type[] {Type.STRING}, 374 Const.INVOKESTATIC)); 375 376 // System.out.println(fullClassName + " --- " + invokeList.size()); 377 return invokeList; 378 } 379 380 // Map<Integer, InstructionHandle> offset_map = new HashMap<>(); 381 InstructionHandle[] offset_map; 382 383 /** 384 * Instrument all the methods in a class. For each method, add instrumentation code at the entry 385 * and at each return from the method. In addition, changes each return statement to first place 386 * the value being returned into a local and then return. This allows us to work around the JDI 387 * deficiency of not being able to query return values. 388 * 389 * @param fullClassName must be fully qualified: packageName.className 390 */ 391 private ClassInfo instrument_all_methods(ClassGen cg, String fullClassName, ClassLoader loader) { 392 393 ClassInfo class_info = new ClassInfo(cg.getClassName(), loader); 394 List<MethodInfo> method_infos = new ArrayList<>(); 395 396 if (cg.getMajor() < Const.MAJOR_1_6) { 397 System.out.printf( 398 "Chicory warning: ClassFile: %s - classfile version (%d) is out of date and may not be" 399 + " processed correctly.%n", 400 cg.getClassName(), cg.getMajor()); 401 } 402 403 boolean shouldInclude = false; 404 405 try { 406 // Loop through each method in the class 407 Method[] methods = cg.getMethods(); 408 for (int i = 0; i < methods.length; i++) { 409 410 // The class data in StackMapUtils is not thread safe, 411 // allow only one method at a time to be instrumented. 412 // DynComp does this by creating a new instrumentation object 413 // for each class - probably a cleaner solution. 414 synchronized (this) { 415 pool = cg.getConstantPool(); 416 MethodGen mg = new MethodGen(methods[i], cg.getClassName(), pool); 417 MethodContext context = new MethodContext(cg, mg); 418 419 // check for the class static initializer method 420 if (mg.getName().equals("<clinit>")) { 421 if (Chicory.checkStaticInit) { 422 cg.replaceMethod(methods[i], addInvokeToClinit(cg, mg, fullClassName)); 423 cg.update(); 424 } 425 if (!Chicory.instrument_clinit) { 426 continue; 427 } 428 } 429 430 // If method is synthetic... (default constructors and <clinit> are not synthetic) 431 if ((Const.ACC_SYNTHETIC & mg.getAccessFlags()) > 0) { 432 continue; 433 } 434 435 // Get the instruction list and skip methods with no instructions 436 InstructionList il = mg.getInstructionList(); 437 if (il == null) { 438 continue; 439 } 440 441 if (debugInstrument.enabled) { 442 Type[] arg_types = mg.getArgumentTypes(); 443 String[] arg_names = mg.getArgumentNames(); 444 LocalVariableGen[] local_vars = mg.getLocalVariables(); 445 String types = "", names = "", locals = ""; 446 447 for (int j = 0; j < arg_types.length; j++) { 448 types = types + arg_types[j] + " "; 449 } 450 for (int j = 0; j < arg_names.length; j++) { 451 names = names + arg_names[j] + " "; 452 } 453 for (int j = 0; j < local_vars.length; j++) { 454 locals = locals + local_vars[j].getName() + " "; 455 } 456 debugInstrument.log("%nMethod = %s%n", mg); 457 debugInstrument.log("arg_types(%d): %s%n", arg_types.length, types); 458 debugInstrument.log("arg_names(%d): %s%n", arg_names.length, names); 459 debugInstrument.log("localvars(%d): %s%n", local_vars.length, locals); 460 debugInstrument.log("Original code: %s%n", mg.getMethod().getCode()); 461 debugInstrument.log("%n"); 462 } 463 464 // Get existing StackMapTable (if present) 465 setCurrentStackMapTable(mg, cg.getMajor()); 466 467 fixLocalVariableTable(mg); 468 469 // Create a MethodInfo that describes this methods arguments 470 // and exit line numbers (information not available via reflection) 471 // and add it to the list for this class. 472 MethodInfo mi = create_method_info(class_info, mg); 473 474 printStackMapTable("After create_method_info"); 475 476 if (mi == null) { // method filtered out! 477 continue; 478 } 479 480 shouldInclude = true; // at least one method not filtered out 481 482 // Create a map of Uninitialized_variable_info offsets to 483 // InstructionHandles. We will use this map after we 484 // complete instrumentation to update the offsets due 485 // to code modification and expansion. 486 // The offsets point to 'new' instructions; since we do 487 // not modify these, their Instruction Handles will remain 488 // unchanged throught the instrumentaion process. 489 buildUninitializedNewMap(il); 490 491 method_infos.add(mi); 492 493 synchronized (SharedData.methods) { 494 cur_method_info_index = SharedData.methods.size(); 495 SharedData.methods.add(mi); 496 } 497 498 // Add nonce local to matchup enter/exits 499 add_entry_instrumentation(il, context); 500 501 printStackMapTable("After add_entry_instrumentation"); 502 503 debugInstrument.log("Modified code: %s%n", mg.getMethod().getCode()); 504 505 // Need to see if there are any switches after this location. 506 // If so, we may need to update the corresponding stackmap if 507 // the amount of the switch padding changed. 508 modifyStackMapsForSwitches(il.getStart(), il); 509 510 Iterator<Boolean> shouldIncIter = mi.is_included.iterator(); 511 Iterator<Integer> exitIter = mi.exit_locations.iterator(); 512 513 // Loop through each instruction looking for the return(s) 514 for (InstructionHandle ih = il.getStart(); ih != null; ) { 515 Instruction inst = ih.getInstruction(); 516 517 // If this is a return instruction, insert method exit instrumentation 518 InstructionList new_il = 519 generate_return_instrumentation(inst, context, shouldIncIter, exitIter); 520 521 // Remember the next instruction to process 522 InstructionHandle next_ih = ih.getNext(); 523 524 // If this instruction was modified, replace it with the new 525 // instruction list. If this instruction was the target of any 526 // jumps, replace it with the first instruction in the new list 527 insertBeforeHandle(mg, ih, new_il, true); 528 529 // Go on to the next instruction in the list 530 ih = next_ih; 531 } 532 533 // Update the Uninitialized_variable_info offsets before 534 // we write out the new StackMapTable. 535 updateUninitializedNewOffsets(il); 536 537 createNewStackMapAttribute(mg); 538 539 remove_local_variable_type_table(mg); 540 541 // Update the instruction list 542 mg.setInstructionList(il); 543 mg.update(); 544 545 // Update the max stack 546 mg.setMaxStack(); 547 mg.update(); 548 549 // Update the method in the class 550 try { 551 cg.replaceMethod(methods[i], mg.getMethod()); 552 } catch (Exception e) { 553 if (e.getMessage().startsWith("Branch target offset too large")) { 554 System.out.printf( 555 "Chicory warning: ClassFile: %s - method %s is too large to instrument and is" 556 + " being skipped.%n", 557 cg.getClassName(), mg.getName()); 558 continue; 559 } else { 560 throw e; 561 } 562 } 563 564 if (debugInstrument.enabled) { 565 debugInstrument.log("Modified code: %s%n", mg.getMethod().getCode()); 566 dump_code_attributes(mg); 567 } 568 cg.update(); 569 } 570 } 571 } catch (Exception e) { 572 System.out.printf("Unexpected exception encountered: %s", e); 573 e.printStackTrace(); 574 } 575 576 // Add the class and method information to runtime so it is available 577 // as enter/exit ppts are processed. 578 class_info.set_method_infos(method_infos); 579 580 if (shouldInclude) { 581 debug_transform.log("Added trace info to class %s%n", class_info); 582 synchronized (SharedData.new_classes) { 583 SharedData.new_classes.add(class_info); 584 } 585 synchronized (SharedData.all_classes) { 586 SharedData.all_classes.add(class_info); 587 } 588 } else { // not included 589 debug_transform.log("Trace info not added to class %s%n", class_info); 590 } 591 592 class_info.shouldInclude = shouldInclude; 593 return class_info; 594 } 595 596 // This method exists only to suppress interning warnings 597 @Pure 598 private static boolean isVoid(Type t) { 599 return t == Type.VOID; 600 } 601 602 /** 603 * If this is a return instruction, generate new il to assign the result to a local variable 604 * (return__$trace2_val) and then call daikon.chicory.Runtime.exit(). This il wil be inserted 605 * immediately before the return. 606 */ 607 private @Nullable InstructionList generate_return_instrumentation( 608 Instruction inst, 609 MethodContext c, 610 Iterator<Boolean> shouldIncIter, 611 Iterator<Integer> exitIter) { 612 613 switch (inst.getOpcode()) { 614 case Const.ARETURN: 615 case Const.DRETURN: 616 case Const.FRETURN: 617 case Const.IRETURN: 618 case Const.LRETURN: 619 case Const.RETURN: 620 break; 621 622 default: 623 return null; 624 } 625 626 if (!shouldIncIter.hasNext()) { 627 throw new RuntimeException("Not enough entries in shouldIncIter"); 628 } 629 630 boolean shouldInclude = shouldIncIter.next(); 631 632 if (!shouldInclude) { 633 return null; 634 } 635 636 Type type = c.mgen.getReturnType(); 637 InstructionList il = new InstructionList(); 638 if (!isVoid(type)) { 639 LocalVariableGen return_loc = get_return_local(c.mgen, type); 640 il.append(InstructionFactory.createDup(type.getSize())); 641 il.append(InstructionFactory.createStore(type, return_loc.getIndex())); 642 } 643 644 if (!exitIter.hasNext()) { 645 throw new RuntimeException("Not enough exit locations in the exitIter"); 646 } 647 648 il.append(call_enter_exit(c, "exit", exitIter.next())); 649 return il; 650 } 651 652 /** 653 * Returns the local variable used to store the return result. If it is not present, creates it 654 * with the specified type. If the variable is known to already exist, the type can be null. 655 */ 656 private LocalVariableGen get_return_local(MethodGen mg, @Nullable Type return_type) { 657 658 // Find the local used for the return value 659 LocalVariableGen return_local = null; 660 for (LocalVariableGen lv : mg.getLocalVariables()) { 661 if (lv.getName().equals("return__$trace2_val")) { 662 return_local = lv; 663 break; 664 } 665 } 666 667 // If a type was specified and the variable was found, they must match 668 if (return_local == null) { 669 assert return_type != null : " return__$trace2_val doesn't exist"; 670 } else { 671 assert return_type.equals(return_local.getType()) 672 : " return_type = " + return_type + "current type = " + return_local.getType(); 673 } 674 675 if (return_local == null) { 676 debugInstrument.log("Adding return local of type %s%n", return_type); 677 return_local = mg.addLocalVariable("return__$trace2_val", return_type, null, null); 678 } 679 680 return return_local; 681 } 682 683 /** Finds the nonce local variable. Returns null if not present. */ 684 private @Nullable LocalVariableGen get_nonce_local(MethodGen mg) { 685 686 // Find the local used for the nonce value 687 for (LocalVariableGen lv : mg.getLocalVariables()) { 688 if (lv.getName().equals("this_invocation_nonce")) { 689 return lv; 690 } 691 } 692 693 return null; 694 } 695 696 /** 697 * Inserts instrumentation code at the start of the method. This includes adding a local variable 698 * (this_invocation_nonce) that is initialized to Runtime.nonce++. This provides a unique id on 699 * each method entry/exit that allows them to be matched up from the dtrace file. Inserts code to 700 * call daikon.chicory.Runtime.enter(). 701 * 702 * @param il instruction list for method 703 * @param c MethodContext for method 704 * @throws IOException if there is trouble with I/O 705 */ 706 private void add_entry_instrumentation(InstructionList il, MethodContext c) throws IOException { 707 708 String atomic_int_classname = "java.util.concurrent.atomic.AtomicInteger"; 709 Type atomic_int_type = new ObjectType(atomic_int_classname); 710 711 InstructionList nl = new InstructionList(); 712 713 // create the local variable 714 LocalVariableGen nonce_lv = 715 create_method_scope_local(c.mgen, "this_invocation_nonce", Type.INT); 716 717 printStackMapTable("After cln"); 718 719 if (debugInstrument.enabled) { 720 debugInstrument.log("Modified code: %s%n", c.mgen.getMethod().getCode()); 721 } 722 723 // The following implements: 724 // this_invocation_nonce = Runtime.nonce++; 725 726 // getstatic Runtime.nonce (load reference to AtomicInteger daikon.chicory.Runtime.nonce) 727 nl.append(c.ifact.createGetStatic(runtime_classname, "nonce", atomic_int_type)); 728 729 // do an atomic get and increment of nonce value 730 // this is multi-thread safe and leaves int value of nonce on stack 731 nl.append( 732 c.ifact.createInvoke( 733 atomic_int_classname, "getAndIncrement", Type.INT, new Type[] {}, Const.INVOKEVIRTUAL)); 734 735 // istore <lv> (pop original value of nonce into this_invocation_nonce) 736 nl.append(InstructionFactory.createStore(Type.INT, nonce_lv.getIndex())); 737 738 nl.setPositions(); 739 InstructionHandle end = nl.getEnd(); 740 int len_part1 = end.getPosition() + end.getInstruction().getLength(); 741 742 // call Runtime.enter() 743 nl.append(call_enter_exit(c, "enter", -1)); 744 745 nl.setPositions(); 746 end = nl.getEnd(); 747 int len_part2 = end.getPosition() + end.getInstruction().getLength() - len_part1; 748 749 // Add the new instructions at the start and move any LineNumbers 750 // and Local variables to point to them. Other targeters 751 // (branches, exceptions) should still point to the old start 752 // NOTE: Don't use insert_at_method_start as it tries to update StackMaps 753 // and that will be done with special code below. 754 InstructionHandle old_start = il.getStart(); 755 InstructionHandle new_start = il.insert(nl); 756 for (InstructionTargeter it : old_start.getTargeters()) { 757 if ((it instanceof LineNumberGen) || (it instanceof LocalVariableGen)) { 758 it.updateTarget(old_start, new_start); 759 } 760 } 761 762 // For Java 7 and beyond the StackMapTable is part of the 763 // verification process. We need to create and or update it to 764 // account for instrumentation code we have inserted as well as 765 // adjustments for the new 'nonce' local. 766 767 boolean skipFirst = false; 768 769 // Modify existing StackMapTable (if present) 770 if (stackMapTable.length > 0) { 771 // Each stack map frame specifies (explicity or implicitly) an 772 // offset_delta that is used to calculate the actual bytecode 773 // offset at which the frame applies. This is caluclated by 774 // by adding offset_delta + 1 to the bytecode offset of the 775 // previous frame, unless the previous frame is the initial 776 // frame of the method, in which case the bytecode offset is 777 // offset_delta. (From the Java Virual Machine Specification, 778 // Java SE 7 Edition, section 4.7.4) 779 780 // Since we are inserting (1 or 2) new stack map frames at the 781 // beginning of the stack map table, we need to adjust the 782 // offset_delta of the original first stack map frame due to 783 // the fact that it will no longer be the first entry. We must 784 // subtract 1. BUT, if the original first entry has an offset 785 // of 0 (because bytecode address 0 is a branch target) then 786 // we must delete it as it will be replaced by the new frames 787 // we are adding. (did you get all of that? - markro) 788 789 if (stackMapTable[0].getByteCodeOffset() == 0) { 790 skipFirst = true; 791 } else { 792 stackMapTable[0].updateByteCodeOffset(-1); 793 } 794 } 795 796 // Create new StackMap entries for our instrumentation code. 797 int new_table_length = stackMapTable.length + ((len_part2 > 0) ? 2 : 1) - (skipFirst ? 1 : 0); 798 StackMapEntry[] new_map = new StackMapEntry[new_table_length]; 799 StackMapType nonce_type = new StackMapType(Const.ITEM_Integer, -1, pool.getConstantPool()); 800 StackMapType[] old_nonce_type = {nonce_type}; 801 new_map[0] = 802 new StackMapEntry( 803 Const.APPEND_FRAME, len_part1, old_nonce_type, null, pool.getConstantPool()); 804 805 int new_index = 1; 806 if (len_part2 > 0) { 807 new_map[1] = 808 new StackMapEntry( 809 ((len_part2 - 1) > Const.SAME_FRAME_MAX 810 ? Const.SAME_FRAME_EXTENDED 811 : Const.SAME_FRAME + len_part2 - 1), 812 len_part2 - 1, 813 null, 814 null, 815 pool.getConstantPool()); 816 new_index++; 817 } 818 819 // We can just copy the rest of the stack frames over as the FULL_FRAME 820 // ones were already updated when the nonce variable was allocated. 821 for (int i = (skipFirst ? 1 : 0); i < stackMapTable.length; i++) { 822 new_map[new_index++] = stackMapTable[i]; 823 } 824 stackMapTable = new_map; 825 } 826 827 /** 828 * Pushes the object, nonce, parameters, and return value on the stack and calls the specified 829 * Method (normally enter or exit) in daikon.chicory.Runtime. The parameters are passed as an 830 * array of objects. Any primitive values are wrapped in the appropriate daikon.chicory.Runtime 831 * wrapper (IntWrap, FloatWrap, etc). 832 */ 833 private InstructionList call_enter_exit(MethodContext c, String method_name, int line) { 834 835 InstructionList il = new InstructionList(); 836 InstructionFactory ifact = c.ifact; 837 MethodGen mg = c.mgen; 838 Type[] arg_types = mg.getArgumentTypes(); 839 840 // aload 841 // Push the object. Null if this is a static method or a constructor 842 if (mg.isStatic() || (method_name.equals("enter") && is_constructor(mg))) { 843 il.append(new ACONST_NULL()); 844 } else { // must be an instance method 845 il.append(InstructionFactory.createLoad(Type.OBJECT, 0)); 846 } 847 848 // Determine the offset of the first parameter 849 int param_offset = 1; 850 if (mg.isStatic()) { 851 param_offset = 0; 852 } 853 854 // iload 855 // Push the nonce 856 LocalVariableGen nonce_lv = get_nonce_local(mg); 857 il.append(InstructionFactory.createLoad(Type.INT, nonce_lv.getIndex())); 858 859 // iconst 860 // Push the MethodInfo index 861 il.append(ifact.createConstant(cur_method_info_index)); 862 863 // iconst 864 // anewarray 865 // Create an array of objects with elements for each parameter 866 il.append(ifact.createConstant(arg_types.length)); 867 Type object_arr_typ = new ArrayType("java.lang.Object", 1); 868 il.append(ifact.createNewArray(Type.OBJECT, (short) 1)); 869 870 // Put each argument into the array 871 int param_index = param_offset; 872 for (int ii = 0; ii < arg_types.length; ii++) { 873 il.append(InstructionFactory.createDup(object_arr_typ.getSize())); 874 il.append(ifact.createConstant(ii)); 875 Type at = arg_types[ii]; 876 if (at instanceof BasicType) { 877 il.append(create_wrapper(c, at, param_index)); 878 } else { // must be reference of some sort 879 il.append(InstructionFactory.createLoad(Type.OBJECT, param_index)); 880 } 881 il.append(InstructionFactory.createArrayStore(Type.OBJECT)); 882 param_index += at.getSize(); 883 } 884 885 // If this is an exit, push the return value and line number. 886 // The return value is stored in the local "return__$trace2_val". 887 // If the return value is a primitive, wrap it in the appropriate wrapper. 888 if (method_name.equals("exit")) { 889 Type ret_type = mg.getReturnType(); 890 if (isVoid(ret_type)) { 891 il.append(new ACONST_NULL()); 892 } else { 893 LocalVariableGen return_local = get_return_local(mg, ret_type); 894 if (ret_type instanceof BasicType) { 895 il.append(create_wrapper(c, ret_type, return_local.getIndex())); 896 } else { 897 il.append(InstructionFactory.createLoad(Type.OBJECT, return_local.getIndex())); 898 } 899 } 900 901 // push line number 902 // System.out.println(mg.getName() + " --> " + line); 903 il.append(ifact.createConstant(line)); 904 } 905 906 // Call the specified method 907 Type[] method_args; 908 if (method_name.equals("exit")) { 909 method_args = 910 new Type[] {Type.OBJECT, Type.INT, Type.INT, object_arr_typ, Type.OBJECT, Type.INT}; 911 } else { 912 method_args = new Type[] {Type.OBJECT, Type.INT, Type.INT, object_arr_typ}; 913 } 914 il.append( 915 c.ifact.createInvoke( 916 runtime_classname, method_name, Type.VOID, method_args, Const.INVOKESTATIC)); 917 918 return il; 919 } 920 921 /** 922 * Creates code to put the local var/param at the specified var_index into a wrapper appropriate 923 * for prim_type. prim_type should be one of the basic types (eg, Type.INT, Type.FLOAT, etc). The 924 * wrappers are those defined in daikon.chicory.Runtime. 925 * 926 * <p>The stack is left with a pointer to the newly created wrapper at the top. 927 */ 928 private InstructionList create_wrapper(MethodContext c, Type prim_type, int var_index) { 929 930 String wrapper; 931 switch (prim_type.getType()) { 932 case Const.T_BOOLEAN: 933 wrapper = "BooleanWrap"; 934 break; 935 case Const.T_BYTE: 936 wrapper = "ByteWrap"; 937 break; 938 case Const.T_CHAR: 939 wrapper = "CharWrap"; 940 break; 941 case Const.T_DOUBLE: 942 wrapper = "DoubleWrap"; 943 break; 944 case Const.T_FLOAT: 945 wrapper = "FloatWrap"; 946 break; 947 case Const.T_INT: 948 wrapper = "IntWrap"; 949 break; 950 case Const.T_LONG: 951 wrapper = "LongWrap"; 952 break; 953 case Const.T_SHORT: 954 wrapper = "ShortWrap"; 955 break; 956 default: 957 throw new Error("unexpected type " + prim_type); 958 } 959 960 InstructionList il = new InstructionList(); 961 String classname = runtime_classname + "$" + wrapper; 962 il.append(c.ifact.createNew(classname)); 963 il.append(InstructionFactory.createDup(Type.OBJECT.getSize())); 964 il.append(InstructionFactory.createLoad(prim_type, var_index)); 965 il.append( 966 c.ifact.createInvoke( 967 classname, "<init>", Type.VOID, new Type[] {prim_type}, Const.INVOKESPECIAL)); 968 969 return il; 970 } 971 972 /** 973 * Returns true iff mgen is a constructor. 974 * 975 * @return true iff mgen is a constructor 976 */ 977 @Pure 978 private boolean is_constructor(MethodGen mgen) { 979 980 if (mgen.getName().equals("<init>") || mgen.getName().equals("")) { 981 debugInstrument.log("method '%s' is a constructor%n", mgen.getName()); 982 return true; 983 } else { 984 return false; 985 } 986 } 987 988 /** 989 * Return an array of strings, each corresponding to mgen's argument types. 990 * 991 * @return an array of strings, each corresponding to mgen's argument types 992 */ 993 private @BinaryName String[] getArgTypes(MethodGen mgen) { 994 995 Type[] arg_types = mgen.getArgumentTypes(); 996 @BinaryName String[] arg_type_strings = new @BinaryName String[arg_types.length]; 997 998 for (int ii = 0; ii < arg_types.length; ii++) { 999 Type t = arg_types[ii]; 1000 /*if (t instanceof ObjectType) 1001 arg_type_strings[ii] = ((ObjectType) t).getClassName(); 1002 else { 1003 arg_type_strings[ii] = t.getSignature().replace('/', '.'); 1004 } 1005 */ 1006 arg_type_strings[ii] = t.toString(); 1007 } 1008 1009 return arg_type_strings; 1010 } 1011 1012 // creates a MethodInfo struct corresponding to mgen 1013 @SuppressWarnings("unchecked") 1014 private @Nullable MethodInfo create_method_info(ClassInfo class_info, MethodGen mgen) { 1015 1016 // Get the argument names for this method 1017 String[] arg_names = mgen.getArgumentNames(); 1018 LocalVariableGen[] lvs = mgen.getLocalVariables(); 1019 int param_offset = 1; 1020 if (mgen.isStatic()) { 1021 param_offset = 0; 1022 } 1023 if (debugInstrument.enabled) { 1024 debugInstrument.log("create_method_info1 %s%n", arg_names.length); 1025 for (int ii = 0; ii < arg_names.length; ii++) { 1026 debugInstrument.log("arg: %s%n", arg_names[ii]); 1027 } 1028 } 1029 1030 int lv_start = 0; 1031 // If this is an inner class constructor, then its first parameter is 1032 // the outer class constructor. I need to detect this and adjust the 1033 // parameter names appropriately. This check is ugly. 1034 if (mgen.getName().equals("<init>") && mgen.getArgumentTypes().length > 0) { 1035 int dollarPos = mgen.getClassName().lastIndexOf("$"); 1036 if (dollarPos >= 0 1037 && 1038 // type of first parameter is classname up to the "$" 1039 mgen.getClassName().substring(0, dollarPos).equals(mgen.getArgumentType(0).toString())) { 1040 // As a further check, for javac-generated classfiles, the 1041 // constant pool index #1 is "this$0", and the first 5 bytes of 1042 // the bytecode are: 1043 // 0: aload_0 1044 // 1: aload_1 1045 // 2: putfield #1 1046 1047 lv_start++; 1048 arg_names[0] = mgen.getArgumentType(0).toString() + ".this"; 1049 } 1050 } 1051 1052 if (lvs != null) { 1053 for (int ii = lv_start; ii < arg_names.length; ii++) { 1054 if ((ii + param_offset) < lvs.length) { 1055 arg_names[ii] = lvs[ii + param_offset].getName(); 1056 } 1057 } 1058 } 1059 1060 if (debugInstrument.enabled) { 1061 debugInstrument.log("create_method_info2 %s%n", arg_names.length); 1062 for (int ii = 0; ii < arg_names.length; ii++) { 1063 debugInstrument.log("arg: %s%n", arg_names[ii]); 1064 } 1065 } 1066 1067 boolean shouldInclude = false; 1068 1069 // see if we should track the entry point 1070 if (!shouldIgnore( 1071 class_info.class_name, 1072 mgen.getName(), 1073 DaikonWriter.methodEntryName( 1074 class_info.class_name, getArgTypes(mgen), mgen.toString(), mgen.getName()))) { 1075 shouldInclude = true; 1076 } 1077 // Get the argument types for this method 1078 Type[] arg_types = mgen.getArgumentTypes(); 1079 @ClassGetName String[] arg_type_strings = new @ClassGetName String[arg_types.length]; 1080 for (int ii = 0; ii < arg_types.length; ii++) { 1081 arg_type_strings[ii] = typeToClassGetName(arg_types[ii]); 1082 } 1083 1084 // Loop through each instruction and find the line number for each 1085 // return opcode 1086 List<Integer> exit_locs = new ArrayList<>(); 1087 1088 // tells whether each exit loc in the method is included or not (based on filters) 1089 List<Boolean> isIncluded = new ArrayList<>(); 1090 1091 debugInstrument.log("Looking for exit points in %s%n", mgen.getName()); 1092 InstructionList il = mgen.getInstructionList(); 1093 int line_number = 0; 1094 int last_line_number = 0; 1095 boolean foundLine; 1096 1097 for (InstructionHandle ih : il) { 1098 foundLine = false; 1099 1100 if (ih.hasTargeters()) { 1101 for (InstructionTargeter it : ih.getTargeters()) { 1102 if (it instanceof LineNumberGen) { 1103 LineNumberGen lng = (LineNumberGen) it; 1104 // debugInstrument.log(" line number at %s: %d%n", ih, lng.getSourceLine()); 1105 line_number = lng.getSourceLine(); 1106 foundLine = true; 1107 } 1108 } 1109 } 1110 1111 switch (ih.getInstruction().getOpcode()) { 1112 case Const.ARETURN: 1113 case Const.DRETURN: 1114 case Const.FRETURN: 1115 case Const.IRETURN: 1116 case Const.LRETURN: 1117 case Const.RETURN: 1118 debugInstrument.log("Exit at line %d%n", line_number); 1119 1120 // only do incremental lines if we don't have the line generator 1121 if (line_number == last_line_number && foundLine == false) { 1122 debugInstrument.log("Could not find line... at %d%n", line_number); 1123 line_number++; 1124 } 1125 1126 last_line_number = line_number; 1127 1128 if (!shouldIgnore( 1129 class_info.class_name, 1130 mgen.getName(), 1131 DaikonWriter.methodExitName( 1132 class_info.class_name, 1133 getArgTypes(mgen), 1134 mgen.toString(), 1135 mgen.getName(), 1136 line_number))) { 1137 shouldInclude = true; 1138 exit_locs.add(line_number); 1139 1140 isIncluded.add(true); 1141 } else { 1142 isIncluded.add(false); 1143 } 1144 1145 break; 1146 1147 default: 1148 break; 1149 } 1150 } 1151 1152 if (shouldInclude) { 1153 return new MethodInfo( 1154 class_info, mgen.getName(), arg_names, arg_type_strings, exit_locs, isIncluded); 1155 } else { 1156 return null; 1157 } 1158 } 1159 1160 public void dump_code_attributes(MethodGen mg) { 1161 // mg.getMethod().getCode().getAttributes() forces attributes 1162 // to be instantiated; mg.getCodeAttributes() does not 1163 for (Attribute a : mg.getMethod().getCode().getAttributes()) { 1164 int con_index = a.getNameIndex(); 1165 Constant c = pool.getConstant(con_index); 1166 String att_name = ((ConstantUtf8) c).getBytes(); 1167 debugInstrument.log("Attribute Index: %s Name: %s%n", con_index, att_name); 1168 } 1169 } 1170 1171 /** Any information needed by InstTransform routines about the method and class. */ 1172 private static class MethodContext { 1173 1174 public InstructionFactory ifact; 1175 public MethodGen mgen; 1176 1177 public MethodContext(ClassGen cg, MethodGen mgen) { 1178 ifact = new InstructionFactory(cg); 1179 this.mgen = mgen; 1180 } 1181 } 1182 1183 /** 1184 * Returns whether or not the specified class is part of Chicory itself (and thus should not be 1185 * instrumented). Some Daikon classes that are used by Chicory are included here as well. 1186 * 1187 * @param classname the name of the class to test, in internal form 1188 * @return true if the given class is part of Chicory itself 1189 */ 1190 @Pure 1191 private static boolean is_chicory(@InternalForm String classname) { 1192 1193 if (classname.startsWith("daikon/chicory") && !classname.equals("daikon/chicory/ChicoryTest")) { 1194 return true; 1195 } 1196 if (classname.equals("daikon/PptTopLevel$PptType")) { 1197 return true; 1198 } 1199 if (classname.startsWith("daikon/plumelib")) { 1200 return true; 1201 } 1202 return false; 1203 } 1204}