/*
* This file is part of JOP, the Java Optimized Processor
* see <http://www.jopdesign.com/>
*
* Copyright (C) 2010, Stefan Hepp (stefan@stefant.org).
* Copyright (C) 2008, Wolfgang Puffitsch
*
* 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.dfa;
import com.jopdesign.common.AppEventHandler;
import com.jopdesign.common.AppInfo;
import com.jopdesign.common.AppSetup;
import com.jopdesign.common.ClassInfo;
import com.jopdesign.common.EmptyTool;
import com.jopdesign.common.FieldInfo;
import com.jopdesign.common.KeyManager.CustomKey;
import com.jopdesign.common.KeyManager.KeyType;
import com.jopdesign.common.MemberInfo.AccessType;
import com.jopdesign.common.MethodCode;
import com.jopdesign.common.MethodInfo;
import com.jopdesign.common.code.CallString;
import com.jopdesign.common.code.ExecutionContext;
import com.jopdesign.common.config.Config;
import com.jopdesign.common.config.Config.BadConfigurationException;
import com.jopdesign.common.config.StringOption;
import com.jopdesign.common.graphutils.Pair;
import com.jopdesign.common.misc.MethodNotFoundException;
import com.jopdesign.common.tools.ClinitOrder;
import com.jopdesign.common.tools.UpdatePositions;
import com.jopdesign.common.type.Descriptor;
import com.jopdesign.common.type.MemberID;
import com.jopdesign.dfa.analyses.CallStringReceiverTypes;
import com.jopdesign.dfa.analyses.LoopBounds;
import com.jopdesign.dfa.analyses.SymbolicPointsTo;
import com.jopdesign.dfa.analyses.ValueMapping;
import com.jopdesign.dfa.framework.Analysis;
import com.jopdesign.dfa.framework.AnalysisResultSerialization;
import com.jopdesign.dfa.framework.Context;
import com.jopdesign.dfa.framework.ContextMap;
import com.jopdesign.dfa.framework.Flow;
import com.jopdesign.dfa.framework.FlowEdge;
import com.jopdesign.dfa.framework.Interpreter;
import org.apache.bcel.generic.ACONST_NULL;
import org.apache.bcel.generic.BranchInstruction;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.GOTO;
import org.apache.bcel.generic.ICONST;
import org.apache.bcel.generic.INVOKESTATIC;
import org.apache.bcel.generic.Instruction;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.NOP;
import org.apache.bcel.generic.ReturnInstruction;
import org.apache.bcel.generic.Select;
import org.apache.bcel.generic.UnconditionalBranch;
import org.apache.log4j.Logger;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* Tool for dataflow analysis
*/
public class DFATool extends EmptyTool<AppEventHandler> {
private static final String prologueName = "<prologue>";
private static final String prologueSig = "()V";
// Root logger
public static final String LOG_DFA = "dfa";
public static final String LOG_DFA_ANALYSES = "dfa.analyses";
public static final String LOG_DFA_FRAMEWORK = "dfa.framework";
private static final Logger logger = Logger.getLogger(LOG_DFA + ".DFATool");
private AppInfo appInfo;
private MethodInfo prologue;
private boolean analyzeBootMethod;
private List<InstructionHandle> statements;
private Flow flow;
private CallStringReceiverTypes receivers;
private LoopBounds loopBounds;
/**
* This key is used to attach a NOP instruction handle to each method which is used as handle for
* the result state of a method.
*/
private CustomKey KEY_NOP;
private File cacheDir = null;
public DFATool() {
super("head");
this.appInfo = AppInfo.getSingleton();
this.statements = new LinkedList<InstructionHandle>();
this.flow = new Flow();
this.receivers = null;
this.analyzeBootMethod = false;
}
public AppInfo getAppInfo() {
return appInfo;
}
@Override
public void registerOptions(Config config) {
config.addOption(OPT_DFA_CACHE_DIR);
}
@Override
public void onSetupConfig(AppSetup setup) throws Config.BadConfigurationException {
if (setup.getConfig().getOption(OPT_DFA_CACHE_DIR) != null) {
this.cacheDir = new File(setup.getConfig().getOption(OPT_DFA_CACHE_DIR));
}
}
@Override
public void onSetupAppInfo(AppSetup setup, AppInfo appInfo) throws BadConfigurationException {
// We do not call load() here, because some other tool might want to modify the code before
// running the DFA the first tool..
KEY_NOP = appInfo.getKeyManager().registerKey(KeyType.STRUCT, "dfa.nop");
}
public void setAnalyzeBootMethod(boolean analyzeBootMethod) {
this.analyzeBootMethod = analyzeBootMethod;
}
public boolean doUseCache() {
return cacheDir != null;
}
/**
* Load the methods into the internal DFA structures and initialize the DFA tool for a new analysis.
* You need to call this method before starting the first analysis and before starting an analysis after
* the code has been modified.
*/
public void load() {
// First clear everything ..
statements.clear();
flow.clear();
receivers = null;
prologue = createPrologue();
// Now we need to process all classes (for DFA's internal flow graph)
for (ClassInfo cls : appInfo.getClassInfos()) {
for (MethodInfo mi : cls.getMethods()) {
if (mi.hasCode()) {
loadMethod(mi);
}
}
}
if (cacheDir != null && !appInfo.updateCheckSum(prologue)) {
cacheDir = null;
}
}
private MethodInfo createPrologue() {
// find ordering for class initializers
ClinitOrder c = new ClinitOrder();
appInfo.iterate(c);
List<ClassInfo> order = c.findOrder();
MethodInfo mainClass = appInfo.getMainMethod();
// create prologue
return buildPrologue(mainClass, statements, flow, order);
}
/**
* Remove all helper objects. You need to run {@link #load()} again before performing a new analysis.
*/
public void cleanup() {
appInfo.getKeyManager().clearAllValues(KEY_NOP);
flow.clear();
statements.clear();
}
private void loadMethod(MethodInfo method) {
MethodCode mcode = method.getCode();
// TODO is there a better way to get an instruction handle? Do we need to keep the list somehow?
InstructionHandle exit;
exit = (InstructionHandle) method.getCustomValue(KEY_NOP);
// we reuse the NOP if it exists, so that we can have multiple DFAs running at the same time
if (exit == null) {
exit = new InstructionList(new NOP()).getStart();
// We do not really want to modify the REAL instruction list and append exit
// (SH) Fixed :) Yep, we need the NOP somewhere, else doInvoke() will collect the wrong result state.
// But we can eliminate the NOP by adding the instruction not to the list, but instead to the
// MethodInfo, and also retrieve it from there.
method.setCustomValue(KEY_NOP, exit);
}
this.getStatements().add(exit);
// We do not modify the code, so we leave existing CFGs alone, just make sure the instruction list is uptodate
InstructionList il = mcode.getInstructionList(true, false);
// we need correct positions for the DFA cache serialization stuff
il.setPositions();
for (Iterator<?> l = il.iterator(); l.hasNext(); ) {
InstructionHandle handle = (InstructionHandle) l.next();
this.getStatements().add(handle);
Instruction instr = handle.getInstruction();
if (instr instanceof BranchInstruction) {
if (instr instanceof Select) {
Select s = (Select) instr;
InstructionHandle[] target = s.getTargets();
for (InstructionHandle aTarget : target) {
this.getFlow().addEdge(new FlowEdge(handle, aTarget,
FlowEdge.TRUE_EDGE));
}
this.getFlow().addEdge(new FlowEdge(handle, s.getTarget(),
FlowEdge.FALSE_EDGE));
} else {
BranchInstruction b = (BranchInstruction) instr;
this.getFlow().addEdge(new FlowEdge(handle, b.getTarget(),
FlowEdge.TRUE_EDGE));
}
}
if (handle.getNext() != null
&& !(instr instanceof UnconditionalBranch
|| instr instanceof Select || instr instanceof ReturnInstruction)) {
if (instr instanceof BranchInstruction) {
this.getFlow().addEdge(new FlowEdge(handle, handle.getNext(),
FlowEdge.FALSE_EDGE));
} else {
this.getFlow().addEdge(new FlowEdge(handle, handle.getNext(),
FlowEdge.NORMAL_EDGE));
}
}
if (instr instanceof ReturnInstruction) {
this.getFlow().addEdge(new FlowEdge(handle, exit,
FlowEdge.NORMAL_EDGE));
}
}
}
public InstructionHandle getEntryHandle(MethodInfo method) {
// for symmetry, lets also provide a getter for the first instruction ..
MethodCode mCode = method.getCode();
// since we already compiled any existing CFG in load(), there is no need to do it again here
return mCode.getInstructionList(false, false).getStart();
}
public InstructionHandle getExitHandle(MethodInfo method) {
return (InstructionHandle) method.getCustomValue(KEY_NOP);
}
private MethodInfo buildPrologue(MethodInfo mainMethod, List<InstructionHandle> statements, Flow flow, List<ClassInfo> clinits) {
// we use a prologue sequence for startup
InstructionList prologue = new InstructionList();
ConstantPoolGen prologueCP = mainMethod.getConstantPoolGen();
Instruction instr;
int idx;
// add magic initializers to prologue sequence
if (!analyzeBootMethod) {
instr = new ICONST(0);
prologue.append(instr);
instr = new ICONST(0);
prologue.append(instr);
idx = prologueCP.addMethodref("com.jopdesign.sys.GC", "init", "(II)V");
instr = new INVOKESTATIC(idx);
prologue.append(instr);
}
// Not in prologue anymore
// idx = prologueCP.addMethodref("java.lang.System", "<init>", "()V");
// instr = new INVOKESTATIC(idx);
// prologue.append(instr);
// add class initializers
for (ClassInfo clinit : clinits) {
MemberID cSig = appInfo.getClinitSignature(clinit.getClassName());
idx = prologueCP.addMethodref(cSig.getClassName(), cSig.getMemberName(),
cSig.getDescriptor().toString());
instr = new INVOKESTATIC(idx);
prologue.append(instr);
}
if (analyzeBootMethod) {
// Let's just analyze the full boot method, so that the callgraph-builder is happy.
// Doing this after clinit, so that we have DFA infos on fields in JVMHelp.init()
idx = prologueCP.addMethodref("com.jopdesign.sys.Startup", "boot", "()V");
instr = new INVOKESTATIC(idx);
prologue.append(instr);
}
// add main method
instr = new ACONST_NULL();
prologue.append(instr);
idx = prologueCP.addMethodref(mainMethod.getClassName(), mainMethod.getShortName(),
mainMethod.getDescriptor().toString());
instr = new INVOKESTATIC(idx);
prologue.append(instr);
// // invoke startMission() to ensure analysis of threads
// idx = prologueCP.addMethodref("joprt.RtThread", "startMission", "()V");
// instr = new INVOKESTATIC(idx);
// prologue.append(instr);
instr = new NOP();
prologue.append(instr);
prologue.setPositions(true);
// System.out.println(prologue);
// add prologue to program structure
for (Iterator l = prologue.iterator(); l.hasNext(); ) {
InstructionHandle handle = (InstructionHandle) l.next();
statements.add(handle);
if (handle.getInstruction() instanceof GOTO) {
GOTO g = (GOTO) handle.getInstruction();
flow.addEdge(new FlowEdge(handle, g.getTarget(), FlowEdge.NORMAL_EDGE));
} else if (handle.getNext() != null) {
flow.addEdge(new FlowEdge(handle, handle.getNext(), FlowEdge.NORMAL_EDGE));
}
}
MemberID pSig = new MemberID(prologueName, Descriptor.parse(prologueSig));
MethodInfo mi = mainMethod.getClassInfo().createMethod(pSig, null, prologue);
mi.setAccessType(AccessType.ACC_PRIVATE);
return mi;
}
public Map<InstructionHandle, ContextMap<CallString, Set<String>>> runReceiverAnalysis(int callstringLength) {
CallStringReceiverTypes recTys = new CallStringReceiverTypes(callstringLength);
@SuppressWarnings({"unchecked"})
Map<InstructionHandle, ContextMap<CallString, Set<String>>> receiverResults = runAnalysis(recTys);
setReceivers(recTys);
return receiverResults;
}
public void runLoopboundAnalysis(int callstringLength) {
LoopBounds dfaLoopBounds = new LoopBounds(callstringLength);
runAnalysis(dfaLoopBounds);
setLoopBounds(dfaLoopBounds);
}
@SuppressWarnings("unchecked")
public Map runAnalysis(Analysis analysis) {
/* use cached results if possible */
Map results;
if ((results = getCachedResults(analysis)) != null) {
logger.warn("Analysis " + analysis.getId() + ": Using cached DFA analysis results");
return results;
}
Interpreter interpreter = new Interpreter(analysis, this);
MethodInfo main = appInfo.getMainMethod();
MethodInfo prologue = main.getClassInfo().getMethodInfo(prologueName + prologueSig);
Context context = new Context();
context.stackPtr = 0;
context.syncLevel = 0;
context.setMethodInfo(prologue);
analysis.initialize(main, context);
InstructionHandle entry = prologue.getCode().getInstructionList().getStart();
interpreter.interpret(context, entry, new LinkedHashMap(), true);
/* cache results if requested */
writeCachedResults(analysis);
return analysis.getResult();
}
public <K, V>
Map runLocalAnalysis(Analysis<K,V> localAnalysis, ExecutionContext scope) {
Interpreter<K, V> interpreter = new Interpreter<K, V>(localAnalysis, this);
if (scope == null) throw new AssertionError("No such method: " + scope);
Context context = new Context();
MethodCode entryCode = scope.getMethodInfo().getCode();
context.stackPtr = entryCode.getMaxLocals();
context.setMethodInfo(scope.getMethodInfo());
context.setCallString(scope.getCallString());
localAnalysis.initialize(scope.getMethodInfo(), context);
/* Here used to be a extremely-hard-to-trackdown bug!
* Without the boolean parameters, getInstructionList() will dispose
* the CFG, a very bad idea during WCET analysis which relies on
* pointer equality for CFG edges :(
*/
InstructionHandle entry = entryCode.getInstructionList(false,false).getStart();
interpreter.interpret(context, entry, new LinkedHashMap<InstructionHandle, ContextMap<K, V>>(), true);
return localAnalysis.getResult();
}
public List<InstructionHandle> getStatements() {
return statements;
}
public Flow getFlow() {
return flow;
}
public Set<String> getReceivers(InstructionHandle stmt, CallString cs) {
ContextMap<CallString, Set<String>> map = receivers.getResult().get(stmt);
if (map == null) {
return null;
}
Set<String> retval = new LinkedHashSet<String>();
for (CallString c : map.keySet()) {
if (c.hasSuffix(cs)) {
retval.addAll(map.get(c));
}
}
return retval;
}
public Set<MethodInfo> getReceiverMethods(InstructionHandle stmt, CallString cs) {
Set<String> receivers = getReceivers(stmt, cs);
Set<MethodInfo> methods = new LinkedHashSet<MethodInfo>(receivers.size());
for (String rcv : receivers) {
MemberID mID = MemberID.parse(rcv);
methods.add(appInfo.getMethodInfoInherited(mID));
}
return methods;
}
public void setReceivers(CallStringReceiverTypes receivers) {
this.receivers = receivers;
}
public LoopBounds getLoopBounds() {
return loopBounds;
}
public void setLoopBounds(LoopBounds lb) {
this.loopBounds = lb;
}
/**
* Helper method to find the actually invoked method given the
* dynamic type and the method signature
*
* @param recvStr the dynamic type of the receiver
* @param sigStr the signature (without class) of the method.
* @return the actually invoked method, or {@code null} if not found
*/
public MethodInfo getMethod(String recvStr, String sigStr) {
return appInfo.getMethodInfoInherited(recvStr, sigStr);
}
/**
* Helper method to find the actually invoked method given the
* dynamic type and the method signature
*
* @param signature FQ signature of the method
* @return the invoked method, or {@code null} if not found
*/
public MethodInfo getMethod(String signature) {
return appInfo.getMethodInfoInherited(MemberID.parse(signature, true));
}
public boolean containsField(String fieldName) {
MemberID s = MemberID.parse(fieldName, true);
return classForField(s.getClassName(), s.getMemberName()) != null;
}
public ClassInfo classForField(String className, String fieldName) {
ClassInfo cls = getAppInfo().getClassInfo(className);
if (cls == null) {
logger.info("Unknown class as potential receiver of field access" + className);
return null;
}
// TODO maybe we *do* want to check access here...
FieldInfo field = cls.getFieldInfoInherited(fieldName, false);
return field != null ? field.getClassInfo() : null;
}
public String dumpDFA(MethodInfo method) {
if (getLoopBounds() == null) return "n/a";
if (method.isAbstract()) {
return "n/a";
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream os = new PrintStream(baos);
Map<InstructionHandle, ContextMap<CallString, Pair<ValueMapping, ValueMapping>>> results = getLoopBounds().getResult();
if (results == null) return "n/a";
AnalysisResultSerialization<Pair<ValueMapping, ValueMapping>> printer =
new AnalysisResultSerialization<Pair<ValueMapping, ValueMapping>>();
for (Entry<InstructionHandle, ContextMap<CallString, Pair<ValueMapping, ValueMapping>>> ihEntry :
results.entrySet()) {
for (Entry<CallString, Pair<ValueMapping, ValueMapping>> csEntry :
ihEntry.getValue().entrySet()) {
if (ihEntry.getValue().getContext().getMethodInfo().equals(method)) {
printer.addResult(method, ihEntry.getKey().getPosition(), csEntry.getKey(), csEntry.getValue());
}
}
}
printer.dump(os);
try {
return baos.toString("UTF-8");
} catch (UnsupportedEncodingException e) {
return baos.toString();
}
}
/* Updating DFA results iteratively */
/* -------------------------------- */
/**
* @param method the method containing the new instructions
* @param newHandles key is old handle, value is new handle. Value can be null.
*/
public void copyResults(MethodInfo method, Map<InstructionHandle,InstructionHandle> newHandles) {
// TODO this is sort of a hack for now .. We should support updating call strings as well
if (receivers != null) {
receivers.copyResults(method, newHandles);
}
if (loopBounds != null) {
loopBounds.copyResults(method, newHandles);
}
}
/* Caching DFA results */
/* ------------------- */
public static final StringOption OPT_DFA_CACHE_DIR =
new StringOption("dfa-cache-dir", "If dataflow analysis results should " +
"be cached, specify a cache dir to store the results in", true);
/**
* Recalculate the checksum and write all results to the cache files.
* @param recreatePrologue if true, reconstruct the prologue
*/
public void writeCachedResults(boolean recreatePrologue) {
if (recreatePrologue) {
// TODO this is a BIG hack, we should remove the prologue from the cache key
// recreation is needed because the optimizer removes the prologue
// and we need to create the correct constant-pool entries
cleanup();
prologue = createPrologue();
}
appInfo.iterate(new UpdatePositions());
appInfo.updateCheckSum(prologue);
writeCachedResults(loopBounds);
writeCachedResults(receivers);
}
/**
* If caching is enabled, safe the cached results for the given analysis
*/
public void writeCachedResults(Analysis analysis) {
if (cacheDir == null) return;
try {
analysis.serializeResult(getCacheFile(analysis));
} catch (IOException e) {
logger.error("Failed to serialize analysis results: " + e);
}
}
/**
* If caching is enabled, look for cached results for the given analysis
*/
private Map getCachedResults(Analysis analysis) {
if (cacheDir == null) return null;
File cacheFile = getCacheFile(analysis);
try {
if (!cacheFile.exists()) return null;
return analysis.deSerializeResult(appInfo, cacheFile);
} catch (IOException ex) {
logger.error("Deserialization of " + analysis.getId() + " result failed", ex);
} catch (ClassNotFoundException ex) {
logger.error("Deserialization of " + analysis.getId() + " result failed", ex);
} catch (MethodNotFoundException ex) {
logger.error("Deserialization of " + analysis.getId() + " result failed", ex);
}
return null;
}
private File getCacheFile(Analysis analysis) {
if (cacheDir == null) {
throw new AssertionError("Invariant violated: getCacheFile should only be called if cacheDir is non-null");
}
String key = analysis.getId() + "-" + appInfo.getDigestString();
String cacheFile = "dfa-" + key + ".dat";
return new File(cacheDir, cacheFile);
}
}