版本信息:
– Java 17
– SootUp 1.2 [1]
前言
Q:SootUp 和 Soot 有什么区别?
A:Soot 配置过于复杂,SootUp 更加现代化,使用起来也更方便,省去了大量繁琐操作,有点像 Spring 和 Springboot 之前的关系
Q:用什么来生成调用图?
A:从功能上来看,SootUp 支持从源代码、class 文件、jar 文件等输入生成调用图[2],但是实际上对于源代码和 class 文件的解析做的不好,存在一些 bug(也可能只是我菜)。从我的几次尝试来看,使用 Jar 作为输入最为合适
Q:为什么用 1.2 的老版本而不是 2.0 的新版本
A:我也想用啊,但是 2.0 加载代码的时候总是遇到各种各样的错误,网上能查到的资料也少,最后在 b 站找到一个 1.2 的教程[3],试了一下能用所以就使用老版本了。顺带一提,1.2 和 2.0 包差异极大,官方文档也不明朗,除非特殊需求,否则直接用老版本就行,又不是不能用。
Maven 依赖
<dependencies>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.core</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.java.core</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.java.sourcecode</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.java.bytecode</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.jimple.parser</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.callgraph</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.analysis</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
使用 SootUp
本质上分成四个步骤:
1. 加载源
2. 从源创建 View
3. 从 View 获得类和方法
4. 遍历类和方法,生成调用图
1. 加载代码源
可以直接从 Jar 加载,并且指定语言级别(可选)
// 加载 Jar
AnalysisInputLocation inputLocation = new JavaClassPathAnalysisInputLocation("myCode.jar");
// 加载并设置语言级别
AnalysisInputLocation inputLocation =
new MultiReleaseJarAnalysisInputLocation("myCode.jar", new JavaLanguage(10) );
2. 创建 View
View
可以使用一个或者多个 AnalysisInputLocation
作为参数来创建
JavaView view = new JavaView(inputLocation);
3. 从 View 获取类和方法签名
既可以由我们定义好方法签名以后从 View
中获取指定的方法[4],也可以直接从 View 中获取 JavaSootClass
的集合,然后从类中获取 JavaSootMethod
的集合,实现遍历。
JavaView view = new JavaView(inputLocation);
// 类集合
Collection<JavaSootClass> classes = view.getClasses();
// 方法集合
for (JavaSootClass clz : classes) {
Set<JavaSootMethod> methods = clz.getMethods();
}
4. 创建调用图
调用图支持两种算法[5],这里使用RTA。
Set<JavaSootMethod> methods = clz.getMethods();
// 遍历方法
List<MethodSignature> signatures = new ArrayList<>();
for (JavaSootMethod method : methods) {
signatures.add(method.getSignature());
}
// 当前类的调用图
CallGraphAlgorithm rta = new RapidTypeAnalysisAlgorithm(view);
CallGraph cg = rta.initialize(signatures);
// 遍历每一个方法签名
for (MethodSignature signature : signatures) {
// 获得每一个方法的调用关系
Set<MethodSignature> targetMethods = cg.callsFrom(signature);
}
然后就可以从 MethodSignature
里得到方法、被调用方法的方法签名了。按照这个思路遍历就能得到整个项目的调用图
参考代码
以下方法将 Jar 路径、软件名、软件版本作为参数,生成调用图并保存.csv 文件
public static void generate_call_graph_csv(String jarPath, String name, String version) {
// 从 Jar 创建 view
AnalysisInputLocation inputLocation = new JavaClassPathAnalysisInputLocation( jarPath);
JavaView view = new JavaView(inputLocation);
// 调用图算法
CallGraphAlgorithm rta = new RapidTypeAnalysisAlgorithm(view);
// 获得类
Collection<JavaSootClass> classes = view.getClasses();
Path csvPath = Paths.get("调用图-" + name + "-" + version + ".csv");
try (BufferedWriter writer = Files.newBufferedWriter(csvPath,
java.nio.file.StandardOpenOption.CREATE,
java.nio.file.StandardOpenOption.TRUNCATE_EXISTING)) {
// 写入CSV标题行
writer.write("软件,版本,包,类,返回值,方法名,方法参数,调用包,调用类,返回值,方法名,方法参数");
writer.newLine();
int classSize = classes.size();
int current = 1;
for (JavaSootClass clz : classes) {
System.out.println("Processing class " + current + "/" + classSize + ": " + clz.getName());
Set<JavaSootMethod> methods = clz.getMethods();
// 遍历方法
List<MethodSignature> signatures = new ArrayList<>();
for (JavaSootMethod method : methods) {
signatures.add(method.getSignature());
}
// 当前类的调用图
CallGraph cg = rta.initialize(signatures);
// 遍历每一个方法签名
for (MethodSignature signature : signatures) {
Set<MethodSignature> targetMethods = cg.callsFrom(signature);
String sourcePackage = signature.getDeclClassType().getPackageName().toString();
String sourceClass = signature.getDeclClassType().getClassName();
String sourceReturnType = signature.getType().toString();
String sourceName = signature.getName();
String sourceParameterTypes = signature.getParameterTypes().stream().map(Type::toString)
.collect(Collectors.joining(";"));
for (MethodSignature targetMethod : targetMethods) {
String targetPackage = targetMethod.getDeclClassType().getPackageName().toString();
String targetClass = targetMethod.getDeclClassType().getClassName();
String targetReturnType = targetMethod.getType().toString();
String targetName = targetMethod.getName();
String targetParameterTypes = targetMethod.getParameterTypes().stream().map(Type::toString)
.collect(Collectors.joining(";"));
// 构建CSV行并写入文件
String csvLine = String.format("%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s",
name, version,
sourcePackage, sourceClass, sourceReturnType, sourceName, sourceParameterTypes,
targetPackage, targetClass, targetReturnType, targetName, targetParameterTypes);
writer.write(csvLine);
writer.newLine();
}
}
current++;
}
System.out.println("CSV文件已保存到: " + csvPath.toAbsolutePath());
} catch (IOException e) {
System.err.println("保存CSV文件时出错: " + e.getMessage());
e.printStackTrace();
}
}