使用 SootUp 生成 Java 代码调用图

版本信息:
– 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();
        }
    }
上一篇
下一篇