/* * This file is part of JOP, the Java Optimized Processor * see <http://www.jopdesign.com/> * * Copyright (C) 2010, Stefan Hepp (stefan@stefant.org). * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.jopdesign.common; import com.jopdesign.common.bcel.BcelRepositoryWrapper; import com.jopdesign.common.code.CFGProvider; import com.jopdesign.common.code.CallGraph; import com.jopdesign.common.code.CallGraph.CallgraphBuilder; import com.jopdesign.common.code.CallString; import com.jopdesign.common.code.ControlFlowGraph; import com.jopdesign.common.code.DefaultCallgraphBuilder; import com.jopdesign.common.code.InvokeSite; import com.jopdesign.common.graphutils.ClassHierarchyTraverser; import com.jopdesign.common.graphutils.ClassVisitor; import com.jopdesign.common.logger.LogConfig; import com.jopdesign.common.misc.AppInfoError; import com.jopdesign.common.misc.ClassInfoNotFoundException; import com.jopdesign.common.misc.JavaClassFormatError; import com.jopdesign.common.misc.MethodNotFoundException; import com.jopdesign.common.misc.MissingClassError; import com.jopdesign.common.misc.NamingConflictException; import com.jopdesign.common.misc.Ternary; import com.jopdesign.common.processormodel.ProcessorModel; import com.jopdesign.common.tools.ClinitOrder; import com.jopdesign.common.type.ClassRef; import com.jopdesign.common.type.FieldRef; import com.jopdesign.common.type.MemberID; import com.jopdesign.common.type.MethodRef; import org.apache.bcel.Constants; import org.apache.bcel.Repository; import org.apache.bcel.classfile.ClassFormatException; import org.apache.bcel.classfile.ClassParser; import org.apache.bcel.classfile.ConstantPool; import org.apache.bcel.classfile.JavaClass; import org.apache.bcel.generic.ClassGen; import org.apache.bcel.generic.Type; import org.apache.bcel.util.ClassPath; import org.apache.bcel.util.ClassPath.ClassFile; import org.apache.log4j.Logger; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; /** * The AppInfo class loads, creates and holds ClassInfos, handles all the loading related stuff, * manages CustomKeys and modification events, maintains a class hierarchy and provides various * methods to get and to iterate over ClassInfos. * * @author Stefan Hepp (stefan@stefant.org) */ public final class AppInfo implements ImplementationFinder, CFGProvider { private static final Logger logger = Logger.getLogger(LogConfig.LOG_STRUCT + ".AppInfo"); private static final Logger loadLogger = Logger.getLogger(LogConfig.LOG_LOADING + ".AppInfo"); private ClassPath classPath; private final Map<String,ClassInfo> classes; private final Set<MemberInfo> roots; private MethodInfo mainMethod; // if true, an invalid or missing (and not excluded) class does not trigger an error private boolean ignoreMissingClasses; // if true, native classes are loaded too private boolean loadNatives; // if true, library classes are loaded too private boolean loadLibraries; private boolean exitOnMissingClass; private final Set<String> hwObjectClasses; private final Set<String> libraryClasses; private final Set<String> ignoredClasses; private final List<AppEventHandler> eventHandlers; private ProcessorModel processor; private int callstringLength; private CallGraph callGraph; private String dumpCacheKeyFile = null; private byte[] digest = null; ////////////////////////////////////////////////////////////////////////////// // Singleton ////////////////////////////////////////////////////////////////////////////// private static final AppInfo singleton; static { singleton = new AppInfo(); Repository.setRepository(new BcelRepositoryWrapper()); } public static AppInfo getSingleton() { return singleton; } private AppInfo() { this.classPath = new ClassPath("."); ignoreMissingClasses = false; loadNatives = true; loadLibraries = true; exitOnMissingClass = false; classes = new LinkedHashMap<String, ClassInfo>(); roots = new LinkedHashSet<MemberInfo>(); hwObjectClasses = new LinkedHashSet<String>(); libraryClasses = new LinkedHashSet<String>(1); ignoredClasses = new LinkedHashSet<String>(1); eventHandlers = new ArrayList<AppEventHandler>(3); } ////////////////////////////////////////////////////////////////////////////// // AppEventHandler and CustomKey management, AppInfo setup stuff ////////////////////////////////////////////////////////////////////////////// public void registerEventHandler(AppEventHandler handler) { handler.onRegisterEventHandler(this); eventHandlers.add(handler); } public boolean hasEventHandler(AppEventHandler handler) { return eventHandlers.contains(handler); } /** * Get a list of all registered eventHandlers. Do not modify this list. * @return a list of registered AppEventHandlers */ public List<AppEventHandler> getEventHandlers() { return Collections.unmodifiableList(eventHandlers); } /** * Just a shortcut for {@link KeyManager#getSingleton()} * @return the KeyManager */ public KeyManager getKeyManager() { return KeyManager.getSingleton(); } /** * Get the current classpath used for loading classes. * @return the current BCEL classpath. */ public ClassPath getClassPath() { return classPath; } /** * Set the new classPath, overwriting the old one. * ClassInfos are not reloaded, use {@link #reloadClasses(boolean)} for that. * * @param classPath the new classpath. */ public void setClassPath(ClassPath classPath) { this.classPath = classPath; } ////////////////////////////////////////////////////////////////////////////// // Methods to create, load, get and remove ClassInfos ////////////////////////////////////////////////////////////////////////////// /** * Create a new classInfo. If a class with the same name exists, return the existing classInfo. * * <p>Note that this does not update the complete class hierarchy. You need to call {@link #reloadClassHierarchy()} * after you finished creating and loading classes. SuperClass will be set, but nothing more.</p> * * @param className the fully qualified name of the class. * @param superClass the references to the superclass, or null to use java.lang.Object (ignored if the new class is * java.lang.Object). * @param isInterface true if this class should be an interface. * @return a new ClassInfo or the current ClassInfo by the same name if it exists. * @throws NamingConflictException if a class with the same name exists, but has a different definition. */ public ClassInfo createClass(String className, ClassRef superClass, boolean isInterface) throws NamingConflictException { String superClassName; if (superClass == null) { superClassName = "java.lang.Object".equals(className) ? null : "java.lang.Object"; } else { superClassName = superClass.getClassName(); } // check for existing class ClassInfo cls = classes.get(className); if ( cls != null ) { if ( isInterface != cls.isInterface() || !cls.getSuperClassName().equals(superClassName) ) { throw new NamingConflictException("Class '"+className+ "' already exists but has a different definition."); } return cls; } // create cls = createClassInfo(className, superClassName, isInterface); // do a "partial" class hierarchy update (i.e. set superClass, but not subclasses of the new class) cls.updateClassHierarchy(); // register class classes.put(className, cls); for (AppEventHandler mgr : eventHandlers) { mgr.onCreateClass(cls, false); } return cls; } /** * Try to load a classInfo. * If the class is already loaded, return the existing classInfo. * If the class is excluded, return null. * If the class could not be loaded but is not excluded, throw an {@link MissingClassError} or abort. * * @see #loadClass(String, boolean, boolean) * @param className the fully qualified name of the class. * @return the classInfo for the classname or null if excluded. */ public ClassInfo loadClass(String className) { ClassInfo info = null; try { info = loadClass(className, false, false); } catch (ClassInfoNotFoundException e) { String msg = "Failed to load class '"+className+"' from '"+getClassPath()+"' in '" + new File(".").getAbsolutePath() + "'"; handleClassLoadFailure(msg, e); } return info; } /** * Try to load a classInfo. * If a class is excluded (native, library, ignored), return null. * If a class is not found or if loading failed and the class is not excluded, or if it is * required, an exception is thrown. * If a class is not required and ignoreMissing is true, no exception will be thrown. * To update the class hierarchy relations of the ClassInfos, you need to call * {@link #reloadClassHierarchy()} after all classes have been loaded. * * @param className the fully qualified name of the class. * @param required if true, throw an exception even if the class is excluded or ignoreMissing is true. * @param reload if true, reload the classInfo if it already exists. * @return the classInfo for the classname, or null if excluded. * @throws ClassInfoNotFoundException if the class could not be loaded and is not excluded. */ public ClassInfo loadClass(String className, boolean required, boolean reload) throws ClassInfoNotFoundException { ClassInfo cls = classes.get(className); if ( cls != null ) { if ( reload ) { removeClass(cls); } else { return cls; } } // check if it is excluded (and not required) if ( isExcluded(className) ) { if ( required ) { throw new ClassInfoNotFoundException("Class '"+className+"' is excluded but required."); } return null; } return performLoadClass(className, required); } /** * Check if a class exists. Returns true even if it is not loaded or if the class is excluded. * * @param className the FQ classname with '.' separators * @return true if the class can be found in the classpath or has been created (even if it is excluded). */ public boolean classExists(String className) { if (classes.containsKey(className)) { return true; } return checkClassExists(className); } public ClassFile getClassFile(ClassInfo ci) throws FileNotFoundException { try { return classPath.getClassFile(ci.getClassName()); } catch (FileNotFoundException e) { throw e; } catch (IOException e) { throw new AppInfoError("Could not get classfile for class "+ci, e); } } /** * Remove a single class and all its nested classes from AppInfo, and update the class hierarchy. * <p> * To remove several classes or all subclasses of a class, use {@link #removeClasses(Collection)} to * remove all classes in one step, as this is faster. * </p> * * @param classInfo the class to remove. */ public void removeClass(ClassInfo classInfo) { removeClasses(Collections.singleton(classInfo)); } /** * Remove a collection of classes and all their nested classes from AppInfo, and update the class hierarchy. * * @param classes the classes to remove. Duplicates in this collection will be removed first. */ public void removeClasses(Collection<ClassInfo> classes) { // first, collect all nested classes and remove duplicates. final Map<String,ClassInfo> map = new LinkedHashMap<String, ClassInfo>(classes.size()); for (ClassInfo classInfo : classes) { ClassVisitor v = new ClassVisitor() { @Override public boolean visitClass(ClassInfo classInfo) { // we put the visited (nested) class in the map, and descend if it is not already there return map.put(classInfo.getClassName(), classInfo) == null; } @Override public void finishClass(ClassInfo classInfo) {} }; ClassHierarchyTraverser cht = new ClassHierarchyTraverser(v); cht.setVisitSubclasses(false, false); cht.setVisitInnerClasses(true); cht.traverseDown(classInfo); } // now we go through all classes and remove them from the class-list and from the class hierarchy for (ClassInfo classInfo : classes) { for ( AppEventHandler mgr : eventHandlers ) { mgr.onRemoveClass(classInfo); } this.classes.remove(classInfo.getClassName()); classInfo.removeFromClassHierarchy(); } // finally go through all classes once more to update the FullyKnown-flags for (ClassInfo classInfo : classes) { // since we already removed the classes from the class hierarchy, this won't descend down // classes we are removing classInfo.finishRemoveFromHierarchy(); classInfo.resetHierarchyInfos(); } } /** * Get an already loaded class. * * This method only returns null if classes are excluded from loading or the class is missing * and doIgnoreMissingClasses is set. Else a {@link MissingClassError} is thrown. * * @see #getClassInfo(String, boolean) * @param className the classname of the class. * @return the classInfo of the class or null if the class is excluded. */ public ClassInfo getClassInfo(String className) { ClassInfo classInfo = null; try { classInfo = getClassInfo(className, false); } catch (ClassInfoNotFoundException e) { handleClassLoadFailure(e.getMessage(), e); } return classInfo; } /** * Get an already loaded class. * * This method only returns null if classes are excluded from loading or the class is missing * and doIgnoreMissingClasses is set. Else an exception is thrown. * If required is true, this method never returns null but throws an exception if the classInfo is not found. * * @see #getClassInfo(String) * @param className fully qualified name of the class to get. * @param required if true, always throw an exception if not loaded. * @return the classInfo, or null if excluded or not found and ignored and not required. * @throws ClassInfoNotFoundException if the class is required but not found or excluded. */ public ClassInfo getClassInfo(String className, boolean required) throws ClassInfoNotFoundException { ClassInfo cls = classes.get(className); if ( cls != null ) { return cls; } if ( isExcluded(className) ) { if ( required ) { throw new ClassInfoNotFoundException("Class '"+className+"' is excluded but required."); } return null; } // class is null, not excluded, and not loaded on demand, i.e. missing if ( required ) { throw new ClassInfoNotFoundException("Required class '"+className+"' not loaded."); } if ( !ignoreMissingClasses ) { throw new ClassInfoNotFoundException("Requested class '"+className+"' is missing and not excluded."); } return cls; } /** * Remove all classInfos. * * @param clearRoots if true, clear list of roots and main method as well, else * keep the root classes in the class list. */ public void clear(boolean clearRoots) { for (AppEventHandler mgr : eventHandlers) { mgr.onClearAppInfo(this); } classes.clear(); if ( clearRoots ) { roots.clear(); mainMethod = null; } else { // re-add all root classes for (MemberInfo root : roots) { classes.put(root.getClassInfo().getClassName(), root.getClassInfo()); } } callGraph = null; } /** * Reload all currently loaded classInfos from disk, using the current classpath. * @param checkExcludes if true, reevaluate excludes, else reload all classes even if excluded. * @throws ClassInfoNotFoundException if a class could not be reloaded. */ public void reloadClasses(boolean checkExcludes) throws ClassInfoNotFoundException { List<String> clsNames = new LinkedList<String>(classes.keySet()); clear(false); for (String clsName : clsNames ) { if ( checkExcludes && isExcluded(clsName) ) { continue; } performLoadClass(clsName, false); } // reload mainMethod if ( mainMethod != null ) { ClassInfo mainClass = classes.get(mainMethod.getClassInfo().getClassName()); if ( mainClass == null ) { mainMethod = null; throw new ClassInfoNotFoundException("Could not find main class."); } mainMethod = mainClass.getMethodInfo(mainMethod.getMemberID().getMethodSignature()); if (mainMethod == null) { throw new ClassInfoNotFoundException("Could not find main method in main class"); } } reloadClassHierarchy(); } /** * Reload all super- and subclass references of all classInfos. */ public void reloadClassHierarchy() { for (ClassInfo cls : classes.values()) { cls.resetHierarchyInfos(); } for (ClassInfo cls : classes.values()) { cls.updateClassHierarchy(); } for (ClassInfo cls : classes.values()) { cls.updateCompleteFlag(false); } } public boolean hasClassInfo(String className) { return classes.containsKey(className); } /** * Get a collection of all classInfos in this AppInfo. * Changes to the AppInfo are visible to the returned collection. * You should not modify this collection directly. * * @return an unmodifiable view of the collection of AppInfos. */ public Collection<ClassInfo> getClassInfos() { return Collections.unmodifiableCollection(classes.values()); } public Collection<String> getClassNames() { return Collections.unmodifiableCollection(classes.keySet()); } public void iterate(ClassVisitor visitor) { for (ClassInfo c : classes.values()) { if (!visitor.visitClass(c)) { return; } visitor.finishClass(c); } } ////////////////////////////////////////////////////////////////////////////// // Helper methods to find classes, fields and methods; Convenience methods ////////////////////////////////////////////////////////////////////////////// public ClassRef getClassRef(String className) { ClassInfo cls = classes.get(className); if ( cls != null ) { return cls.getClassRef(); } return new ClassRef(className); } public ClassRef getClassRef(String className, boolean isInterface) { ClassInfo cls = classes.get(className); if ( cls != null ) { if ( cls.isInterface() != isInterface ) { throw new ClassFormatException("Class '"+className+"' interface flag does not match."); } return cls.getClassRef(); } return new ClassRef(className, isInterface); } public MethodRef getMethodRef(MemberID memberID) { ClassInfo cls = classes.get(memberID.getClassName()); ClassRef clsRef; if ( cls != null ) { clsRef = cls.getClassRef(); } else { clsRef = new ClassRef(memberID.getClassName()); } return getMethodRef(clsRef, memberID); } /** * Get a reference to a method using the given memberID. * If the method is defined only in a (known) superclass and is inherited by this class, * get a methodRef which contains a ClassRef to the given class but a MethodInfo from the superclass. * <p> * If you already have a classRef, use {@link #getMethodRef(ClassRef, MemberID)} * instead. * </p> * @param memberID the memberID of the method * @param isInterfaceMethod true if the class is an interface. * @return a method reference with or without MethodInfo or ClassInfo. */ public MethodRef getMethodRef(MemberID memberID, boolean isInterfaceMethod) { ClassInfo cls = classes.get(memberID.getClassName()); ClassRef clsRef; if ( cls != null ) { if ( cls.isInterface() != isInterfaceMethod ) { throw new ClassFormatException("Class '"+cls.getClassName()+"' interface flag does not match."); } clsRef = cls.getClassRef(); } else { clsRef = new ClassRef(memberID.getClassName(),isInterfaceMethod); } return getMethodRef(clsRef, memberID); } /** * Get a reference to a method using the given memberID. * If the method is defined only in a (known) superclass and is inherited by this class, * get a methodRef which contains a ClassRef to the given class but a MethodInfo from the superclass. * * @param classRef The reference to the class or interface of the method. * @param memberID The memberID of the method. Only memberName and memberDescriptor are used. * @return A method reference with or without MethodInfo or ClassInfo. */ public MethodRef getMethodRef(ClassRef classRef, MemberID memberID) { return new MethodRef(classRef, memberID.getMemberName(), memberID.getDescriptor()); } /** * Get a reference to a method using the given signature. * If the method is defined only in a (known) superclass and is inherited by this class, * get a methodRef which contains a ClassRef to the given class but a MethodInfo from the superclass. * * @param className The fully qualified name of the class or interface of the method. * @param methodSignature The signature of the method. * @return A method reference with or without MethodInfo or ClassInfo. */ public MethodRef getMethodRef(String className, String methodSignature) { return getMethodRef(getClassRef(className), MemberID.parse(methodSignature, true)); } /** * Get a reference to a field using the given memberID. * * @param memberID The memberID of the field. * @return A field reference with or without FieldInfo or ClassInfo. */ public FieldRef getFieldRef(MemberID memberID) { return getFieldRef(memberID.getClassName(), memberID.getMemberName()); } public FieldRef getFieldRef(String className, String fieldName) { ClassInfo cls = classes.get(className); ClassRef clsRef; if ( cls != null ) { FieldInfo field = cls.getFieldInfo(fieldName); if (field != null) { return field.getFieldRef(); } clsRef = cls.getClassRef(); } else { clsRef = new ClassRef(className); } return new FieldRef(clsRef, fieldName, null); } /** * Get a reference to a field using the given memberID. * * @param classRef The class which contains the field. * @param memberID The memberID of the field. Only memberName and memberDescriptor are used. The descriptor * defines the type of the field, if the field is unknown. * @return A field reference with or without FieldInfo or ClassInfo. */ public FieldRef getFieldRef(ClassRef classRef, MemberID memberID) { ClassInfo cls = classRef.getClassInfo(); if ( cls != null ) { // We do not check for inherited fields here, this is done in FieldRef FieldInfo field = cls.getFieldInfo(memberID.getMemberName()); if ( field != null ) { return field.getFieldRef(); } } Type type = null; if (memberID.hasDescriptor()) { type = memberID.getDescriptor().getType(); } return new FieldRef(classRef, memberID.getMemberName(), type); } /** * Find a MethodInfo using a class name and the given signature or name of a method. * This does not check superclasses for inherited methods. * * @see #getMethodInfoInherited(String, String) * @param className the fully qualified name of the class * @param methodSignature either the name of the method if unique, or the method signature. * @return the method * @throws MethodNotFoundException if the method is not found or if multiple matches are found. */ public MethodInfo getMethodInfo(String className, String methodSignature) throws MethodNotFoundException { ClassInfo classInfo = classes.get(className); if (classInfo == null) { throw new MethodNotFoundException("Could not find class for method "+className+"."+methodSignature); } // check signature first since this is faster MethodInfo method = classInfo.getMethodInfo(methodSignature); if (method == null) { Set<MethodInfo> candidates = classInfo.getMethodByName(methodSignature); if (candidates.size() == 1) { method = candidates.iterator().next(); } else { if (candidates.size() == 0) { throw new MethodNotFoundException("Could not find method "+className+"."+methodSignature); } else { throw new MethodNotFoundException("Multiple candidates for method "+className+"."+methodSignature); } } } return method; } public MethodInfo getMethodInfo(MemberID memberID) throws MethodNotFoundException { return getMethodInfo(memberID.getClassName(), memberID.getMethodSignature()); } /** * @param memberID at least a class name. * @return a set of all matching methods. * @throws MethodNotFoundException if the base class cannot be found */ public Collection<MethodInfo> getMethodInfos(MemberID memberID) throws MethodNotFoundException { String className = memberID.getClassName(); ClassInfo classInfo = classes.get(className); if (classInfo == null) { throw new MethodNotFoundException("Could not find class for method "+memberID); } if (!memberID.hasMemberName()) { // We could filter out methods by descriptor if it is set return classInfo.getMethods(); } return classInfo.getMethodInfos(memberID); } /** * Find a MethodInfo using a class name and the given signature of a method. * Only methods which are are accessible (i.e. inherited) by the class are returned. * * @see ClassInfo#getMethodInfoInherited(MemberID , boolean) * @param className the fully qualified name of the class * @param methodSignature the method signature with name and descriptor. * @return the method, or null if not found. */ public MethodInfo getMethodInfoInherited(String className, String methodSignature) { ClassInfo classInfo = getClassInfo(className); if (classInfo == null) return null; return classInfo.getMethodInfoInherited(methodSignature, true); } /** * Find a MethodInfo using a class name and the given memberID of a method. * Only methods which are are accessible (i.e. inherited) by the class are returned. * * @see ClassInfo#getMethodInfoInherited(MemberID , boolean) * @param memberID the full memberID with classname and method name and descriptor. * @return the method, or null if not found. */ public MethodInfo getMethodInfoInherited(MemberID memberID) { ClassInfo classInfo = getClassInfo(memberID.getClassName()); if (classInfo == null) return null; return classInfo.getMethodInfoInherited(memberID, true); } /** * Convenience method to implement CFGProvider. * * @param method the method to get the CFG for. * @return the CFG attached to the method's code. */ @Override public ControlFlowGraph getFlowGraph(MethodInfo method) { if (!method.hasCode()) return null; return method.getCode().getControlFlowGraph(false); } ////////////////////////////////////////////////////////////////////////////// // Roots ////////////////////////////////////////////////////////////////////////////// public void addRoot(ClassInfo classInfo) { roots.add(classInfo); } public void addRoot(MethodInfo methodInfo) { roots.add(methodInfo); } /** * @return a set of all classinfos of all roots. */ public Collection<ClassInfo> getRootClasses() { Set<ClassInfo> rootClasses = new LinkedHashSet<ClassInfo>(); for (MemberInfo root : roots) { rootClasses.add(root.getClassInfo()); } return rootClasses; } /** * Get a set of all root methods, i.e. all root methods and all methods in all root classes. * @return a set of all root methods. */ public Set<MethodInfo> getRootMethods() { Set<MethodInfo> methods = new LinkedHashSet<MethodInfo>(); for (MemberInfo root : roots) { addRootMethods(methods, root); } return methods; } /** * @return an unmodifiable set of all root methods and root classes. */ public Set<MemberInfo> getRoots() { return Collections.unmodifiableSet( roots ); } /** * This find all non JVM related root methods. * @return a set of all application root methods. */ public Set<MethodInfo> getAppRootMethods() { Set<MethodInfo> methods = new LinkedHashSet<MethodInfo>(); if (processor == null) { return getRootMethods(); } List<String> jvmClasses = processor.getJVMClasses(); List<String> nativeClasses = processor.getNativeClasses(); for (MemberInfo root : roots) { if (nativeClasses.contains(root.getClassName()) || jvmClasses.contains(root.getClassName())) { continue; } addRootMethods(methods, root); } return methods; } public Set<MethodInfo> getJvmRootMethods() { Set<MethodInfo> methods = new LinkedHashSet<MethodInfo>(); if (processor == null) { return methods; } List<String> jvmClasses = processor.getJVMClasses(); List<String> nativeClasses = processor.getNativeClasses(); for (MemberInfo root : roots) { if (nativeClasses.contains(root.getClassName()) || jvmClasses.contains(root.getClassName())) { addRootMethods(methods, root); } } return methods; } public Collection<MethodInfo> getClinitMethods() { List<MethodInfo> methods = new ArrayList<MethodInfo>(); for (ClassInfo cls : classes.values()) { MethodInfo clinit = cls.getMethodInfo(ClinitOrder.clinitSig); if (clinit != null) { methods.add(clinit); } } return methods; } /** * Find all Runnable.run() implementations from all the loaded classes. * @param callgraphRootsOnly if true and if a callgraph has been created, only check callgraph root classes. * This can be used to ignore unused Runnables by removing them from the callgraph (roots). * @return a set of methods which implement Runnable.run(). */ public Collection<MethodInfo> getThreadRootMethods(boolean callgraphRootsOnly) { List<MethodInfo> methods = new ArrayList<MethodInfo>(); Collection<ClassInfo> classList; if (callGraph != null && callgraphRootsOnly) { classList = callGraph.getRootClasses(); } else { classList = classes.values(); } for (ClassInfo cls : classList) { Ternary isRunnable = cls.hasSuperClass("java.lang.Runnable", true); if (isRunnable == Ternary.UNKNOWN) { // what if unsafe? We ignore for now, must be added as root manually; should we log? continue; } if (isRunnable == Ternary.TRUE) { MethodInfo run = cls.getMethodInfo("run()V"); if (run != null && !run.isAbstract()) { methods.add(run); } // TODO any other methods we might need to add? } } return methods; } /** * @param classInfo a classinfo to check. * @return true if this class implements Runnable and belongs to the JVM implementation. */ public boolean isJVMThread(ClassInfo classInfo) { // TODO make this check less hardcoded.. Move to ProcessorModel? if ("joprt".equals(classInfo.getPackageName()) || "com.jopdesign.sys".equals(classInfo.getPackageName())) { Ternary isRunnable = classInfo.hasSuperClass("java.lang.Runnable", true); if (isRunnable != Ternary.FALSE) { return true; } } return false; } private void addRootMethods(Set<MethodInfo> methods, MemberInfo root) { if (root instanceof MethodInfo) { methods.add((MethodInfo) root); } else if (root instanceof ClassInfo) { for (MethodInfo m : ((ClassInfo)root).getMethods()) { methods.add(m); } } else { throw new AppInfoError("Found fieldinfo "+root+" in roots, which is not allowed"); } } public void setMainMethod(MethodInfo main) { if ( main != null ) { addRoot(main); } mainMethod = main; } public MethodInfo getMainMethod() { return mainMethod; } public MemberID getMainSignature() { return mainMethod.getMemberID(); } public MemberID getClinitSignature(String className) { return new MemberID(className, ClinitOrder.clinitName, ClinitOrder.clinitDesc); } ////////////////////////////////////////////////////////////////////////////// // CallGraph ////////////////////////////////////////////////////////////////////////////// /** * @return the maximum length of the callstrings used in the callgraph */ public int getCallstringLength() { return callstringLength; } /** * Set the maximum length of the callstrings of the default callgraph. * <p> * The next call to {@link #getCallGraph()} will create a new callgraph if this value is changed. * </p> * @param callstringLength the new callstring length to use. */ public void setCallstringLength(int callstringLength) { this.callstringLength = callstringLength; } /** * Build a new default callgraph using the current roots as roots for the callgraph, if the default * callgraph has not yet been created. * @param rebuild if true, rebuild the graph if it already exists. All manual changes and optimizations * of the graph will be lost. * @return the default callgraph */ public CallGraph buildCallGraph(boolean rebuild) { if (rebuild) { // we set the callgraph null first, so that rebuilding it does not use the old graph callGraph = null; } if (callGraph == null) { CallgraphBuilder builder = new DefaultCallgraphBuilder(getCallstringLength()); buildCallGraph(builder); } return callGraph; } public CallGraph buildCallGraph(CallgraphBuilder builder) { // we need to set the callgraph after building it, so it will not be used while constructing it. callGraph = CallGraph.buildCallGraph(this, builder); return callGraph; } public boolean hasCallGraph() { return callGraph != null; } /** * Get the current default callgraph. * <p> * Changes to the classes, the roots or callstringLength are not reflected automatically in the callgraph, * call {@link #buildCallGraph(boolean)} with rebuild=true to ensure that the callgraph is uptodate, but * make sure that nobody holds any references to elements of the graph. * </p> * <p> * Note that you do not need to use this graph, you can create your own callgraph if required. * </p> * @return the callgraph starting at the AppInfo roots. */ public CallGraph getCallGraph() { return callGraph; } /** * Find all methods which might get invoked for a given invokesite. * This uses the callgraph returned by {@link #getCallGraph()} to lookup possible implementations. * Use callgraph thinning to make the result of this method more precise. * If the callgraph has not yet been built by {@link #buildCallGraph(boolean)}, this uses * {@link #findImplementations(MethodRef)} to resolve virtual invocations. * * @see #findImplementations(InvokeSite, CallString) * @param invokeSite the invokesite to look up * @return a list of possible implementations for the invocation including native methods, or an empty set if resolution fails or is not safe. */ public Set<MethodInfo> findImplementations(InvokeSite invokeSite) { return findImplementations(invokeSite, CallString.EMPTY); } /** * Find all methods which might get invoked for a given invokesite. * This uses the callgraph returned by {@link #getCallGraph()} to lookup possible implementations. * Use callgraph thinning to make the result of this method more precise. * If the callgraph has not yet been built by {@link #buildCallGraph(boolean)}, this uses * {@link #findImplementations(MethodRef)} to resolve virtual invocations. * * @param invokeSite the invokesite to look up * @param cs the callstring up to the method containing the invocation, excluding the given invokesite * @return a list of possible implementations for the invocation including native methods, or an empty set if resolution fails or is not safe. */ public Set<MethodInfo> findImplementations(InvokeSite invokeSite, CallString cs) { return findImplementations(cs.push(invokeSite)); } /** * Find all methods which might get invoked for a given invokesite. * This uses the callgraph returned by {@link #getCallGraph()} to lookup possible implementations. * Use callgraph thinning to make the result of this method more precise. * If the callgraph has not yet been built by {@link #buildCallGraph(boolean)}, this uses * {@link #findImplementations(MethodRef)} to resolve virtual invocations. * * @param cs the callstring to the the invocation, including the given invokesite. Must not be empty. * @return a list of possible implementations for the invocation including native methods, or an empty set if resolution fails or is not safe. */ public Set<MethodInfo> findImplementations(CallString cs) { if (cs.length() == 0) { throw new AssertionError("findImplementations() called with empty callstring!"); } InvokeSite invokeSite = cs.top(); // Handle special/static invokes // We could use the callgraph to check them too, but only if the callstring length of the // callgraph is at least one, else we will get incorrect results if (!invokeSite.isVirtual()) { Set<MethodInfo> methods = new LinkedHashSet<MethodInfo>(); MethodInfo method = invokeSite.getInvokeeRef().getMethodInfo(); if (method == null) { return methods; } if (method.isAbstract()) { throw new JavaClassFormatError("Invokespecial calls abstract method "+invokeSite.getInvokeeRef()); } methods.add(method); return methods; } if (callGraph == null) { // we do not have a callgraph, so just use typegraph info return findImplementations(invokeSite.getInvokeeRef()); } if (!callGraph.containsMethod(invokeSite.getInvoker())) { if (logger.isTraceEnabled()) { logger.trace("Could not find method "+invokeSite.getInvoker()+ " in the callgraph, falling back to typegraph"); } return findImplementations(invokeSite.getInvokeeRef()); } return callGraph.findImplementations(cs); } /** * Find all methods which might get invoked for a given methodRef. * This does not use the callgraph to eliminate methods. If you want a more precise result, * use {@link #findImplementations(InvokeSite, CallString)} and use callgraph thinning first. * <p> * Note that this method is slightly different from {@link MethodInfo#getImplementations(boolean)}, since * it returns only methods for subclasses of the invokee class, not of the implementing class. * </p> * <p>To handle invocations of super-methods correctly, use {@link #findImplementations(InvokeSite)} * instead.</p> * * @see #findImplementations(InvokeSite) * @see MethodInfo#overrides(MethodRef, boolean) * @param invokee the method to resolve. * @return all possible implementations, including native methods. */ public Set<MethodInfo> findImplementations(final MethodRef invokee) { final Set<MethodInfo> methods = new LinkedHashSet<MethodInfo>(); // 'method' may refer to an inherited MethodInfo or to an interface method if there is no implementation final MethodInfo method = invokee.getMethodInfo(); if (method != null && (method.isStatic() || method.isPrivate())) { methods.add(method); return methods; } final String methodSig = invokee.getMethodSignature(); final ClassInfo invokeeClass = invokee.getClassRef().getClassInfo(); if (invokeeClass == null) { // ok, now, if the target class is unknown, there is not much we can do, so return an empty set logger.debug("Trying to find implementations of a method in an unknown class "+invokee.toString()); return methods; } // Constructors are only called by invokespecial if ("<init>".equals(invokee.getName())) { MethodInfo init = invokee.getMethodInfo(); if (init == null) { throw new JavaClassFormatError("Constructor not found: "+invokee); } if (init.isAbstract()) { throw new JavaClassFormatError("Found abstract constructor, this isn't right..: "+invokee); } methods.add(init); return methods; } boolean undefinedBaseMethod = false; // check if method is defined in the referenced class or in a superclass if (invokeeClass.getMethodInfo(methodSig) == null) { // method is inherited, add to implementations if (method != null && !method.isAbstract()) { methods.add(method); } else if (method == null) { // hm, invoke to an unknown method (maybe excluded or native), what should we do? if (invokeeClass.isFullyKnown(true)) { // .. or maybe the method has not been loaded somehow when the MethodRef was created (check!) throw new JavaClassFormatError("Method implementation not found in superclass: "+invokee.toString()); } else { // maybe defined in excluded superclass, but we do not know for sure.. // We *must* return an empty set, but lets try to continue for now and // handle it like an excluded class, and abort only if we find overriding methods logger.debug("Method implementation not found in incomplete superclass: "+invokee.toString()); undefinedBaseMethod = true; } } } // now, we have a virtual call on our hands .. ClassVisitor visitor = new ClassVisitor() { public boolean visitClass(ClassInfo classInfo) { // Note: we also handle interface classes here, because they can contain <clinit> methods MethodInfo m; if (invokeeClass.isInterface() && !classInfo.isInterface()) { // If we invoke an interface method, we also need to find inherited methods in implementing // classes m = classInfo.getMethodInfoInherited(methodSig,true); } else { // If we do not invoke an interface method, 'method' is already the only possible inherited // method; If the visited class is an interface, it does not inherit implementations. m = classInfo.getMethodInfo(methodSig); } if ( m != null ) { if ( m.isPrivate() && !classInfo.equals(invokeeClass)) { // found an overriding method which is private .. this is interesting.. logger.error("Found private method "+m.getMethodSignature()+" in "+ classInfo.getClassName()+" overriding non-private method in "+ invokee.getClassName()); } if ( !m.isAbstract() && (method == null || m.overrides(method,false)) ) { methods.add(m); } } return true; } public void finishClass(ClassInfo classInfo) { } }; ClassHierarchyTraverser traverser = new ClassHierarchyTraverser(visitor); traverser.setVisitSubclasses(true, true); traverser.traverseDown(invokeeClass); if (undefinedBaseMethod && methods.size() > 0) { // now this is a problem: base implementation is unknown but we have some // overriding methods, this we cannot handle for now throw new JavaClassFormatError("Found overriding methods for "+invokee+" but superclasses are undefined!"); } return methods; } ////////////////////////////////////////////////////////////////////////////// // Class loading configuration, processor model ////////////////////////////////////////////////////////////////////////////// public ProcessorModel getProcessorModel() { return processor; } public void setProcessorModel(ProcessorModel processor) { this.processor = processor; } /** * Add the name of a library class or a library package. * Libraries must not contain references to application code classes. * * @see #setLoadLibraries(boolean) * @param libraryClass the FQN of a library class or a package of a library. */ public void addLibrary(String libraryClass) { libraryClasses.add(libraryClass); } /** * Add the name of a class or a package which should not be loaded. * If anything is added here, the tools must be able to handle missing classes. * * <p>Note that this is independent of the value of {@link #doIgnoreMissingClasses()}.</p> * * @param ignoredClass the FQN of a class or package to exclude from loading. */ public void addIgnored(String ignoredClass) { ignoredClasses.add(ignoredClass); } /** * If this is set to true, a missing class or unreadable class file will be ignored * and {@link #getClassInfo(String)} and {@link #loadClass(String)} (and their variants) will * return {@code null} instead. Else, a missing and not excluded class will trigger an {@link MissingClassError}. * * <p>This affects only classes which are not excluded by other means. If this is set to true, * the tools must be able to handle missing classes.</p> * * @param ignoreMissingClasses if true, ignore class load errors. */ public void setIgnoreMissingClasses(boolean ignoreMissingClasses) { this.ignoreMissingClasses = ignoreMissingClasses; } /** * Check if missing and not excluded or invalid class files do not trigger an Error. * @see #setIgnoreMissingClasses(boolean) * @return true if missing or invalid classes do not trigger an error on load. */ public boolean doIgnoreMissingClasses() { return ignoreMissingClasses; } public boolean doLoadLibraries() { return loadLibraries; } public void setLoadLibraries(boolean loadLibraries) { this.loadLibraries = loadLibraries; } public boolean doLoadNatives() { return loadNatives; } /** * Set to true to load native classes too. * * @param loadNatives if true, load native classes too. */ public void setLoadNatives(boolean loadNatives) { this.loadNatives = loadNatives; } /** * @see #setExitOnMissingClass(boolean) * @return true, if {@link #loadClass(String)} or {@link #getClassInfo(String)} exists instead of throwing an Error. */ public boolean doExitOnMissingClass() { return exitOnMissingClass; } /** * If set to true, the application will exit with an error message instead of throwing an * unchecked {@link MissingClassError} if a non-excluded class cannot be loaded in * {@link #loadClass(String)} or {@link #getClassInfo(String)}. * * @param exitOnMissingClass if true, exit with an error message instead of throwing an error. */ public void setExitOnMissingClass(boolean exitOnMissingClass) { this.exitOnMissingClass = exitOnMissingClass; } public boolean isNative(String className) { return matchClassName(className, processor.getNativeClasses(), false); } public boolean isLibrary(String className) { return matchClassName(className, libraryClasses, false); } public boolean isIgnored(String className) { return matchClassName(className, ignoredClasses, false); } /** * Check if the classname is excluded from loading, * either because it is a native or library class (and loading is disabled for them) * or if the class is ignored. * * @param className the fully qualified class name to check. * @return true if it should be excluded from loading. */ public boolean isExcluded(String className) { if ( !loadNatives && isNative(className) ) { return true; } if ( !loadLibraries && isLibrary(className) ) { return true; } return isIgnored(className); } /** * @param hwObject the fully qualified class name of a hardware class, the superclass of a hardware * class, or a package name which contains hardware objects. */ public void addHwObjectName(String hwObject) { hwObjectClasses.add(hwObject); } public boolean isHwObject(ClassInfo classInfo) { return isHwObject(classInfo.getClassName()); } /** * Check if a class is a hardware object, i.e. it represents a hardware interface * and must not be modified. * * @param className the full class name * @return true if the class is a hardware interface. */ public boolean isHwObject(String className) { return matchClassName(className, hwObjectClasses, true); } ////////////////////////////////////////////////////////////////////////////// // Caching support ////////////////////////////////////////////////////////////////////////////// public void setDumpCacheKeyFile(String dumpCacheKeyFile) { this.dumpCacheKeyFile = dumpCacheKeyFile; } public boolean updateCheckSum(MethodInfo prologue) { // Also compute SHA-1 checksum for this DFA problem // (for caching purposes) MessageDigest md, md2; try { md = MessageDigest.getInstance("SHA1"); md2 = MessageDigest.getInstance("SHA1"); } catch (NoSuchAlgorithmException e) { logger.info("No digest algorithm found", e); digest = null; return false; } PrintWriter writer = null; File tempFile = null; if (dumpCacheKeyFile != null) { try { tempFile = new File(dumpCacheKeyFile +"-temp.txt"); writer = new PrintWriter(tempFile); } catch (FileNotFoundException e) { e.printStackTrace(); } } if (prologue != null) { // TODO the prologue method is a hack, we should not use it in the checksum // instead we should only hash the required infos (entry-method, clinit-order?,..) updateChecksum(prologue, md); } List<String> classNames = new ArrayList<String>(getClassNames()); // We iterate in lexical order to make MD5 checksum a bit more deterministic .. Collections.sort(classNames); for (String name : classNames) { ClassInfo ci = getClassInfo(name); List<String> methodNames = new ArrayList<String>(ci.getMethodSignatures()); Collections.sort(methodNames); for (String method : methodNames) { MethodInfo mi = ci.getMethodInfo(method); if (mi.hasCode()) { updateChecksum(mi, md); if (writer != null) { writer.print("M "+mi+": "); updateChecksum(mi, md2); this.digest = md2.digest(); writer.println(getDigestString()); } } } ConstantPool cp = ci.getConstantPoolGen().getFinalConstantPool(); updateCheckSum(cp, md); if (writer != null) { writer.print("CP " + ci + ": "); updateCheckSum(cp, md2); this.digest = md2.digest(); writer.print(cp.getLength()+" "); writer.println(getDigestString()); } } this.digest = md.digest(); logger.info("AppInfo has checksum: " + getDigestString()); if (tempFile != null && writer != null) { writer.close(); File dest = new File(dumpCacheKeyFile + "-" + getDigestString() + ".txt"); //noinspection ResultOfMethodCallIgnored dest.delete(); tempFile.renameTo(dest); } return true; } private static final char[] digits = "0123456789abcdef".toCharArray(); public String getDigestString() { StringBuilder sb = new StringBuilder(); for (byte b : digest) { int v = b < 0 ? (256 + b) : b; sb.append(digits[v >> 4]); sb.append(digits[v & 0xF]); } return sb.toString(); } private void updateCheckSum(ConstantPool cp, MessageDigest md) { if (md == null) return; ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); try { cp.dump(dos); } catch (IOException e) { logger.error("Dumping the constant pool (checksum calculation) failed: " + e.getMessage()); throw new AppInfoError(e); } md.update(bos.toByteArray()); } private static void updateChecksum(MethodInfo mi, MessageDigest md) { if (md == null) return; ByteArrayOutputStream bos = new ByteArrayOutputStream(); OutputStreamWriter writer = new OutputStreamWriter(bos); try { writer.append(mi.getFQMethodName()); writer.close(); } catch (IOException e) { throw new AppInfoError(e); } md.update(bos.toByteArray()); // finally, also add the code md.update(mi.getCode().getInstructionList().getByteCode()); } ////////////////////////////////////////////////////////////////////////////// // Internal Affairs ////////////////////////////////////////////////////////////////////////////// private boolean matchClassName(String className, Collection<String> list, boolean matchSuper) { ClassInfo cls = null; if (matchSuper) { cls = classes.get(className); } for (String s : list) { if (className.equals(s) || className.startsWith(s + ".")) { return true; } if (cls != null) { // TODO we could also check if any superclass has 'className' as package prefix Ternary rs = cls.hasSuperClass(s, true); // Hmm, what to do if the superclass check is not safe (ie result is UNKNOWN)? // We just do not match for now .. if (rs == Ternary.TRUE) return true; } } return false; } private ClassInfo performLoadClass(String className, boolean required) throws ClassInfoNotFoundException { // try to load the class ClassInfo cls = null; try { cls = tryLoadClass(className); classes.put(className, cls); for (AppEventHandler mgr : eventHandlers) { mgr.onCreateClass(cls,true); } } catch (IOException e) { if ( required || !ignoreMissingClasses) { throw new ClassInfoNotFoundException("Class '"+className+"' could not be loaded: " + e.getMessage(), e); } else cls = null; } return cls; } private ClassInfo tryLoadClass(String className) throws IOException { loadLogger.debug("Loading class "+className); InputStream is = classPath.getInputStream(className); JavaClass javaClass = new ClassParser(is, className).parse(); is.close(); if (javaClass.getMajor() > 50) { // TODO this requires some work: Java 7 introduces new Attributes (must be parsed correctly and // handled by the UsedCodeFinder etc), new constantpool entry types a new invokedynamic // instruction (requires patching of BCEL code similar to Classpath and InstructionFinder) throw new JavaClassFormatError ("Classfiles with versions 51.0 (Java 7) and above are currently not supported!"); } return new ClassInfo(new ClassGen(javaClass)); } private boolean checkClassExists(String className) { try { return classPath.getClassFile(className) != null; } catch (IOException ignored) { return false; } } private ClassInfo createClassInfo(String className, String superClassName, boolean isInterface) { String filename = className.replace(".", File.separator) + ".class"; int af = Constants.ACC_PUBLIC; if ( isInterface ) { af |= Constants.ACC_INTERFACE; } ClassGen clsGen = new ClassGen(className, superClassName, filename, af, new String[0]); return new ClassInfo(clsGen); } private void handleClassLoadFailure(String message, Exception cause) { // Nah, just throw an error anyway, so that we have a stacktrace /* if ( exitOnMissingClass ) { logger.error(message, cause); System.exit(4); } */ throw new MissingClassError(message, cause); } }