pascal.taie.frontend.soot.SootWorldBuilder Maven / Gradle / Ivy
Show all versions of tai-e Show documentation
/*
* Tai-e: A Static Analysis Framework for Java
*
* Copyright (C) 2022 Tian Tan
* Copyright (C) 2022 Yue Li
*
* This file is part of Tai-e.
*
* Tai-e is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* Tai-e 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 Lesser General
* Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with Tai-e. If not, see .
*/
package pascal.taie.frontend.soot;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import pascal.taie.AbstractWorldBuilder;
import pascal.taie.World;
import pascal.taie.analysis.pta.PointerAnalysis;
import pascal.taie.analysis.pta.plugin.reflection.LogItem;
import pascal.taie.config.AnalysisConfig;
import pascal.taie.config.Options;
import pascal.taie.language.classes.ClassHierarchy;
import pascal.taie.language.classes.ClassHierarchyImpl;
import pascal.taie.language.classes.StringReps;
import pascal.taie.language.type.TypeSystem;
import pascal.taie.language.type.TypeSystemImpl;
import soot.G;
import soot.PackManager;
import soot.Scene;
import soot.SceneTransformer;
import soot.SootResolver;
import soot.Transform;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static soot.SootClass.HIERARCHY;
public class SootWorldBuilder extends AbstractWorldBuilder {
private static final Logger logger = LogManager.getLogger(SootWorldBuilder.class);
/**
* Path to the file which specifies the basic classes that should be
* added to Scene in advance.
*/
private static final String BASIC_CLASSES = "basic-classes.yml";
@Override
public void build(Options options, List analyses) {
initSoot(options, analyses, this);
// set arguments and run soot
List args = new ArrayList<>();
// set class path
Collections.addAll(args, "-cp", getClassPath(options));
// set main class
String mainClass = options.getMainClass();
if (mainClass != null) {
Collections.addAll(args, "-main-class", mainClass, mainClass);
}
// add input classes
args.addAll(getInputClasses(options));
runSoot(args.toArray(new String[0]));
}
private static void initSoot(Options options, List analyses,
SootWorldBuilder builder) {
// reset Soot
G.reset();
// set Soot options
soot.options.Options.v().set_output_dir(
new File(options.getOutputDir(), "sootOutput").toString());
soot.options.Options.v().set_output_format(
soot.options.Options.output_format_jimple);
soot.options.Options.v().set_keep_line_number(true);
soot.options.Options.v().set_app(true);
// exclude jdk classes from application classes
soot.options.Options.v().set_exclude(List.of("jdk.*", "apple.laf.*"));
soot.options.Options.v().set_whole_program(true);
soot.options.Options.v().set_no_writeout_body_releasing(true);
soot.options.Options.v().setPhaseOption("jb", "preserve-source-annotations:true");
soot.options.Options.v().setPhaseOption("jb", "model-lambdametafactory:false");
soot.options.Options.v().setPhaseOption("cg", "enabled:false");
if (options.isPrependJVM()) {
// TODO: figure out why -prepend-classpath makes Soot faster
soot.options.Options.v().set_prepend_classpath(true);
}
if (options.isAllowPhantom()) {
soot.options.Options.v().set_allow_phantom_refs(true);
}
if (options.isPreBuildIR()) {
// we need to set this option to false when pre-building IRs,
// otherwise Soot throws RuntimeException saying
// "No method source set for method ...".
// TODO: figure out the reason of "No method source"
soot.options.Options.v().set_drop_bodies_after_load(false);
}
Scene scene = G.v().soot_Scene();
addBasicClasses(scene);
addReflectionLogClasses(analyses, scene);
// Configure Soot transformer
Transform transform = new Transform(
"wjtp.tai-e", new SceneTransformer() {
@Override
protected void internalTransform(String phaseName, Map opts) {
builder.build(options, Scene.v());
}
});
PackManager.v()
.getPack("wjtp")
.add(transform);
}
/**
* Reads basic classes specified by file {@link #BASIC_CLASSES} and
* adds them to {@code scene}.
*/
private static void addBasicClasses(Scene scene) {
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
JavaType type = mapper.getTypeFactory()
.constructCollectionType(List.class, String.class);
try {
InputStream content = SootWorldBuilder.class
.getClassLoader()
.getResourceAsStream(BASIC_CLASSES);
List classNames = mapper.readValue(content, type);
classNames.forEach(name -> scene.addBasicClass(name, HIERARCHY));
} catch (IOException e) {
throw new SootFrontendException("Failed to read Soot basic classes", e);
}
}
/**
* Add classes in reflection log to the scene.
* Tai-e's ClassHierarchy depends on Soot's Scene, which does not change
* after hierarchy's construction, thus we need to add the classes
* in the reflection log before starting Soot.
*
* TODO: this is a tentative solution. We should remove it and use other
* way to load basic classes in the reflection log, so that world builder
* does not depend on analyses to be executed.
*
* @param analyses the analyses to be executed
* @param scene the Soot's scene
*/
private static void addReflectionLogClasses(List analyses, Scene scene) {
analyses.forEach(config -> {
if (config.getId().equals(PointerAnalysis.ID)) {
String path = config.getOptions().getString("reflection-log");
if (path != null) {
LogItem.load(path).forEach(item -> {
// add target class
String target = item.target;
String targetClass;
if (target.startsWith("<")) {
targetClass = StringReps.getClassNameOf(target);
} else {
targetClass = target;
}
if (StringReps.isArrayType(targetClass)) {
targetClass = StringReps.getBaseTypeNameOf(target);
}
scene.addBasicClass(targetClass);
});
}
}
});
}
private void build(Options options, Scene scene) {
World.reset();
World world = new World();
World.set(world);
// options will be used during World building, thus it should be
// set at first.
world.setOptions(options);
// initialize class hierarchy
ClassHierarchy hierarchy = new ClassHierarchyImpl();
SootClassLoader loader = new SootClassLoader(
scene, hierarchy, options.isAllowPhantom());
hierarchy.setDefaultClassLoader(loader);
hierarchy.setBootstrapClassLoader(loader);
world.setClassHierarchy(hierarchy);
// initialize type manager
TypeSystem typeSystem = new TypeSystemImpl(hierarchy);
world.setTypeSystem(typeSystem);
// initialize converter
Converter converter = new Converter(loader, typeSystem);
loader.setConverter(converter);
// build classes in hierarchy
buildClasses(hierarchy, scene);
// set main method
if (options.getMainClass() != null) {
if (scene.hasMainClass()) {
world.setMainMethod(
converter.convertMethod(scene.getMainMethod()));
} else {
logger.warn("Warning: main class '{}'" +
" does not have main(String[]) method!",
options.getMainClass());
}
} else {
logger.warn("Warning: main class was not given!");
}
// set implicit entries
world.setImplicitEntries(implicitEntries.stream()
.map(hierarchy::getJREMethod)
// some implicit entries may not exist in certain JDK version,
// thus we filter out null
.filter(Objects::nonNull)
.toList());
// initialize IR builder
world.setNativeModel(getNativeModel(typeSystem, hierarchy, options));
IRBuilder irBuilder = new IRBuilder(converter);
world.setIRBuilder(irBuilder);
if (options.isPreBuildIR()) {
irBuilder.buildAll(hierarchy);
}
}
protected static void buildClasses(ClassHierarchy hierarchy, Scene scene) {
// TODO: parallelize?
new ArrayList<>(scene.getClasses()).forEach(c ->
hierarchy.getDefaultClassLoader().loadClass(c.getName()));
}
private static void runSoot(String[] args) {
try {
soot.Main.v().run(args);
} catch (SootResolver.SootClassNotFoundException e) {
throw new RuntimeException(e.getMessage()
.replace("is your soot-class-path set",
"are your class path and class name given"));
} catch (AssertionError e) {
if (e.getStackTrace()[0].toString()
.startsWith("soot.SootResolver.resolveClass")) {
throw new RuntimeException("Exception thrown by class resolver," +
" are your class path and class name given properly?", e);
}
throw e;
} catch (Exception e) {
if (e.getStackTrace()[0].getClassName().startsWith("soot.JastAdd")) {
throw new RuntimeException("""
Soot frontend failed to parse input Java source file(s).
This exception may be caused by:
1. syntax or semantic errors in the source code. In this case, please fix the errors.
2. language features introduced by Java 8+ in the source code.
In this case, you could either compile the source code to bytecode (*.class)
or rewrite the code by using old features.""", e);
}
throw e;
}
}
}